diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..ce45cb283 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# https://github.com/jenkinsci/ec2-plugin/pull/1011 Spotless +98fc9a96b65375210d2dd3f9285d924d91582f3f diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 4e0774d51..9440b1807 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -2,6 +2,6 @@ io.jenkins.tools.incrementals git-changelist-maven-extension - 1.8 + 1.13 diff --git a/Jenkinsfile b/Jenkinsfile index 92ac6abc4..d91b58ac9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,4 +1,3 @@ buildPlugin(useContainerAgent: true, configurations: [ - [platform: 'linux', jdk: '17'], - [ platform: 'linux', jdk: '21' ], + [ platform: 'linux', jdk: 25 ] ]) diff --git a/README.md b/README.md index 3aeaff214..c82098f6d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # ec2-plugin -[![Jenkins](https://ci.jenkins.io/job/Plugins/job/ec2-plugin/job/master/badge/icon)](https://ci.jenkins.io/job/Plugins/job/ec2-plugin/job/master/) + +[![Jenkins](https://ci.jenkins.io/buildStatus/icon?job=Plugins%2Fec2-plugin%2Fmaster)](https://ci.jenkins.io/job/Plugins/job/ec2-plugin/job/master/) [![Jenkins Plugin](https://img.shields.io/jenkins/plugin/v/ec2.svg)](https://plugins.jenkins.io/ec2) [![GitHub release](https://img.shields.io/github/release/jenkinsci/ec2-plugin.svg?label=changelog)](https://github.com/jenkinsci/ec2-plugin/releases/latest) [![Gitter](https://badges.gitter.im/ec2-plugin/Lobby.svg)](https://gitter.im/ec2-plugin/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) @@ -33,8 +34,7 @@ # Introduction Allow Jenkins to start agents on -[EC2](http://aws.amazon.com/ec2/) or -[Eucalyptus](https://www.eucalyptus.cloud/) on demand, and +[EC2](http://aws.amazon.com/ec2/) on demand, and kill them as they get unused. With this plugin, if Jenkins notices that your build cluster is @@ -52,14 +52,25 @@ the main "Manage Jenkins" \> "Configure System" page, and scroll down near the bottom to the "Cloud" section. There, you click the "Add a new cloud" button, and select the "Amazon EC2" option. This will display the UI for configuring the EC2 plugin.  Then enter the Access Key and Secret -Access Key which act like a username/password (see IAM section). Because -of the way EC2 works, you also need to have an RSA private key that the +Access Key which act like a username/password (see IAM section). + +Because of the way EC2 works, you also need to have an RSA private key that the cloud has the other half for, to permit sshing into the instances that are started. Please use the AWS console or any other tool of your choice -to generate the private key to interactively log in to EC2 instances. +to generate the private key to interactively log in to EC2 instances. + +Once you have generated the needed private key you must either store it as +a Jenkins `SSH Private Key` credential (and select that credential in your cloud +config). + +If you do not want to create a new Jenkins credential you may alterantively store it +in plain text on disk, indicating its file path via the Jenkins system property +`hudson.plugins.ec2.EC2Cloud.sshPrivateKeyFilePath`. If this system property has a non-empty value then +it will override the ssh credential specified in the cloud configuration page. This +approach works well for `k8s` secrets that are mounted in a jenkins container for example. -Once you have put in your Access Key and Secret Access Key, select a -region for the cloud (not shown in screenshot). You may define only one +Once you have put in your Access Key, Secret Access Key, and configured an ssh private key +select a region for the cloud (not shown in screenshot). You may define only one cloud for each region, and the regions offered in the UI will show only the regions that you don't already have clouds defined for them. @@ -283,12 +294,12 @@ console](https://wiki.jenkins.io/display/JENKINS/Jenkins+Script+Console), example: ```groovy -import com.amazonaws.services.ec2.model.InstanceType +import software.amazon.awssdk.services.ec2.model.InstanceType import com.cloudbees.jenkins.plugins.awscredentials.AWSCredentialsImpl import com.cloudbees.plugins.credentials.* import com.cloudbees.plugins.credentials.domains.Domain import hudson.model.* -import hudson.plugins.ec2.AmazonEC2Cloud +import hudson.plugins.ec2.EC2Cloud import hudson.plugins.ec2.AMITypeData import hudson.plugins.ec2.EC2Tag import hudson.plugins.ec2.SlaveTemplate @@ -305,7 +316,7 @@ def sshPortToConnectWith = '22' // store parameters def slaveTemplateUsEast1Parameters = [ ami: 'ami-AAAAAAAA', - associatePublicIp: false, + associateIpStrategy: AssociateIpStrategy.valueOf('PRIVATE_IP'), spotConfig: null, connectBySSHProcess: false, connectUsingPublicIp: false, @@ -340,6 +351,7 @@ def slaveTemplateUsEast1Parameters = [ metadataEndpointEnabled: true, metadataTokensRequired: true, // `true` enforces IMDSv2 only (over IMDSv1), an important AWS security best practice metadataHopsLimit: 1, + enclaveEnabled: true, // launches the instance with Nitro Enclave enabled minimumNumberOfInstances: 0, minimumNumberOfSpareInstances: 0, maxTotalUses: -1, @@ -352,7 +364,7 @@ def slaveTemplateUsEast1Parameters = [ nodeProperties: null ] -def AmazonEC2CloudParameters = [ +def EC2CloudParameters = [ name: 'MyCompany', credentialsId: 'jenkins-aws-key', instanceCapStr: '2', @@ -434,7 +446,7 @@ SlaveTemplate slaveTemplateUsEast1 = new SlaveTemplate( slaveTemplateUsEast1Parameters.deleteRootOnTermination, slaveTemplateUsEast1Parameters.useEphemeralDevices, slaveTemplateUsEast1Parameters.launchTimeoutStr, - slaveTemplateUsEast1Parameters.associatePublicIp, + slaveTemplateUsEast1Parameters.associateIpStrategy, slaveTemplateUsEast1Parameters.customDeviceMapping, slaveTemplateUsEast1Parameters.connectBySSHProcess, slaveTemplateUsEast1Parameters.monitoring, @@ -449,16 +461,17 @@ SlaveTemplate slaveTemplateUsEast1 = new SlaveTemplate( slaveTemplateUsEast1Parameters.metadataEndpointEnabled, slaveTemplateUsEast1Parameters.metadataTokensRequired, slaveTemplateUsEast1Parameters.metadataHopsLimit, + slaveTemplateUsEast1Parameters.enclaveEnabled, ) -// https://javadoc.jenkins.io/plugin/ec2/index.html?hudson/plugins/ec2/AmazonEC2Cloud.html -AmazonEC2Cloud amazonEC2Cloud = new AmazonEC2Cloud( - AmazonEC2CloudParameters.name, - AmazonEC2CloudParameters.useInstanceProfileForCredentials, - AmazonEC2CloudParameters.credentialsId, - AmazonEC2CloudParameters.region, - AmazonEC2CloudParameters.privateKey, - AmazonEC2CloudParameters.instanceCapStr, +// https://javadoc.jenkins.io/plugin/ec2/hudson/plugins/ec2/EC2Cloud.html +EC2Cloud ec2Cloud = new EC2Cloud( + EC2CloudParameters.name, + EC2CloudParameters.useInstanceProfileForCredentials, + EC2CloudParameters.credentialsId, + EC2CloudParameters.region, + EC2CloudParameters.privateKey, + EC2CloudParameters.instanceCapStr, [slaveTemplateUsEast1], '', '' @@ -477,7 +490,7 @@ def store = jenkins.getExtensionList('com.cloudbees.plugins.credentials.SystemCr store.addCredentials(domain, aWSCredentialsImpl) // add cloud configuration to Jenkins -jenkins.clouds.add(amazonEC2Cloud) +jenkins.clouds.add(ec2Cloud) // save current Jenkins state to disk jenkins.save() @@ -493,7 +506,7 @@ Example: ```java // Assuming on the Jenkins instance, there exists an EC2Cloud with the name "AwsCloud" - AmazonEC2Cloud cloud = (AmazonEC2Cloud) Jenkins.get().clouds.stream().filter(cloud1 -> Objects.equals(cloud.getDisplayName(), "AwsCloud")).findFirst().get(); + EC2Cloud cloud = (EC2Cloud) Jenkins.get().clouds.stream().filter(cloud1 -> Objects.equals(cloud.getDisplayName(), "AwsCloud")).findFirst().get(); SlaveTemplate template = new SlaveTemplate(/*constructor*/); // View available constructors at https://github.com/jenkinsci/ec2-plugin/blob/master/src/main/java/hudson/plugins/ec2/SlaveTemplate.java diff --git a/pom.xml b/pom.xml index 2e02b6b9e..e7472779c 100644 --- a/pom.xml +++ b/pom.xml @@ -22,216 +22,218 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --> - - - 4.0.0 - - - org.jenkins-ci.plugins - plugin - 4.88 - - + 4.0.0 + + org.jenkins-ci.plugins + plugin + 5.24 + + - ec2 - ${changelist} - hpi + ec2 + ${changelist} + hpi + Amazon EC2 plugin + This is a Jenkins plugin to support ephemeral Jenkins agents on Amazon EC2 or other EC2-compatible clouds + https://github.com/jenkinsci/${project.artifactId}-plugin - Amazon EC2 plugin - This is a Jenkins plugin to support ephemeral Jenkins agents on Amazon EC2 or other EC2-compatible clouds - https://github.com/jenkinsci/${project.artifactId}-plugin - - - The MIT License - https://opensource.org/licenses/MIT - repo - - + + + The MIT License + https://opensource.org/licenses/MIT + repo + + - - - - thoulen - F Manfred Furuholen - fabrizio.manfredi@gmail.com - - - julienduchesne - Julien Duchesne - julienduchesne@live.com - - - raihaan - Raihaan Shouhell - raihaanhimself@gmail.com - - + + + + thoulen + F Manfred Furuholen + fabrizio.manfredi@gmail.com + + + julienduchesne + Julien Duchesne + julienduchesne@live.com + + + raihaan + Raihaan Shouhell + raihaanhimself@gmail.com + + - - scm:git:https://github.com/${gitHubRepo}.git - scm:git:git@github.com:${gitHubRepo}.git - https://github.com/${gitHubRepo} - ${scmTag} - + + scm:git:https://github.com/${gitHubRepo}.git + scm:git:git@github.com:${gitHubRepo}.git + ${scmTag} + https://github.com/${gitHubRepo} + - - 999999-SNAPSHOT - 2.414.3 - jenkinsci/${project.artifactId}-plugin - 1626 - + + 999999-SNAPSHOT + + 2.479 + ${jenkins.baseline}.3 + jenkinsci/${project.artifactId}-plugin + asn-one,mbassador,smbj + true + 1885 + false + false + + - - com.hierynomus - smbj - 0.13.0 - - - org.bouncycastle - bcprov-jdk18on - - - org.slf4j - slf4j-api - - - - - org.jenkins-ci.plugins - credentials - - - org.jenkins-ci.plugins - aws-credentials - - - org.jenkins-ci.plugins - ssh-credentials - - - org.jenkins-ci.plugins - bouncycastle-api - - - org.jenkins-ci.plugins.aws-java-sdk - aws-java-sdk-ec2 - 1.12.696-451.v0651a_da_9ca_ec - - - org.jenkins-ci.plugins.aws-java-sdk - aws-java-sdk-minimal - 1.12.767-467.vb_e93f0c614b_6 - - - org.jenkins-ci.plugins - node-iterator-api - 55.v3b_77d4032326 - - - org.jenkins-ci.plugins - apache-httpcomponents-client-4-api - - - org.jenkins-ci.plugins - command-launcher - - - org.jenkins-ci.plugins - trilead-api - - - org.jenkins-ci.plugins.workflow - workflow-step-api - - - org.mockito - mockito-core - test - - - junit - junit - test - - - org.jenkins-ci.plugins.workflow - workflow-cps - test - - - org.jenkins-ci.plugins.workflow - workflow-durable-task-step - test - - - org.jenkins-ci.plugins.workflow - workflow-job - test - - - org.jenkins-ci.plugins.workflow - workflow-api - test - - - io.jenkins - configuration-as-code - test - - - io.jenkins.configuration-as-code - test-harness - test - - - org.testcontainers - testcontainers - 1.20.2 - test - - - org.kohsuke - libzfs - 0.8 - + + io.jenkins.tools.bom + bom-${jenkins.baseline}.x + 5054.v620b_5d2b_d5e6 + pom + import + - - - - io.jenkins.tools.bom - bom-2.414.x - 2982.vdce2153031a_0 - import - pom - - - + + + + + com.hierynomus + smbj + 0.14.0 + + + org.bouncycastle + bcprov-jdk18on + + + org.slf4j + slf4j-api + + + + + io.jenkins.plugins.aws-java-sdk2 + aws-java-sdk2-core + + + io.jenkins.plugins.aws-java-sdk2 + aws-java-sdk2-ec2 + + + io.jenkins.plugins.mina-sshd-api + mina-sshd-api-core + + + io.jenkins.plugins.mina-sshd-api + mina-sshd-api-scp + + + org.jenkins-ci.plugins + apache-httpcomponents-client-4-api + + + org.jenkins-ci.plugins + aws-credentials + + + org.jenkins-ci.plugins + bouncycastle-api + + + org.jenkins-ci.plugins + command-launcher + + + org.jenkins-ci.plugins + credentials + + + org.jenkins-ci.plugins + node-iterator-api + + + org.jenkins-ci.plugins + ssh-credentials + + + org.jenkins-ci.plugins.workflow + workflow-step-api + + + io.jenkins + configuration-as-code + test + + + io.jenkins.configuration-as-code + test-harness + test + + + org.jenkins-ci.plugins.workflow + workflow-api + test + + + org.jenkins-ci.plugins.workflow + workflow-cps + test + + + org.jenkins-ci.plugins.workflow + workflow-durable-task-step + test + + + org.jenkins-ci.plugins.workflow + workflow-job + test + + + org.junit-pioneer + junit-pioneer + 2.3.0 + test + + + org.mockito + mockito-junit-jupiter + test + + + org.testcontainers + junit-jupiter + 1.21.3 + test + + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + - - - repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ - - - - - repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ - - + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + - - - - org.jenkins-ci.tools - maven-hpi-plugin - true - - 1.45 - - - - + + + + org.jenkins-ci.tools + maven-hpi-plugin + true + + 1.45 + + + + diff --git a/src/main/java/hudson/plugins/ec2/AMITypeData.java b/src/main/java/hudson/plugins/ec2/AMITypeData.java index 6d27b95c8..3a4096f2c 100644 --- a/src/main/java/hudson/plugins/ec2/AMITypeData.java +++ b/src/main/java/hudson/plugins/ec2/AMITypeData.java @@ -1,7 +1,6 @@ package hudson.plugins.ec2; import hudson.model.AbstractDescribableImpl; - import java.util.concurrent.TimeUnit; public abstract class AMITypeData extends AbstractDescribableImpl { @@ -11,16 +10,20 @@ public abstract class AMITypeData extends AbstractDescribableImpl { public abstract boolean isMac(); + public abstract boolean isSSHAgent(); + + public abstract boolean isWinRMAgent(); + public abstract String getBootDelay(); public int getBootDelayInMillis() { - if (getBootDelay() == null) + if (getBootDelay() == null) { return 0; + } try { return (int) TimeUnit.SECONDS.toMillis(Integer.parseInt(getBootDelay())); } catch (NumberFormatException nfe) { return 0; } } - } diff --git a/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java b/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java index 0441987a4..745a2d092 100644 --- a/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/AmazonEC2Cloud.java @@ -23,222 +23,67 @@ */ package hudson.plugins.ec2; -import com.amazonaws.SdkClientException; import hudson.Extension; -import hudson.Util; -import hudson.model.Failure; -import hudson.model.ItemGroup; -import hudson.plugins.ec2.util.AmazonEC2Factory; -import hudson.util.FormValidation; -import hudson.util.ListBoxModel; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; import java.util.List; -import java.util.Locale; -import java.util.logging.Level; -import java.util.logging.Logger; -import edu.umd.cs.findbugs.annotations.Nullable; -import javax.servlet.ServletException; - -import jenkins.model.Jenkins; - -import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.DataBoundSetter; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.interceptor.RequirePOST; - -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.model.DescribeRegionsResult; -import com.amazonaws.services.ec2.model.Region; -import org.kohsuke.stapler.verb.POST; /** - * The original implementation of {@link EC2Cloud}. - * - * @author Kohsuke Kawaguchi + * @deprecated use {@link EC2Cloud} */ +@Deprecated public class AmazonEC2Cloud extends EC2Cloud { - private final static Logger LOGGER = Logger.getLogger(AmazonEC2Cloud.class.getName()); - - /** - * Represents the region. Can be null for backward compatibility reasons. - */ - private String region; - - private String altEC2Endpoint; - - private boolean noDelayProvisioning; - @DataBoundConstructor - public AmazonEC2Cloud(String name, boolean useInstanceProfileForCredentials, String credentialsId, String region, String privateKey, String sshKeysCredentialsId, String instanceCapStr, List templates, String roleArn, String roleSessionName) { - super(name, useInstanceProfileForCredentials, credentialsId, privateKey, sshKeysCredentialsId, instanceCapStr, templates, roleArn, roleSessionName); - this.region = region; - } - - @Deprecated - public AmazonEC2Cloud(String name, boolean useInstanceProfileForCredentials, String credentialsId, String region, String privateKey, String instanceCapStr, List templates, String roleArn, String roleSessionName) { - super(name, useInstanceProfileForCredentials, credentialsId, privateKey, instanceCapStr, templates, roleArn, roleSessionName); - this.region = region; - } - - /** - * @deprecated Use public field "name" instead. - */ - @Deprecated - public String getCloudName() { - return name; - } - - public String getRegion() { - if (region == null) - region = DEFAULT_EC2_HOST; // Backward compatibility - // Handles pre 1.14 region names that used the old AwsRegion enum, note we don't change - // the region here to keep the meta-data compatible in the case of a downgrade (is that right?) - if (region.indexOf('_') > 0) - return region.replace('_', '-').toLowerCase(Locale.ENGLISH); - return region; - } - - public static URL getEc2EndpointUrl(String region) { - try { - return new URL("https://" + getAwsPartitionHostForService(region, "ec2")); - } catch (MalformedURLException e) { - throw new Error(e); // Impossible - } - } - - @Override - public URL getEc2EndpointUrl() { - return getEc2EndpointUrl(getRegion()); - } - - @Override - public URL getS3EndpointUrl() { - try { - return new URL("https://" + getAwsPartitionHostForService(getRegion(), "s3") + "/"); - } catch (MalformedURLException e) { - throw new Error(e); // Impossible - } + public AmazonEC2Cloud( + String name, + boolean useInstanceProfileForCredentials, + String credentialsId, + String region, + String privateKey, + String sshKeysCredentialsId, + String instanceCapStr, + List templates, + String roleArn, + String roleSessionName) { + super( + name, + useInstanceProfileForCredentials, + credentialsId, + region, + privateKey, + sshKeysCredentialsId, + instanceCapStr, + templates, + roleArn, + roleSessionName); } - public boolean isNoDelayProvisioning() { - return noDelayProvisioning; - } - - @DataBoundSetter - public void setNoDelayProvisioning(boolean noDelayProvisioning) { - this.noDelayProvisioning = noDelayProvisioning; - } - - public String getAltEC2Endpoint() { - return altEC2Endpoint; - } - - @DataBoundSetter - public void setAltEC2Endpoint(String altEC2Endpoint) { - this.altEC2Endpoint = altEC2Endpoint; - } - - @Override - protected AWSCredentialsProvider createCredentialsProvider() { - return createCredentialsProvider(isUseInstanceProfileForCredentials(), getCredentialsId(), getRoleArn(), getRoleSessionName(), getRegion()); + public AmazonEC2Cloud( + String name, + boolean useInstanceProfileForCredentials, + String credentialsId, + String region, + String privateKey, + String instanceCapStr, + List templates, + String roleArn, + String roleSessionName) { + super( + name, + useInstanceProfileForCredentials, + credentialsId, + region, + privateKey, + instanceCapStr, + templates, + roleArn, + roleSessionName); } @Extension public static class DescriptorImpl extends EC2Cloud.DescriptorImpl { - @Override public String getDisplayName() { - return "Amazon EC2"; - } - - @POST - public FormValidation doCheckCloudName(@QueryParameter String value) { - if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { - return FormValidation.ok(); - } - try { - Jenkins.checkGoodName(value); - } catch (Failure e) { - return FormValidation.error(e.getMessage()); - } - return FormValidation.ok(); - } - - @POST - public FormValidation doCheckAltEC2Endpoint(@QueryParameter String value) { - if (Util.fixEmpty(value) != null) { - try { - new URL(value); - } catch (MalformedURLException ignored) { - return FormValidation.error(Messages.AmazonEC2Cloud_MalformedUrl()); - } - } - return FormValidation.ok(); - } - - @RequirePOST - public ListBoxModel doFillRegionItems( - @QueryParameter String altEC2Endpoint, - @QueryParameter boolean useInstanceProfileForCredentials, - @QueryParameter String credentialsId) - - throws IOException, ServletException { - ListBoxModel model = new ListBoxModel(); - if (Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { - try { - AWSCredentialsProvider credentialsProvider = createCredentialsProvider(useInstanceProfileForCredentials, - credentialsId); - AmazonEC2 client = AmazonEC2Factory.getInstance().connect(credentialsProvider, determineEC2EndpointURL(altEC2Endpoint)); - DescribeRegionsResult regions = client.describeRegions(); - List regionList = regions.getRegions(); - for (Region r : regionList) { - String name = r.getRegionName(); - model.add(name, name); - } - } catch (SdkClientException ex) { - // Ignore, as this may happen before the credentials are specified - } - } - return model; - } - - // Will use the alternate EC2 endpoint if provided by the UI (via a @QueryParameter field), or use the default - // value if not specified. - //VisibleForTesting - URL determineEC2EndpointURL(@Nullable String altEC2Endpoint) throws MalformedURLException { - if (Util.fixEmpty(altEC2Endpoint) == null) { - return new URL(DEFAULT_EC2_ENDPOINT); - } - try { - return new URL(altEC2Endpoint); - } catch (MalformedURLException e) { - LOGGER.log(Level.WARNING, "The alternate EC2 endpoint is malformed ({0}). Using the default endpoint ({1})", new Object[]{altEC2Endpoint, DEFAULT_EC2_ENDPOINT}); - return new URL(DEFAULT_EC2_ENDPOINT); - } - } - - @RequirePOST - public FormValidation doTestConnection( - @AncestorInPath ItemGroup context, - @QueryParameter String region, - @QueryParameter boolean useInstanceProfileForCredentials, - @QueryParameter String credentialsId, - @QueryParameter String sshKeysCredentialsId, - @QueryParameter String roleArn, - @QueryParameter String roleSessionName) - - throws IOException, ServletException { - - if (Util.fixEmpty(region) == null) { - region = DEFAULT_EC2_HOST; - } - - return super.doTestConnection(context, getEc2EndpointUrl(region), useInstanceProfileForCredentials, credentialsId, sshKeysCredentialsId, roleArn, roleSessionName, region); + return "Amazon EC2 (Deprecated - use EC2 instead)"; } } } diff --git a/src/main/java/hudson/plugins/ec2/AssociateIPStrategy.java b/src/main/java/hudson/plugins/ec2/AssociateIPStrategy.java new file mode 100644 index 000000000..236e696f4 --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/AssociateIPStrategy.java @@ -0,0 +1,34 @@ +package hudson.plugins.ec2; + +/** + * + * Strategy for associating a public IPv4 address with the instance’s primary network interface at launch. + * + * @see AWS Network Interface Specification + * @see AWS Subnet API + * */ +public enum AssociateIPStrategy { + SUBNET("Inherit from Subnet"), + PUBLIC_IP("Public IP"), + PRIVATE_IP("Private IP"), + DEFAULT("Default"); + + private final String displayText; + + AssociateIPStrategy(String displayText) { + this.displayText = displayText; + } + + public String getDisplayText() { + return this.displayText; + } + + /** + * For backwards compatibility. + * @param associatePublicIp whether or not to use a public ip to establish a connection. + * @return an {@link AssociateIPStrategy} based on provided parameters that keeps {@code associatePublicIp} behavior. + */ + public static AssociateIPStrategy backwardsCompatible(boolean associatePublicIp) { + return associatePublicIp ? PUBLIC_IP : DEFAULT; + } +} diff --git a/src/main/java/hudson/plugins/ec2/CloudHelper.java b/src/main/java/hudson/plugins/ec2/CloudHelper.java index 82344f194..335ce37b5 100644 --- a/src/main/java/hudson/plugins/ec2/CloudHelper.java +++ b/src/main/java/hudson/plugins/ec2/CloudHelper.java @@ -1,37 +1,38 @@ package hudson.plugins.ec2; -import com.amazonaws.AmazonClientException; -import com.amazonaws.AmazonServiceException; -import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.model.AvailabilityZone; -import com.amazonaws.services.ec2.model.DescribeAvailabilityZonesResult; -import com.amazonaws.services.ec2.model.DescribeImagesRequest; -import com.amazonaws.services.ec2.model.DescribeInstancesRequest; -import com.amazonaws.services.ec2.model.Image; -import com.amazonaws.services.ec2.model.Instance; -import com.amazonaws.services.ec2.model.Reservation; -import java.util.ArrayList; -import org.apache.commons.lang.StringUtils; - import edu.umd.cs.findbugs.annotations.CheckForNull; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.logging.Logger; - -import static hudson.plugins.ec2.EC2Cloud.EC2_REQUEST_EXPIRED_ERROR_CODE; +import org.apache.commons.lang.StringUtils; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.ec2.Ec2Client; +import software.amazon.awssdk.services.ec2.model.AvailabilityZone; +import software.amazon.awssdk.services.ec2.model.DescribeAvailabilityZonesResponse; +import software.amazon.awssdk.services.ec2.model.DescribeImagesRequest; +import software.amazon.awssdk.services.ec2.model.DescribeInstancesRequest; +import software.amazon.awssdk.services.ec2.model.Image; +import software.amazon.awssdk.services.ec2.model.Instance; +import software.amazon.awssdk.services.ec2.model.Reservation; final class CloudHelper { private static final Logger LOGGER = Logger.getLogger(CloudHelper.class.getName()); - static Instance getInstanceWithRetry(String instanceId, EC2Cloud cloud) throws AmazonClientException, InterruptedException { + static Instance getInstanceWithRetry(String instanceId, EC2Cloud cloud) throws SdkException, InterruptedException { // Sometimes even after a successful RunInstances, DescribeInstances // returns an error for a few seconds. We do a few retries instead of // failing instantly. See [JENKINS-15319]. for (int i = 0; i < 5; i++) { try { return getInstance(instanceId, cloud); - } catch (AmazonServiceException e) { - if (e.getErrorCode().equals("InvalidInstanceID.NotFound") || EC2_REQUEST_EXPIRED_ERROR_CODE.equals(e.getErrorCode())) { + } catch (AwsServiceException e) { + if ("InvalidInstanceID.NotFound".equals(e.awsErrorDetails().errorCode()) + || EC2Cloud.EC2_REQUEST_EXPIRED_ERROR_CODE.equals( + e.awsErrorDetails().errorCode())) { // retry in 5 seconds. Thread.sleep(5000); continue; @@ -44,46 +45,74 @@ static Instance getInstanceWithRetry(String instanceId, EC2Cloud cloud) throws A } @CheckForNull - static Instance getInstance(String instanceId, EC2Cloud cloud) throws AmazonClientException { - if (StringUtils.isEmpty(instanceId) || cloud == null) + static Instance getInstance(String instanceId, EC2Cloud cloud) throws SdkException { + if (StringUtils.isEmpty(instanceId) || cloud == null) { return null; + } - DescribeInstancesRequest request = new DescribeInstancesRequest(); - request.setInstanceIds(Collections.singletonList(instanceId)); + DescribeInstancesRequest request = DescribeInstancesRequest.builder() + .instanceIds(Collections.singletonList(instanceId)) + .build(); - List reservations = cloud.connect().describeInstances(request).getReservations(); + List reservations = + cloud.connect().describeInstances(request).reservations(); if (reservations.size() != 1) { - String message = "Unexpected number of reservations reported by EC2 for instance id '" + instanceId + "', expected 1 result, found " + reservations + "."; - if (reservations.size() == 0) { - message += " Instance seems to be dead."; - } - LOGGER.info(message); - throw new AmazonClientException(message); + String message = "Unexpected number of reservations reported by EC2 for instance id '" + instanceId + + "', expected 1 result, found " + reservations + "."; + if (reservations.isEmpty()) { + message += " Instance seems to be dead."; + } + LOGGER.info(message); + throw SdkException.builder().message(message).build(); } Reservation reservation = reservations.get(0); - List instances = reservation.getInstances(); + List instances = reservation.instances(); if (instances.size() != 1) { - String message = "Unexpected number of instances reported by EC2 for instance id '" + instanceId + "', expected 1 result, found " + instances + "."; - if (instances.size() == 0) { - message += " Instance seems to be dead."; - } - LOGGER.info(message); - throw new AmazonClientException(message); + String message = "Unexpected number of instances reported by EC2 for instance id '" + instanceId + + "', expected 1 result, found " + instances + "."; + if (instances.isEmpty()) { + message += " Instance seems to be dead."; + } + LOGGER.info(message); + throw SdkException.builder().message(message).build(); } return instances.get(0); } + /** + * Fetches multiple instances in a single EC2 API call. More efficient than N single-instance calls. + * Instance IDs not found (e.g. terminated) are omitted from the result. + */ + static Map getInstancesBatch(List instanceIds, EC2Cloud cloud) throws SdkException { + if (instanceIds == null || instanceIds.isEmpty() || cloud == null) { + return Collections.emptyMap(); + } + Map result = new HashMap<>(); + final int chunkSize = 100; + for (int i = 0; i < instanceIds.size(); i += chunkSize) { + List chunk = instanceIds.subList(i, Math.min(i + chunkSize, instanceIds.size())); + DescribeInstancesRequest request = + DescribeInstancesRequest.builder().instanceIds(chunk).build(); + for (Reservation r : cloud.connect().describeInstances(request).reservations()) { + for (Instance inst : r.instances()) { + result.put(inst.instanceId(), inst); + } + } + } + return result; + } + @CheckForNull - static Image getAmiImage(AmazonEC2 ec2, String ami) { + static Image getAmiImage(Ec2Client ec2, String ami) { List images = Collections.singletonList(ami); List owners = Collections.emptyList(); List users = Collections.emptyList(); - DescribeImagesRequest request = new DescribeImagesRequest(); - request.setImageIds(images); - request.setOwners(owners); - request.setExecutableUsers(users); - List img = ec2.describeImages(request).getImages(); + DescribeImagesRequest.Builder requestBuilder = DescribeImagesRequest.builder(); + requestBuilder.imageIds(images); + requestBuilder.owners(owners); + requestBuilder.executableUsers(users); + List img = ec2.describeImages(requestBuilder.build()).images(); if (img == null || img.isEmpty()) { // de-registered AMI causes an empty list to be // returned. so be defensive @@ -95,14 +124,14 @@ static Image getAmiImage(AmazonEC2 ec2, String ami) { } // Retrieve the availability zones for the region connected on - static ArrayList getAvailabilityZones(AmazonEC2 ec2) { + static ArrayList getAvailabilityZones(Ec2Client ec2) { ArrayList availabilityZones = new ArrayList<>(); - DescribeAvailabilityZonesResult zones = ec2.describeAvailabilityZones(); - List zoneList = zones.getAvailabilityZones(); + DescribeAvailabilityZonesResponse zones = ec2.describeAvailabilityZones(); + List zoneList = zones.availabilityZones(); for (AvailabilityZone z : zoneList) { - availabilityZones.add(z.getZoneName()); + availabilityZones.add(z.zoneName()); } return availabilityZones; diff --git a/src/main/java/hudson/plugins/ec2/ConnectionStrategy.java b/src/main/java/hudson/plugins/ec2/ConnectionStrategy.java index f182b712d..bb49716a2 100644 --- a/src/main/java/hudson/plugins/ec2/ConnectionStrategy.java +++ b/src/main/java/hudson/plugins/ec2/ConnectionStrategy.java @@ -23,7 +23,8 @@ public String getDisplayText() { * @param associatePublicIp whether or not to associate to a public ip. * @return an {@link ConnectionStrategy} based on provided parameters. */ - public static ConnectionStrategy backwardsCompatible(boolean usePrivateDnsName, boolean connectUsingPublicIp, boolean associatePublicIp) { + public static ConnectionStrategy backwardsCompatible( + boolean usePrivateDnsName, boolean connectUsingPublicIp, boolean associatePublicIp) { if (usePrivateDnsName && !connectUsingPublicIp) { return PRIVATE_DNS; } else if (connectUsingPublicIp || associatePublicIp) { diff --git a/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java b/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java index 949124021..8c1dd850f 100644 --- a/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java +++ b/src/main/java/hudson/plugins/ec2/EC2AbstractSlave.java @@ -31,44 +31,43 @@ import hudson.model.Slave; import hudson.plugins.ec2.util.AmazonEC2Factory; import hudson.plugins.ec2.util.ResettableCountDownLatch; -import hudson.slaves.NodeProperty; import hudson.slaves.ComputerLauncher; +import hudson.slaves.NodeProperty; import hudson.slaves.RetentionStrategy; import hudson.util.ListBoxModel; - +import hudson.util.Secret; import java.io.IOException; +import java.io.Serial; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; - -import hudson.util.Secret; import jenkins.model.Jenkins; import net.sf.json.JSONObject; - import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerRequest; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.model.AvailabilityZone; -import com.amazonaws.services.ec2.model.CreateTagsRequest; -import com.amazonaws.services.ec2.model.DeleteTagsRequest; -import com.amazonaws.services.ec2.model.DescribeAvailabilityZonesResult; -import com.amazonaws.services.ec2.model.Instance; -import com.amazonaws.services.ec2.model.InstanceBlockDeviceMapping; -import com.amazonaws.services.ec2.model.InstanceStateName; -import com.amazonaws.services.ec2.model.InstanceType; -import com.amazonaws.services.ec2.model.StopInstancesRequest; -import com.amazonaws.services.ec2.model.Tag; -import com.amazonaws.services.ec2.model.TerminateInstancesRequest; +import org.kohsuke.stapler.StaplerRequest2; import org.kohsuke.stapler.verb.POST; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.ec2.Ec2Client; +import software.amazon.awssdk.services.ec2.model.AvailabilityZone; +import software.amazon.awssdk.services.ec2.model.CreateTagsRequest; +import software.amazon.awssdk.services.ec2.model.DeleteTagsRequest; +import software.amazon.awssdk.services.ec2.model.DescribeAvailabilityZonesResponse; +import software.amazon.awssdk.services.ec2.model.Instance; +import software.amazon.awssdk.services.ec2.model.InstanceBlockDeviceMapping; +import software.amazon.awssdk.services.ec2.model.InstanceStateName; +import software.amazon.awssdk.services.ec2.model.InstanceType; +import software.amazon.awssdk.services.ec2.model.StopInstancesRequest; +import software.amazon.awssdk.services.ec2.model.Tag; +import software.amazon.awssdk.services.ec2.model.TerminateInstancesRequest; /** * Agent running on EC2. @@ -80,6 +79,7 @@ public abstract class EC2AbstractSlave extends Slave { public static final Boolean DEFAULT_METADATA_SUPPORTED = Boolean.TRUE; public static final Boolean DEFAULT_METADATA_ENDPOINT_ENABLED = Boolean.TRUE; public static final Boolean DEFAULT_METADATA_TOKENS_REQUIRED = Boolean.TRUE; + public static final Boolean DEFAULT_ENCLAVE_ENABLED = Boolean.FALSE; public static final Integer DEFAULT_METADATA_HOPS_LIMIT = 1; public static final String DEFAULT_JAVA_PATH = "java"; @@ -91,6 +91,7 @@ public abstract class EC2AbstractSlave extends Slave { * Comes from {@link SlaveTemplate#initScript}. */ public final String initScript; + public final String tmpDir; public final String remoteAdmin; // e.g. 'ubuntu' @@ -117,6 +118,8 @@ public abstract class EC2AbstractSlave extends Slave { private Boolean metadataTokensRequired; private Integer metadataHopsLimit; + private Boolean enclaveEnabled; + // Temporary stuff that is obtained live from EC2 public transient String publicDNS; public transient String privateDNS; @@ -134,8 +137,8 @@ public abstract class EC2AbstractSlave extends Slave { * The time (in milliseconds) after which we will always re-fetch externally changeable EC2 data when we are asked * for it */ - protected static final long MIN_FETCH_TIME = Long.getLong("hudson.plugins.ec2.EC2AbstractSlave.MIN_FETCH_TIME", - TimeUnit.SECONDS.toMillis(20)); + protected static final long MIN_FETCH_TIME = + Long.getLong("hudson.plugins.ec2.EC2AbstractSlave.MIN_FETCH_TIME", TimeUnit.SECONDS.toMillis(20)); protected final int launchTimeout; @@ -153,11 +156,40 @@ public abstract class EC2AbstractSlave extends Slave { public transient String slaveCommandSuffix; - private transient long createdTime; + private transient Instant createdTime; public static final String TEST_ZONE = "testZone"; - public EC2AbstractSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, Mode mode, String labelString, ComputerLauncher launcher, RetentionStrategy retentionStrategy, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String javaPath, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, List tags, String cloudName, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses, Tenancy tenancy, Boolean metadataEndpointEnabled, Boolean metadataTokensRequired, Integer metadataHopsLimit, Boolean metadataSupported) + public EC2AbstractSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + Mode mode, + String labelString, + ComputerLauncher launcher, + RetentionStrategy retentionStrategy, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String javaPath, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + Tenancy tenancy, + Boolean metadataEndpointEnabled, + Boolean metadataTokensRequired, + Integer metadataHopsLimit, + Boolean metadataSupported, + Boolean enclaveEnabled) throws FormException, IOException { super(name, remoteFS, launcher); setNumExecutors(numExecutors); @@ -187,34 +219,295 @@ public EC2AbstractSlave(String name, String instanceId, String templateDescripti this.metadataTokensRequired = metadataTokensRequired; this.metadataHopsLimit = metadataHopsLimit; this.metadataSupported = metadataSupported; + this.enclaveEnabled = enclaveEnabled; readResolve(); } @Deprecated - public EC2AbstractSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, Mode mode, String labelString, ComputerLauncher launcher, RetentionStrategy retentionStrategy, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String javaPath, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, List tags, String cloudName, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses, Tenancy tenancy, Boolean metadataEndpointEnabled, Boolean metadataTokensRequired, Integer metadataHopsLimit) + public EC2AbstractSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + Mode mode, + String labelString, + ComputerLauncher launcher, + RetentionStrategy retentionStrategy, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String javaPath, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + Tenancy tenancy, + Boolean metadataEndpointEnabled, + Boolean metadataTokensRequired, + Integer metadataHopsLimit, + Boolean metadataSupported) throws FormException, IOException { - this(name, instanceId, templateDescription, remoteFS, numExecutors, mode, labelString, launcher, retentionStrategy, initScript, tmpDir, nodeProperties, remoteAdmin, DEFAULT_JAVA_PATH, jvmopts, stopOnTerminate, idleTerminationMinutes, tags, cloudName, launchTimeout, amiType, connectionStrategy, maxTotalUses, tenancy, metadataEndpointEnabled, metadataTokensRequired, metadataHopsLimit, DEFAULT_METADATA_SUPPORTED); + this( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + mode, + labelString, + launcher, + retentionStrategy, + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + DEFAULT_JAVA_PATH, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + tenancy, + metadataEndpointEnabled, + metadataTokensRequired, + metadataHopsLimit, + metadataSupported, + DEFAULT_ENCLAVE_ENABLED); } @Deprecated - public EC2AbstractSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, Mode mode, String labelString, ComputerLauncher launcher, RetentionStrategy retentionStrategy, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, List tags, String cloudName, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses, Tenancy tenancy) + public EC2AbstractSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + Mode mode, + String labelString, + ComputerLauncher launcher, + RetentionStrategy retentionStrategy, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String javaPath, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + Tenancy tenancy, + Boolean metadataEndpointEnabled, + Boolean metadataTokensRequired, + Integer metadataHopsLimit) throws FormException, IOException { - this(name, instanceId, templateDescription, remoteFS, numExecutors, mode, labelString, launcher, retentionStrategy, initScript, tmpDir, nodeProperties, remoteAdmin, DEFAULT_JAVA_PATH, jvmopts, stopOnTerminate, idleTerminationMinutes, tags, cloudName, launchTimeout, amiType, connectionStrategy, maxTotalUses, tenancy, DEFAULT_METADATA_ENDPOINT_ENABLED, DEFAULT_METADATA_TOKENS_REQUIRED, DEFAULT_METADATA_HOPS_LIMIT); + this( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + mode, + labelString, + launcher, + retentionStrategy, + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + DEFAULT_JAVA_PATH, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + tenancy, + metadataEndpointEnabled, + metadataTokensRequired, + metadataHopsLimit, + DEFAULT_METADATA_SUPPORTED); } @Deprecated - public EC2AbstractSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, Mode mode, String labelString, ComputerLauncher launcher, RetentionStrategy retentionStrategy, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, List tags, String cloudName, boolean useDedicatedTenancy, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses) + public EC2AbstractSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + Mode mode, + String labelString, + ComputerLauncher launcher, + RetentionStrategy retentionStrategy, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + Tenancy tenancy) throws FormException, IOException { + this( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + mode, + labelString, + launcher, + retentionStrategy, + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + DEFAULT_JAVA_PATH, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + tenancy, + DEFAULT_METADATA_ENDPOINT_ENABLED, + DEFAULT_METADATA_TOKENS_REQUIRED, + DEFAULT_METADATA_HOPS_LIMIT); + } - this(name, instanceId, templateDescription, remoteFS, numExecutors, mode, labelString, launcher, retentionStrategy, initScript, tmpDir, nodeProperties, remoteAdmin, jvmopts, stopOnTerminate, idleTerminationMinutes, tags, cloudName, launchTimeout, amiType, connectionStrategy, maxTotalUses, Tenancy.backwardsCompatible(useDedicatedTenancy)); + @Deprecated + public EC2AbstractSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + Mode mode, + String labelString, + ComputerLauncher launcher, + RetentionStrategy retentionStrategy, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + List tags, + String cloudName, + boolean useDedicatedTenancy, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses) + throws FormException, IOException { + + this( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + mode, + labelString, + launcher, + retentionStrategy, + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + Tenancy.backwardsCompatible(useDedicatedTenancy)); } @Deprecated - public EC2AbstractSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, Mode mode, String labelString, ComputerLauncher launcher, RetentionStrategy retentionStrategy, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, List tags, String cloudName, boolean usePrivateDnsName, boolean useDedicatedTenancy, int launchTimeout, AMITypeData amiType) + public EC2AbstractSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + Mode mode, + String labelString, + ComputerLauncher launcher, + RetentionStrategy retentionStrategy, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + List tags, + String cloudName, + boolean usePrivateDnsName, + boolean useDedicatedTenancy, + int launchTimeout, + AMITypeData amiType) throws FormException, IOException { - this(name, instanceId, templateDescription, remoteFS, numExecutors, mode, labelString, launcher, retentionStrategy, initScript, tmpDir, nodeProperties, remoteAdmin, jvmopts, stopOnTerminate, idleTerminationMinutes, tags, cloudName, useDedicatedTenancy, launchTimeout, amiType, ConnectionStrategy.backwardsCompatible(usePrivateDnsName, false, false), -1); + this( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + mode, + labelString, + launcher, + retentionStrategy, + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + tags, + cloudName, + useDedicatedTenancy, + launchTimeout, + amiType, + ConnectionStrategy.backwardsCompatible(usePrivateDnsName, false, false), + -1); } + @Serial @Override protected Object readResolve() { var o = (EC2AbstractSlave) super.readResolve(); @@ -228,7 +521,8 @@ protected Object readResolve() { } if (o.amiType == null) { - o.amiType = new UnixData(o.rootCommandPrefix, o.slaveCommandPrefix, o.slaveCommandSuffix, Integer.toString(o.sshPort), null); + o.amiType = new UnixData( + o.rootCommandPrefix, o.slaveCommandPrefix, o.slaveCommandSuffix, Integer.toString(o.sshPort), null); } if (o.maxTotalUses == 0) { @@ -263,174 +557,172 @@ public EC2Cloud getCloud() { /** * See http://aws.amazon.com/ec2/instance-types/ */ - /* package */static int toNumExecutors(InstanceType it) { + /* package */ static int toNumExecutors(InstanceType it) { switch (it) { - case T1Micro: - return 1; - case M1Small: - return 1; - case M1Medium: - return 2; - case M3Medium: - return 2; - case T3Nano: - return 2; - case T3aNano: - return 2; - case T3Micro: - return 2; - case T3aMicro: - return 2; - case T3Small: - return 2; - case T3aSmall: - return 2; - case T3Medium: - return 2; - case T3aMedium: - return 2; - case A1Large: - return 2; - case T3Large: - return 3; - case T3aLarge: - return 3; - case M1Large: - return 4; - case M3Large: - return 4; - case M4Large: - return 4; - case M5Large: - return 4; - case M5aLarge: - return 4; - case T3Xlarge: - return 5; - case T3aXlarge: - return 5; - case A1Xlarge: - return 5; - case C1Medium: - return 5; - case M2Xlarge: - return 6; - case C3Large: - return 7; - case C4Large: - return 7; - case C5Large: - return 7; - case C5dLarge: - return 7; - case M1Xlarge: - return 8; - case T32xlarge: - return 10; - case T3a2xlarge: - return 10; - case A12xlarge: - return 10; - case M22xlarge: - return 13; - case M3Xlarge: - return 13; - case M4Xlarge: - return 13; - case M5Xlarge: - return 13; - case M5aXlarge: - return 13; - case A14xlarge: - return 14; - case C3Xlarge: - return 14; - case C4Xlarge: - return 14; - case C5Xlarge: - return 14; - case C5dXlarge: - return 14; - case C1Xlarge: - return 20; - case M24xlarge: - return 26; - case M32xlarge: - return 26; - case M42xlarge: - return 26; - case M52xlarge: - return 26; - case M5a2xlarge: - return 26; - case G22xlarge: - return 26; - case C32xlarge: - return 28; - case C42xlarge: - return 28; - case C52xlarge: - return 28; - case C5d2xlarge: - return 28; - case Cc14xlarge: - return 33; - case Cg14xlarge: - return 33; - case Hi14xlarge: - return 35; - case Hs18xlarge: - return 35; - case C34xlarge: - return 55; - case C44xlarge: - return 55; - case C54xlarge: - return 55; - case C5d4xlarge: - return 55; - case M44xlarge: - return 55; - case M54xlarge: - return 55; - case M5a4xlarge: - return 55; - case Cc28xlarge: - return 88; - case Cr18xlarge: - return 88; - case C38xlarge: - return 108; - case C48xlarge: - return 108; - case C59xlarge: - return 108; - case C5d9xlarge: - return 108; - case M410xlarge: - return 120; - case M512xlarge: - return 120; - case M5a12xlarge: - return 120; - case M416xlarge: - return 160; - case C518xlarge: - return 216; - case C5d18xlarge: - return 216; - case M524xlarge: - return 240; - case M5a24xlarge: - return 240; - case Dl124xlarge: - return 250; - case Mac1Metal: - return 1; - // We don't have a suggestion, but we don't want to fail completely - // surely? - default: - return 1; + case T1_MICRO: + return 1; + case M1_SMALL: + return 1; + case M1_MEDIUM: + return 2; + case M3_MEDIUM: + return 2; + case T3_NANO: + return 2; + case T3_A_NANO: + return 2; + case T3_MICRO: + return 2; + case T3_A_MICRO: + return 2; + case T3_SMALL: + return 2; + case T3_A_SMALL: + return 2; + case T3_MEDIUM: + return 2; + case T3_A_MEDIUM: + return 2; + case A1_LARGE: + return 2; + case T3_LARGE: + return 3; + case T3_A_LARGE: + return 3; + case M1_LARGE: + return 4; + case M3_LARGE: + return 4; + case M4_LARGE: + return 4; + case M5_LARGE: + return 4; + case M5_A_LARGE: + return 4; + case T3_XLARGE: + return 5; + case T3_A_XLARGE: + return 5; + case A1_XLARGE: + return 5; + case C1_MEDIUM: + return 5; + case M2_XLARGE: + return 6; + case C3_LARGE: + return 7; + case C4_LARGE: + return 7; + case C5_LARGE: + return 7; + case C5_D_LARGE: + return 7; + case M1_XLARGE: + return 8; + case T3_2_XLARGE: + return 10; + case T3_A_2_XLARGE: + return 10; + case A1_2_XLARGE: + return 10; + case M2_2_XLARGE: + return 13; + case M3_XLARGE: + return 13; + case M4_XLARGE: + return 13; + case M5_XLARGE: + return 13; + case M5_A_XLARGE: + return 13; + case A1_4_XLARGE: + return 14; + case C3_XLARGE: + return 14; + case C4_XLARGE: + return 14; + case C5_XLARGE: + return 14; + case C5_D_XLARGE: + return 14; + case C1_XLARGE: + return 20; + case M2_4_XLARGE: + return 26; + case M3_2_XLARGE: + return 26; + case M4_2_XLARGE: + return 26; + case M5_2_XLARGE: + return 26; + case M5_A_2_XLARGE: + return 26; + case G2_2_XLARGE: + return 26; + case C3_2_XLARGE: + return 28; + case C4_2_XLARGE: + return 28; + case C5_2_XLARGE: + return 28; + case C5_D_2_XLARGE: + return 28; + case CC1_4_XLARGE: + return 33; + case CG1_4_XLARGE: + return 33; + case HI1_4_XLARGE: + return 35; + case HS1_8_XLARGE: + return 35; + case C3_4_XLARGE: + return 55; + case C4_4_XLARGE: + return 55; + case C5_4_XLARGE: + return 55; + case C5_D_4_XLARGE: + return 55; + case M4_4_XLARGE: + return 55; + case M5_4_XLARGE: + return 55; + case M5_A_4_XLARGE: + return 55; + case CC2_8_XLARGE: + return 88; + case CR1_8_XLARGE: + return 88; + case C3_8_XLARGE: + return 108; + case C4_8_XLARGE: + return 108; + case C5_9_XLARGE: + return 108; + case C5_D_9_XLARGE: + return 108; + case M4_10_XLARGE: + return 120; + case M5_12_XLARGE: + return 120; + case M5_A_12_XLARGE: + return 120; + case M4_16_XLARGE: + return 160; + case C5_18_XLARGE: + return 216; + case C5_D_18_XLARGE: + return 216; + case M5_24_XLARGE: + return 240; + case M5_A_24_XLARGE: + return 240; + case DL1_24_XLARGE: + return 250; + case MAC1_METAL: + return 1; + default: + return 1; } } @@ -466,12 +758,14 @@ public static Instance getInstance(String instanceId, EC2Cloud cloud) { /** * Terminates the instance in EC2. */ - public abstract void terminate(); + public abstract Future terminate(); void stop() { try { - AmazonEC2 ec2 = getCloud().connect(); - StopInstancesRequest request = new StopInstancesRequest(Collections.singletonList(getInstanceId())); + Ec2Client ec2 = getCloud().connect(); + StopInstancesRequest request = StopInstancesRequest.builder() + .instanceIds(Collections.singletonList(getInstanceId())) + .build(); LOGGER.fine("Sending stop request for " + getInstanceId()); ec2.stopInstances(request); LOGGER.info("EC2 instance stop request sent for " + getInstanceId()); @@ -479,28 +773,29 @@ void stop() { if (computer != null) { computer.disconnect(null); } - } catch (AmazonClientException e) { + } catch (SdkException e) { LOGGER.log(Level.WARNING, "Failed to stop EC2 instance: " + getInstanceId(), e); } - } boolean terminateInstance() { try { - AmazonEC2 ec2 = getCloud().connect(); - TerminateInstancesRequest request = new TerminateInstancesRequest(Collections.singletonList(getInstanceId())); + Ec2Client ec2 = getCloud().connect(); + TerminateInstancesRequest request = TerminateInstancesRequest.builder() + .instanceIds(Collections.singletonList(getInstanceId())) + .build(); LOGGER.fine("Sending terminate request for " + getInstanceId()); ec2.terminateInstances(request); LOGGER.info("EC2 instance terminate request sent for " + getInstanceId()); return true; - } catch (AmazonClientException e) { + } catch (SdkException e) { LOGGER.log(Level.WARNING, "Failed to terminate EC2 instance: " + getInstanceId(), e); return false; } } @Override - public Node reconfigure(final StaplerRequest req, JSONObject form) throws FormException { + public Node reconfigure(final StaplerRequest2 req, JSONObject form) throws FormException { if (form == null) { return null; } @@ -532,7 +827,7 @@ void idleTimeout() { } } - void launchTimeout(){ + void launchTimeout() { LOGGER.info("EC2 instance failed to launch: " + getInstanceId()); terminate(); } @@ -543,29 +838,33 @@ public long getLaunchTimeoutInMillis() { } public String getRemoteAdmin() { - if (remoteAdmin == null || remoteAdmin.length() == 0) + if (remoteAdmin == null || remoteAdmin.isEmpty()) { return amiType.isWindows() ? "Administrator" : "root"; + } return remoteAdmin; } String getRootCommandPrefix() { - String commandPrefix = (amiType.isUnix() ? ((UnixData) amiType).getRootCommandPrefix() : (amiType.isMac() ? ((MacData) amiType).getRootCommandPrefix() : "")); - if (commandPrefix == null || commandPrefix.length() == 0) + String commandPrefix = amiType.isSSHAgent() ? ((SSHData) amiType).getRootCommandPrefix() : ""; + if (commandPrefix == null || commandPrefix.isEmpty()) { return ""; + } return commandPrefix + " "; } String getSlaveCommandPrefix() { - String commandPrefix = (amiType.isUnix() ? ((UnixData) amiType).getSlaveCommandPrefix() :(amiType.isMac() ? ((MacData) amiType).getSlaveCommandPrefix() : "")); - if (commandPrefix == null || commandPrefix.length() == 0) + String commandPrefix = amiType.isSSHAgent() ? ((SSHData) amiType).getSlaveCommandPrefix() : ""; + if (commandPrefix == null || commandPrefix.isEmpty()) { return ""; + } return commandPrefix + " "; } String getSlaveCommandSuffix() { - String commandSuffix = (amiType.isUnix() ? ((UnixData) amiType).getSlaveCommandSuffix() :(amiType.isMac() ? ((MacData) amiType).getSlaveCommandSuffix() : "")); - if (commandSuffix == null || commandSuffix.length() == 0) + String commandSuffix = amiType.isSSHAgent() ? ((SSHData) amiType).getSlaveCommandSuffix() : ""; + if (commandSuffix == null || commandSuffix.isEmpty()) { return ""; + } return " " + commandSuffix; } @@ -578,9 +877,10 @@ String getJvmopts() { } public int getSshPort() { - String sshPort = (amiType.isUnix() ? ((UnixData) amiType).getSshPort() :(amiType.isMac() ? ((MacData) amiType).getSshPort() : "22")); - if (sshPort == null || sshPort.length() == 0) + String sshPort = amiType.isSSHAgent() ? ((SSHData) amiType).getSshPort() : "22"; + if (sshPort == null || sshPort.isEmpty()) { return 22; + } int port = 0; try { @@ -603,10 +903,12 @@ public void onConnected() { protected boolean isAlive(boolean force) { fetchLiveInstanceData(force); - if (lastFetchInstance == null) + if (lastFetchInstance == null) { return false; - if (lastFetchInstance.getState().getName().equals(InstanceStateName.Terminated.toString())) + } + if (lastFetchInstance.state().name().equals(InstanceStateName.TERMINATED)) { return false; + } return true; } @@ -614,7 +916,7 @@ protected boolean isAlive(boolean force) { * Much of the EC2 data is beyond our direct control, therefore we need to refresh it from time to time to ensure we * reflect the reality of the instances. */ - private void fetchLiveInstanceData(boolean force) throws AmazonClientException { + private void fetchLiveInstanceData(boolean force) throws SdkException { /* * If we've grabbed the data recently, don't bother getting it again unless we are forced */ @@ -639,30 +941,40 @@ private void fetchLiveInstanceData(boolean force) throws AmazonClientException { i = CloudHelper.getInstanceWithRetry(getInstanceId(), getCloud()); } catch (InterruptedException e) { // We'll just retry next time we test for idleness. - LOGGER.fine("InterruptedException while get " + getInstanceId() - + " Exception: " + e); + LOGGER.fine("InterruptedException while get " + getInstanceId() + " Exception: " + e); return; } - lastFetchTime = now; lastFetchInstance = i; - if (i == null) + if (i == null) { return; + } - publicDNS = i.getPublicDnsName(); - privateDNS = i.getPrivateIpAddress(); - createdTime = i.getLaunchTime().getTime(); - instanceType = i.getInstanceType(); + updateFromFetchedInstance(i); + } + + /** + * Updates instance data from a pre-fetched Instance. Used by batch operations (e.g. EC2SlaveMonitor) + * to avoid per-node EC2 API calls. + */ + protected void updateFromFetchedInstance(Instance i) { + long now = System.currentTimeMillis(); + lastFetchTime = now; + lastFetchInstance = i; + publicDNS = i.publicDnsName(); + privateDNS = i.privateIpAddress(); + createdTime = i.launchTime(); + instanceType = i.instanceType().name(); /* * Only fetch tags from live instance if tags are set. This check is required to mitigate a race condition * when fetchLiveInstanceData() is called before pushLiveInstancedata(). */ - if(!i.getTags().isEmpty()) { - tags = new LinkedList(); - for (Tag t : i.getTags()) { - tags.add(new EC2Tag(t.getKey(), t.getValue())); + if (!i.tags().isEmpty()) { + tags = new LinkedList<>(); + for (Tag t : i.tags()) { + tags.add(new EC2Tag(t.key(), t.value())); } } } @@ -670,29 +982,29 @@ private void fetchLiveInstanceData(boolean force) throws AmazonClientException { /* * Clears all existing tag data so that we can force the instance into a known state */ - protected void clearLiveInstancedata() throws AmazonClientException { + protected void clearLiveInstancedata() throws SdkException { Instance inst = null; try { inst = CloudHelper.getInstanceWithRetry(getInstanceId(), getCloud()); } catch (InterruptedException e) { // We'll just retry next time we test for idleness. - LOGGER.fine("InterruptedException while get " + getInstanceId() - + " Exception: " + e); + LOGGER.fine("InterruptedException while get " + getInstanceId() + " Exception: " + e); return; } - /* Now that we have our instance, we can clear the tags on it */ if (!tags.isEmpty()) { - HashSet instTags = new HashSet(); + HashSet instTags = new HashSet<>(); for (EC2Tag t : tags) { - instTags.add(new Tag(t.getName(), t.getValue())); + instTags.add(Tag.builder().key(t.getName()).value(t.getValue()).build()); } List resources = getResourcesToTag(inst); - DeleteTagsRequest tagRequest = new DeleteTagsRequest(); - tagRequest.withResources(resources).setTags(instTags); + DeleteTagsRequest tagRequest = DeleteTagsRequest.builder() + .resources(resources) + .tags(instTags) + .build(); getCloud().connect().deleteTags(tagRequest); } } @@ -701,28 +1013,28 @@ protected void clearLiveInstancedata() throws AmazonClientException { * Sets tags on an instance and on the volumes attached to it. This will not clear existing tag data, so call * clearLiveInstancedata if needed */ - protected void pushLiveInstancedata() throws AmazonClientException { + protected void pushLiveInstancedata() throws SdkException { Instance inst = null; try { inst = CloudHelper.getInstanceWithRetry(getInstanceId(), getCloud()); } catch (InterruptedException e) { // We'll just retry next time we test for idleness. - LOGGER.fine("InterruptedException while get " + getInstanceId() - + " Exception: " + e); + LOGGER.fine("InterruptedException while get " + getInstanceId() + " Exception: " + e); } - /* Now that we have our instance, we can set tags on it */ if (inst != null && tags != null && !tags.isEmpty()) { - HashSet instTags = new HashSet(); + HashSet instTags = new HashSet<>(); for (EC2Tag t : tags) { - instTags.add(new Tag(t.getName(), t.getValue())); + instTags.add(Tag.builder().key(t.getName()).value(t.getValue()).build()); } List resources = getResourcesToTag(inst); - CreateTagsRequest tagRequest = new CreateTagsRequest(); - tagRequest.withResources(resources).setTags(instTags); + CreateTagsRequest tagRequest = CreateTagsRequest.builder() + .resources(resources) + .tags(instTags) + .build(); getCloud().connect().createTags(tagRequest); } } @@ -732,9 +1044,9 @@ protected void pushLiveInstancedata() throws AmazonClientException { */ private List getResourcesToTag(Instance inst) { List resources = new ArrayList<>(); - resources.add(inst.getInstanceId()); - for(InstanceBlockDeviceMapping blockDeviceMapping : inst.getBlockDeviceMappings()) { - resources.add(blockDeviceMapping.getEbs().getVolumeId()); + resources.add(inst.instanceId()); + for (InstanceBlockDeviceMapping blockDeviceMapping : inst.blockDeviceMappings()) { + resources.add(blockDeviceMapping.ebs().volumeId()); } return resources; } @@ -759,7 +1071,7 @@ public List getTags() { return Collections.unmodifiableList(tags); } - public long getCreatedTime() { + public Instant getCreatedTime() { fetchLiveInstanceData(false); return createdTime; } @@ -770,11 +1082,11 @@ public boolean getUsePrivateDnsName() { } public Secret getAdminPassword() { - return amiType.isWindows() ? ((WindowsData) amiType).getPassword() : Secret.fromString(""); + return amiType.isWinRMAgent() ? ((WindowsData) amiType).getPassword() : Secret.fromString(""); } public boolean isUseHTTPS() { - return amiType.isWindows() && ((WindowsData) amiType).isUseHTTPS(); + return amiType.isWinRMAgent() && ((WindowsData) amiType).isUseHTTPS(); } public int getBootDelay() { @@ -782,11 +1094,11 @@ public int getBootDelay() { } public Boolean getMetadataSupported() { - return metadataSupported; + return metadataSupported; } public Boolean getMetadataEndpointEnabled() { - return metadataEndpointEnabled; + return metadataEndpointEnabled; } public Boolean getMetadataTokensRequired() { @@ -797,24 +1109,29 @@ public Integer getMetadataHopsLimit() { return metadataHopsLimit; } + public Boolean getEnclaveEnabled() { + return enclaveEnabled; + } + public boolean isSpecifyPassword() { - return amiType.isWindows() && ((WindowsData) amiType).isSpecifyPassword(); + return amiType.isWinRMAgent() && ((WindowsData) amiType).isSpecifyPassword(); } public boolean isAllowSelfSignedCertificate() { - return amiType.isWindows() && ((WindowsData) amiType).isAllowSelfSignedCertificate(); + return amiType.isWinRMAgent() && ((WindowsData) amiType).isAllowSelfSignedCertificate(); } - public static ListBoxModel fillZoneItems(AWSCredentialsProvider credentialsProvider, String region) { + public static ListBoxModel fillZoneItems(AwsCredentialsProvider credentialsProvider, String region) { ListBoxModel model = new ListBoxModel(); if (!StringUtils.isEmpty(region)) { - AmazonEC2 client = AmazonEC2Factory.getInstance().connect(credentialsProvider, AmazonEC2Cloud.getEc2EndpointUrl(region)); - DescribeAvailabilityZonesResult zones = client.describeAvailabilityZones(); - List zoneList = zones.getAvailabilityZones(); + Ec2Client client = + AmazonEC2Factory.getInstance().connect(credentialsProvider, EC2Cloud.parseRegion(region), null); + DescribeAvailabilityZonesResponse zones = client.describeAvailabilityZones(); + List zoneList = zones.availabilityZones(); model.add("", ""); for (AvailabilityZone z : zoneList) { - model.add(z.getZoneName(), z.getZoneName()); + model.add(z.zoneName(), z.zoneName()); } } return model; @@ -823,9 +1140,9 @@ public static ListBoxModel fillZoneItems(AWSCredentialsProvider credentialsProvi /* * Used to determine if the agent is On Demand or Spot */ - abstract public String getEc2Type(); + public abstract String getEc2Type(); - public static abstract class DescriptorImpl extends SlaveDescriptor { + public abstract static class DescriptorImpl extends SlaveDescriptor { @Override public abstract String getDisplayName(); @@ -836,15 +1153,17 @@ public boolean isInstantiable() { } @POST - public ListBoxModel doFillZoneItems(@QueryParameter boolean useInstanceProfileForCredentials, - @QueryParameter String credentialsId, - @QueryParameter String region, - @QueryParameter String roleArn, - @QueryParameter String roleSessionName) { + public ListBoxModel doFillZoneItems( + @QueryParameter boolean useInstanceProfileForCredentials, + @QueryParameter String credentialsId, + @QueryParameter String region, + @QueryParameter String roleArn, + @QueryParameter String roleSessionName) { if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { return new ListBoxModel(); } - AWSCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider(useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); + AwsCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider( + useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); return fillZoneItems(credentialsProvider, region); } @@ -852,5 +1171,4 @@ public List> getAMITypeDescriptors() { return Jenkins.get().getDescriptorList(AMITypeData.class); } } - } diff --git a/src/main/java/hudson/plugins/ec2/EC2CleanupOrphanedNodes.java b/src/main/java/hudson/plugins/ec2/EC2CleanupOrphanedNodes.java new file mode 100644 index 000000000..9902b6cb1 --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/EC2CleanupOrphanedNodes.java @@ -0,0 +1,283 @@ +/* + * The MIT License + * + * Copyright (c) 2025, CloudBees, Inc., and a number of other of contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.plugins.ec2; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.PeriodicWork; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import jenkins.model.Jenkins; +import jenkins.model.JenkinsLocationConfiguration; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.ec2.Ec2Client; +import software.amazon.awssdk.services.ec2.model.DescribeInstancesRequest; +import software.amazon.awssdk.services.ec2.model.DescribeInstancesResponse; +import software.amazon.awssdk.services.ec2.model.Filter; +import software.amazon.awssdk.services.ec2.model.Instance; +import software.amazon.awssdk.services.ec2.model.Reservation; +import software.amazon.awssdk.services.ec2.model.Tag; + +@Extension +public class EC2CleanupOrphanedNodes extends PeriodicWork { + + private final Logger LOGGER = Logger.getLogger(EC2CleanupOrphanedNodes.class.getName()); + + @VisibleForTesting + static final String NODE_EXPIRES_AT_TAG_NAME = "jenkins_node_expires_at"; + + private static final long RECURRENCE_PERIOD = Long.parseLong( + System.getProperty(EC2CleanupOrphanedNodes.class.getName() + ".recurrencePeriod", String.valueOf(HOUR))); + private static final int LOST_MULTIPLIER = + Integer.parseInt(System.getProperty(EC2CleanupOrphanedNodes.class.getName() + ".lostMultiplier", "3")); + + @Override + public long getRecurrencePeriod() { + return RECURRENCE_PERIOD; + } + + @Override + protected void doRun() { + LOGGER.fine(() -> "Starting clean up activity for orphaned nodes"); + getClouds().forEach(this::cleanCloud); + } + + @VisibleForTesting + void cleanCloud(@NonNull EC2Cloud cloud) { + if (!cloud.isCleanUpOrphanedNodes()) { + LOGGER.fine( + () -> "Skipping clean up activity for cloud: " + cloud.getDisplayName() + " as it is disabled."); + return; + } + LOGGER.fine(() -> "Processing clean up activity for cloud: " + cloud.getDisplayName()); + Ec2Client connection; + try { + connection = cloud.connect(); + } catch (SdkException e) { + LOGGER.log(Level.WARNING, "Failed to connect to EC2 cloud: " + cloud.getDisplayName(), e); + return; + } + + Set remoteInstances = getAllRemoteInstance(connection, cloud); + Set localConnectedEC2Instances = getConnectedAgentInstanceIds(cloud); + addMissingTags(connection, remoteInstances, cloud); + Set remoteInstancesIds = + remoteInstances.stream().map(Instance::instanceId).collect(Collectors.toSet()); + Set updatedInstances = + updateLocalInstancesTag(connection, localConnectedEC2Instances, remoteInstancesIds, cloud); + + remoteInstances.stream() + // exclude instances that just got updated + .filter(remote -> !updatedInstances.contains(remote.instanceId())) + .filter(this::isOrphaned) + .forEach(remote -> terminateInstance(remote.instanceId(), connection)); + } + + private List getClouds() { + return Jenkins.get().clouds.getAll(EC2Cloud.class); + } + + /** + * Returns a list of all EC2 instances in states (running, pending, or stopping) AND with the tags + * jenkins_server_url and jenkins_cloud_name + * These are all the instances that are created by the EC2 plugin of this controller and this cloud. + */ + private Set getAllRemoteInstance(Ec2Client connection, EC2Cloud cloud) throws SdkException { + Set instanceIds = new HashSet<>(); + + String nextToken = null; + JenkinsLocationConfiguration jenkinsLocation = JenkinsLocationConfiguration.get(); + if (jenkinsLocation.getUrl() == null) { + LOGGER.warning( + "Jenkins server URL is not set in JenkinsLocationConfiguration.Returning empty remote instance list for cloud: " + + cloud.getDisplayName()); + return instanceIds; + } + + do { + DescribeInstancesRequest.Builder requestBuilder = DescribeInstancesRequest.builder() + .maxResults(500) + .filters( + Filter.builder() + .name("instance-state-name") + .values( + InstanceState.RUNNING.getCode(), + InstanceState.PENDING.getCode(), + InstanceState.STOPPING.getCode()) + .build(), + tagFilter(EC2Tag.TAG_NAME_JENKINS_SERVER_URL, jenkinsLocation.getUrl()), + tagFilter(EC2Tag.TAG_NAME_JENKINS_CLOUD_NAME, cloud.getDisplayName())); + + requestBuilder.nextToken(nextToken); + DescribeInstancesResponse result = connection.describeInstances(requestBuilder.build()); + + for (Reservation r : result.reservations()) { + instanceIds.addAll(new HashSet<>(r.instances())); + } + + nextToken = result.nextToken(); + } while (nextToken != null); + + LOGGER.fine(() -> "Found " + instanceIds.size() + " remote instance ID(s) for cloud: " + cloud.getDisplayName() + + ". Instance IDs: " + + instanceIds.stream().map(Instance::instanceId).collect(Collectors.joining(", "))); + return instanceIds; + } + + /** + * Returns a list of EC2 agent instance IDs connected to Jenkins. + */ + private Set getConnectedAgentInstanceIds(EC2Cloud cloud) { + return Jenkins.get().getNodes().stream() + .filter(EC2AbstractSlave.class::isInstance) + .map(EC2AbstractSlave.class::cast) + .filter(node -> cloud.equals(node.getCloud())) + .map(node -> { + LOGGER.fine( + () -> "Connected agent: " + node.getNodeName() + ", Instance ID: " + node.getInstanceId()); + return node.getInstanceId(); + }) + .collect(Collectors.toSet()); + } + + /** + * Adds a tag to each remote instance that does not have the jenkins_node_last_refresh tag. + */ + private void addMissingTags(Ec2Client connection, Set remoteInstances, EC2Cloud cloud) { + Set instancesToTag = new HashSet<>(); + + for (Instance remoteInstance : remoteInstances) { + boolean hasTag = remoteInstance.tags().stream().anyMatch(tag -> NODE_EXPIRES_AT_TAG_NAME.equals(tag.key())); + if (!hasTag) { + instancesToTag.add(remoteInstance.instanceId()); + } + } + + if (instancesToTag.isEmpty()) { + LOGGER.fine(() -> "No instances to tag in cloud: " + cloud.getDisplayName()); + return; + } + + LOGGER.fine(() -> "Creating tags for " + instancesToTag.size() + " instances"); + createOrUpdateExpiryTagInBulk(connection, cloud, instancesToTag); + } + + /** + * Updates the tag of the local EC2 instances to indicate they are still in use. + */ + private Set updateLocalInstancesTag( + Ec2Client connection, Set localInstanceIds, Set remoteInstanceIds, EC2Cloud cloud) { + if (localInstanceIds.isEmpty()) { + LOGGER.fine(() -> "No local EC2 agents found, skipping tag update."); + return Set.of(); + } + + Set instanceIdsToUpdate = Sets.intersection(remoteInstanceIds, localInstanceIds); + + if (instanceIdsToUpdate.isEmpty()) { + LOGGER.fine(() -> "No local EC2 agents found in remote instances, skipping tag update."); + return Set.of(); + } + + LOGGER.fine(() -> "Updating tags for " + instanceIdsToUpdate.size() + " instances"); + createOrUpdateExpiryTagInBulk(connection, cloud, instanceIdsToUpdate); + return instanceIdsToUpdate; + } + + private void createOrUpdateExpiryTagInBulk(Ec2Client connection, EC2Cloud cloud, Set instancesToTag) { + + String nodeExpiresAtTagValue = OffsetDateTime.now(ZoneOffset.UTC) + .plus(RECURRENCE_PERIOD * LOST_MULTIPLIER, ChronoUnit.MILLIS) + .toString(); + + // Split instancesToTag into batches to avoid exceeding AWS limits + List> batches = Lists.partition(new ArrayList<>(instancesToTag), 500); + LOGGER.fine(() -> + "Creating or updating tags in batches of " + batches.size() + " for cloud: " + cloud.getDisplayName()); + for (List batch : batches) { + try { + connection.createTags(builder -> builder.resources(batch) + .tags(Tag.builder() + .key(NODE_EXPIRES_AT_TAG_NAME) + .value(nodeExpiresAtTagValue) + .build()) + .build()); + LOGGER.finer(() -> "Created or Updated tag for instances " + batch + " to " + nodeExpiresAtTagValue + + " in cloud: " + cloud.getDisplayName()); + } catch (SdkException e) { + LOGGER.log(Level.WARNING, "Error updating tags for instances " + batch, e); + } + } + } + + private boolean isOrphaned(Instance remote) { + String nodeExpiresAt; + if (remote.tags() != null) { + nodeExpiresAt = remote.tags().stream() + .filter(tag -> NODE_EXPIRES_AT_TAG_NAME.equals(tag.key())) + .map(Tag::value) + .findFirst() + .orElse(null); + } else { + nodeExpiresAt = null; + } + + if (nodeExpiresAt == null) { + LOGGER.fine(() -> "Instance " + remote.instanceId() + " does not have the tag " + NODE_EXPIRES_AT_TAG_NAME); + return false; + } + String currentTime = OffsetDateTime.now(ZoneOffset.UTC).toString(); + + // We can do a string compare since the format will always be ISO 8601 + boolean isOrphan = nodeExpiresAt.compareTo(currentTime) < 0; + LOGGER.fine(() -> "Instance " + remote.instanceId() + ", nodeExpiresAt: " + nodeExpiresAt + ", currentDate: " + + currentTime + ", isOrphan: " + isOrphan); + return isOrphan; + } + + private void terminateInstance(String instanceId, Ec2Client connection) { + LOGGER.info(() -> "Removing orphaned instance: " + instanceId); + try { + connection.terminateInstances( + builder -> builder.instanceIds(instanceId).build()); + } catch (SdkException ex) { + LOGGER.log(Level.WARNING, "Error terminating remote instance " + instanceId, ex); + } + } + + private Filter tagFilter(String tagName, String tagValue) { + return Filter.builder().name("tag:" + tagName).values(tagValue).build(); + } +} diff --git a/src/main/java/hudson/plugins/ec2/EC2Cloud.java b/src/main/java/hudson/plugins/ec2/EC2Cloud.java index 76f8d112c..bb827f22f 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Cloud.java +++ b/src/main/java/hudson/plugins/ec2/EC2Cloud.java @@ -18,21 +18,6 @@ */ package hudson.plugins.ec2; -import com.amazonaws.AmazonClientException; -import com.amazonaws.AmazonServiceException; -import com.amazonaws.ClientConfiguration; -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.auth.InstanceProfileCredentialsProvider; -import com.amazonaws.auth.STSAssumeRoleSessionCredentialsProvider; -import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.model.*; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; -import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder; import com.cloudbees.jenkins.plugins.awscredentials.AWSCredentialsImpl; import com.cloudbees.jenkins.plugins.awscredentials.AmazonWebServicesCredentials; import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; @@ -46,19 +31,23 @@ import com.cloudbees.plugins.credentials.common.AbstractIdCredentialsListBoxModel; import com.cloudbees.plugins.credentials.common.StandardListBoxModel; import com.cloudbees.plugins.credentials.domains.Domain; -import com.cloudbees.plugins.credentials.domains.DomainRequirement; +import com.google.common.annotations.VisibleForTesting; +import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.ProxyConfiguration; import hudson.Util; import hudson.model.Computer; import hudson.model.Descriptor; +import hudson.model.Failure; import hudson.model.ItemGroup; import hudson.model.Label; import hudson.model.Node; import hudson.model.PeriodicWork; import hudson.model.TaskListener; import hudson.plugins.ec2.util.AmazonEC2Factory; +import hudson.plugins.ec2.util.FIPS140Utils; +import hudson.plugins.ec2.util.KeyPair; import hudson.security.ACL; import hudson.slaves.Cloud; import hudson.slaves.NodeProvisioner.PlannedNode; @@ -67,58 +56,106 @@ import hudson.util.ListBoxModel; import hudson.util.Secret; import hudson.util.StreamTaskListener; -import jenkins.model.Jenkins; -import jenkins.model.JenkinsLocationConfiguration; -import org.apache.commons.lang.StringUtils; -import org.kohsuke.stapler.AncestorInPath; -import org.kohsuke.stapler.HttpResponse; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; - -import edu.umd.cs.findbugs.annotations.CheckForNull; -import javax.servlet.ServletException; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.io.PrintStream; import java.io.StringReader; import java.io.StringWriter; -import java.net.InetSocketAddress; import java.net.MalformedURLException; -import java.net.Proxy; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; -import java.util.*; -import java.util.concurrent.Callable; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; - -import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; -import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; - +import java.util.regex.Pattern; +import jenkins.model.Jenkins; +import jenkins.model.JenkinsLocationConfiguration; +import jenkins.util.Timer; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest2; +import org.kohsuke.stapler.StaplerResponse2; import org.kohsuke.stapler.interceptor.RequirePOST; import org.kohsuke.stapler.verb.POST; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.auth.signer.Aws4Signer; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.apache.ApacheHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.regions.RegionMetadata; +import software.amazon.awssdk.regions.ServiceEndpointKey; +import software.amazon.awssdk.regions.ServiceMetadata; +import software.amazon.awssdk.services.ec2.Ec2Client; +import software.amazon.awssdk.services.ec2.model.DescribeInstancesRequest; +import software.amazon.awssdk.services.ec2.model.DescribeInstancesResponse; +import software.amazon.awssdk.services.ec2.model.DescribeRegionsResponse; +import software.amazon.awssdk.services.ec2.model.DescribeSpotInstanceRequestsRequest; +import software.amazon.awssdk.services.ec2.model.DescribeSpotInstanceRequestsResponse; +import software.amazon.awssdk.services.ec2.model.Filter; +import software.amazon.awssdk.services.ec2.model.Instance; +import software.amazon.awssdk.services.ec2.model.InstanceStateName; +import software.amazon.awssdk.services.ec2.model.InstanceType; +import software.amazon.awssdk.services.ec2.model.KeyPairInfo; +import software.amazon.awssdk.services.ec2.model.Reservation; +import software.amazon.awssdk.services.ec2.model.SpotInstanceRequest; +import software.amazon.awssdk.services.ec2.model.SpotInstanceState; +import software.amazon.awssdk.services.ec2.model.Tag; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.StsClientBuilder; +import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider; +import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; /** * Hudson's view of EC2. * * @author Kohsuke Kawaguchi */ -public abstract class EC2Cloud extends Cloud { +public class EC2Cloud extends Cloud { private static final Logger LOGGER = Logger.getLogger(EC2Cloud.class.getName()); public static final String DEFAULT_EC2_HOST = "us-east-1"; - public static final String DEFAULT_EC2_ENDPOINT = "https://ec2.amazonaws.com"; - - public static final String AWS_URL_HOST = "amazonaws.com"; - - public static final String AWS_CN_URL_HOST = "amazonaws.com.cn"; - public static final String EC2_SLAVE_TYPE_SPOT = "spot"; public static final String EC2_SLAVE_TYPE_DEMAND = "demand"; @@ -127,8 +164,41 @@ public abstract class EC2Cloud extends Cloud { private static final SimpleFormatter sf = new SimpleFormatter(); + // if this system property is defined and its value points to a valid ssh private key on disk + // then this will be used instead of any configured ssh credential + public static final String SSH_PRIVATE_KEY_FILEPATH = EC2Cloud.class.getName() + ".sshPrivateKeyFilePath"; + private transient ReentrantLock slaveCountingLock = new ReentrantLock(); + /** TTL for instance count cache (ms). Avoids repeated EC2 API calls during provisioning. */ + private static final long INSTANCE_COUNT_CACHE_TTL_MS = Long.getLong("jenkins.ec2.instanceCountCacheTtlMs", 30_000); + + private volatile long instanceCountCacheTimestamp; + private volatile int cachedTotalSlaves = -1; + private transient ConcurrentHashMap cachedTemplateSlaves = new ConcurrentHashMap<>(); + + private static final ExecutorService PROVISIONING_EXECUTOR = Executors.newCachedThreadPool(r -> { + Thread t = new Thread(r, "EC2Cloud-provisioning"); + t.setDaemon(true); + return t; + }); + + private static final long SCHEDULE_MAINTENANCE_DELAY_MS = + Long.getLong("jenkins.ec2.scheduleMaintenanceDelayMs", 1000); + + static void scheduleQueueMaintenance() { + Timer.get() + .schedule( + () -> { + Jenkins j = Jenkins.getInstanceOrNull(); + if (j != null) { + j.getQueue().scheduleMaintenance(); + } + }, + SCHEDULE_MAINTENANCE_DELAY_MS, + TimeUnit.MILLISECONDS); + } + private final boolean useInstanceProfileForCredentials; private final String roleArn; @@ -140,15 +210,19 @@ public abstract class EC2Cloud extends Cloud { */ @CheckForNull private String credentialsId; + @CheckForNull @Deprecated private transient String accessId; + @CheckForNull @Deprecated private transient Secret secretKey; + @CheckForNull @Deprecated private transient EC2PrivateKey privateKey; + @CheckForNull private String sshKeysCredentialsId; @@ -161,15 +235,37 @@ public abstract class EC2Cloud extends Cloud { private transient KeyPair usableKeyPair; - private transient volatile AmazonEC2 connection; + /** + * Represents the region. Can be null for backward compatibility reasons. + */ + private String region; - protected EC2Cloud(String name, boolean useInstanceProfileForCredentials, String credentialsId, String privateKey, String sshKeysCredentialsId, - String instanceCapStr, List templates, String roleArn, String roleSessionName) { + private String altEC2Endpoint; + + private boolean noDelayProvisioning; + + private boolean cleanUpOrphanedNodes; + + private transient volatile Ec2Client connection; + + @DataBoundConstructor + public EC2Cloud( + String name, + boolean useInstanceProfileForCredentials, + String credentialsId, + String region, + String privateKey, + String sshKeysCredentialsId, + String instanceCapStr, + List templates, + String roleArn, + String roleSessionName) { super(name); this.useInstanceProfileForCredentials = useInstanceProfileForCredentials; this.roleArn = roleArn; this.roleSessionName = roleSessionName; this.credentialsId = Util.fixEmpty(credentialsId); + this.region = Util.fixEmpty(region); this.sshKeysCredentialsId = Util.fixEmpty(sshKeysCredentialsId); if (templates == null) { @@ -188,40 +284,183 @@ protected EC2Cloud(String name, boolean useInstanceProfileForCredentials, String } @Deprecated - protected EC2Cloud(String id, boolean useInstanceProfileForCredentials, String credentialsId, String privateKey, - String instanceCapStr, List templates, String roleArn, String roleSessionName) { - this(id, useInstanceProfileForCredentials, credentialsId, privateKey, null, instanceCapStr, templates, roleArn, roleSessionName); + public EC2Cloud( + String name, + boolean useInstanceProfileForCredentials, + String credentialsId, + String region, + String privateKey, + String instanceCapStr, + List templates, + String roleArn, + String roleSessionName) { + this( + name, + useInstanceProfileForCredentials, + credentialsId, + region, + privateKey, + null, + instanceCapStr, + templates, + roleArn, + roleSessionName); + } + + @Deprecated + protected EC2Cloud( + String id, + boolean useInstanceProfileForCredentials, + String credentialsId, + String privateKey, + String instanceCapStr, + List templates, + String roleArn, + String roleSessionName) { + this( + id, + useInstanceProfileForCredentials, + credentialsId, + privateKey, + null, + null, + instanceCapStr, + templates, + roleArn, + roleSessionName); } @CheckForNull - public EC2PrivateKey resolvePrivateKey(){ - if (sshKeysCredentialsId != null) { + public EC2PrivateKey resolvePrivateKey() { + if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { + LOGGER.fine(() -> "(resolvePrivateKey) secret key file configured, will load from disk"); + return EC2PrivateKey.fetchFromDisk(); + } else if (sshKeysCredentialsId != null) { + LOGGER.fine(() -> "(resolvePrivateKey) Using jenkins ssh credential"); SSHUserPrivateKey privateKeyCredential = getSshCredential(sshKeysCredentialsId, Jenkins.get()); if (privateKeyCredential != null) { - return new EC2PrivateKey(privateKeyCredential.getPrivateKey()); + String privateKey = privateKeyCredential.getPrivateKey(); + FIPS140Utils.ensurePrivateKeyInFipsMode(privateKey); + return new EC2PrivateKey(privateKey); } } return null; } - public abstract URL getEc2EndpointUrl() throws IOException; + /** + * @deprecated Use public field "name" instead. + */ + @Deprecated + public String getCloudName() { + return name; + } + + public String getName() { + return name; + } + + public String getRegion() { + if (region == null) { + region = DEFAULT_EC2_HOST; // Backward compatibility + } + // Handles pre 1.14 region names that used the old AwsRegion enum, note we don't change + // the region here to keep the meta-data compatible in the case of a downgrade (is that right?) + if (region.indexOf('_') > 0) { + return region.replace('_', '-').toLowerCase(Locale.ENGLISH); + } + return region; + } + + @CheckForNull + @Restricted(NoExternalUse.class) + public static Region parseRegion(@CheckForNull final String input) { + final String regionId = Util.fixEmpty(input); + if (regionId == null) { + return null; + } + return Region.regions().stream() + .filter(r -> r.id().equals(regionId)) + .findFirst() + .orElse(null); + } + + @CheckForNull + @Restricted(NoExternalUse.class) + public static URI parseEndpoint(@CheckForNull String input) { + final String endpoint = Util.fixEmpty(input); + if (endpoint == null) { + return null; + } + try { + return new URI(endpoint); + } catch (URISyntaxException e) { + LOGGER.log(Level.WARNING, "The alternate EC2 endpoint is malformed ({0}).", endpoint); + return null; + } + } + + @NonNull + static Region getBootstrapRegion(@CheckForNull URI endpoint) { + if (endpoint != null) { + ServiceMetadata metadata = Ec2Client.serviceMetadata(); + for (Region region : metadata.regions()) { + ServiceEndpointKey key = + ServiceEndpointKey.builder().region(region).build(); + if (endpoint.getHost() != null + && endpoint.getHost().equals(metadata.endpointFor(key).toString())) { + return region; + } + } + } + return Region.US_EAST_1; + } - public abstract URL getS3EndpointUrl() throws IOException; + public boolean isNoDelayProvisioning() { + return noDelayProvisioning; + } + + @DataBoundSetter + public void setNoDelayProvisioning(boolean noDelayProvisioning) { + this.noDelayProvisioning = noDelayProvisioning; + } + + public boolean isCleanUpOrphanedNodes() { + return cleanUpOrphanedNodes; + } + + @DataBoundSetter + public void setCleanUpOrphanedNodes(boolean cleanUpOrphanedNodes) { + this.cleanUpOrphanedNodes = cleanUpOrphanedNodes; + } + + public String getAltEC2Endpoint() { + return altEC2Endpoint; + } + + @DataBoundSetter + public void setAltEC2Endpoint(String altEC2Endpoint) { + this.altEC2Endpoint = altEC2Endpoint; + } public void addTemplate(SlaveTemplate newTemplate) throws Exception { String newTemplateDescription = newTemplate.description; - if (getTemplate(newTemplateDescription) != null) throw new Exception( - String.format("A SlaveTemplate with description %s already exists", newTemplateDescription)); + if (getTemplate(newTemplateDescription) != null) { + throw new Exception( + String.format("A SlaveTemplate with description %s already exists", newTemplateDescription)); + } List templatesHolder = new ArrayList<>(templates); templatesHolder.add(newTemplate); templates = templatesHolder; } - public void updateTemplate(SlaveTemplate newTemplate, String oldTemplateDescription) throws Exception{ - Optional optionalOldTemplate = templates.stream().filter(template -> - Objects.equals(template.description, oldTemplateDescription)).findFirst(); - if (!optionalOldTemplate.isPresent()) - throw new Exception(String.format("A SlaveTemplate with description %s does not exist", oldTemplateDescription)); + public void updateTemplate(SlaveTemplate newTemplate, String oldTemplateDescription) throws Exception { + Optional optionalOldTemplate = templates.stream() + .filter(template -> Objects.equals(template.description, oldTemplateDescription)) + .findFirst(); + if (optionalOldTemplate.isEmpty()) { + throw new Exception( + String.format("A SlaveTemplate with description %s does not exist", oldTemplateDescription)); + } int oldTemplateIndex = templates.indexOf(optionalOldTemplate.get()); List templatesHolder = new ArrayList<>(templates); templatesHolder.set(oldTemplateIndex, newTemplate); @@ -230,28 +469,33 @@ public void updateTemplate(SlaveTemplate newTemplate, String oldTemplateDescript private void migratePrivateSshKeyToCredential(String privateKey) { // GET matching private key credential from Credential API if exists - Optional keyCredential = SystemCredentialsProvider.getInstance().getCredentials() - .stream() - .filter((cred) -> cred instanceof SSHUserPrivateKey) - .filter((cred) -> ((SSHUserPrivateKey)cred).getPrivateKey().trim().equals(privateKey.trim())) - .map(cred -> (SSHUserPrivateKey)cred) + Optional keyCredential = SystemCredentialsProvider.getInstance().getCredentials().stream() + .filter(SSHUserPrivateKey.class::isInstance) + .filter(cred -> + ((SSHUserPrivateKey) cred).getPrivateKey().trim().equals(privateKey.trim())) + .map(cred -> (SSHUserPrivateKey) cred) .findFirst(); - if (keyCredential.isPresent()){ + if (keyCredential.isPresent()) { // SET this.sshKeysCredentialsId with the found credential sshKeysCredentialsId = keyCredential.get().getId(); } else { // CREATE new credential String credsId = UUID.randomUUID().toString(); - SSHUserPrivateKey sshKeyCredentials = new BasicSSHUserPrivateKey(CredentialsScope.SYSTEM, credsId, "key", + SSHUserPrivateKey sshKeyCredentials = new BasicSSHUserPrivateKey( + CredentialsScope.SYSTEM, + credsId, + "key", new BasicSSHUserPrivateKey.PrivateKeySource() { @NonNull @Override public List getPrivateKeys() { return Collections.singletonList(privateKey.trim()); } - }, "", "EC2 Cloud Private Key - " + getDisplayName()); + }, + "", + "EC2 Cloud Private Key - " + getDisplayName()); addNewGlobalCredential(sshKeyCredentials); @@ -261,14 +505,21 @@ public List getPrivateKeys() { protected Object readResolve() { this.slaveCountingLock = new ReentrantLock(); + if (this.cachedTemplateSlaves == null) { + this.cachedTemplateSlaves = new ConcurrentHashMap<>(); + } - for (SlaveTemplate t : templates) + for (SlaveTemplate t : templates) { t.parent = this; + } - if (this.sshKeysCredentialsId == null && this.privateKey != null ){ - migratePrivateSshKeyToCredential(this.privateKey.getPrivateKey()); + if (this.sshKeysCredentialsId == null && this.privateKey != null) { + String privateKey = this.privateKey.getPrivateKey(); + FIPS140Utils.ensurePrivateKeyInFipsMode(privateKey); + migratePrivateSshKeyToCredential(privateKey); } - this.privateKey = null; // This enforces it not to be persisted and that CasC will never output privateKey on export + this.privateKey = + null; // This enforces it not to be persisted and that CasC will never output privateKey on export if (this.accessId != null && this.secretKey != null && credentialsId == null) { String secretKeyEncryptedValue = this.secretKey.getEncryptedValue(); @@ -277,12 +528,11 @@ protected Object readResolve() { SystemCredentialsProvider systemCredentialsProvider = SystemCredentialsProvider.getInstance(); // ITERATE ON EXISTING CREDS AND DON'T CREATE IF EXIST - for (Credentials credentials: systemCredentialsProvider.getCredentials()) { - if (credentials instanceof AmazonWebServicesCredentials) { - AmazonWebServicesCredentials awsCreds = (AmazonWebServicesCredentials) credentials; - AWSCredentials awsCredentials = awsCreds.getCredentials(); - if (accessId.equals(awsCredentials.getAWSAccessKeyId()) && - Secret.toString(this.secretKey).equals(awsCredentials.getAWSSecretKey())) { + for (Credentials credentials : systemCredentialsProvider.getCredentials()) { + if (credentials instanceof AmazonWebServicesCredentials awsCreds) { + AwsCredentials awsCredentials = awsCreds.resolveCredentials(); + if (accessId.equals(awsCredentials.accessKeyId()) + && Secret.toString(this.secretKey).equals(awsCredentials.secretAccessKey())) { this.credentialsId = awsCreds.getId(); this.accessId = null; @@ -295,34 +545,39 @@ protected Object readResolve() { // CREATE String credsId = UUID.randomUUID().toString(); addNewGlobalCredential(new AWSCredentialsImpl( - CredentialsScope.SYSTEM, credsId, this.accessId, secretKeyEncryptedValue, + CredentialsScope.SYSTEM, + credsId, + this.accessId, + secretKeyEncryptedValue, "EC2 Cloud - " + getDisplayName())); this.credentialsId = credsId; this.accessId = null; this.secretKey = null; - // PROBLEM, GLOBAL STORE NOT FOUND - LOGGER.log(Level.WARNING, "EC2 Plugin could not migrate credentials to the Jenkins Global Credentials Store, EC2 Plugin for cloud {0} must be manually reconfigured", getDisplayName()); + LOGGER.log( + Level.WARNING, + "EC2 Plugin could not migrate credentials to the Jenkins Global Credentials Store, EC2 Plugin for cloud {0} must be manually reconfigured", + getDisplayName()); } return this; } - private void addNewGlobalCredential(Credentials credentials){ - for (CredentialsStore credentialsStore: CredentialsProvider.lookupStores(Jenkins.get())) { + private void addNewGlobalCredential(Credentials credentials) { + for (CredentialsStore credentialsStore : CredentialsProvider.lookupStores(Jenkins.get())) { - if (credentialsStore instanceof SystemCredentialsProvider.StoreImpl) { + if (credentialsStore instanceof SystemCredentialsProvider.StoreImpl) { try { credentialsStore.addCredentials(Domain.global(), credentials); } catch (IOException e) { this.credentialsId = null; - LOGGER.log(Level.WARNING, "Exception converting legacy configuration to the new credentials API", e); + LOGGER.log( + Level.WARNING, "Exception converting legacy configuration to the new credentials API", e); } } - } } @@ -353,10 +608,11 @@ public EC2PrivateKey getPrivateKey() { } public String getInstanceCapStr() { - if (instanceCap == Integer.MAX_VALUE) + if (instanceCap == Integer.MAX_VALUE) { return ""; - else + } else { return String.valueOf(instanceCap); + } } public int getInstanceCap() { @@ -419,7 +675,7 @@ public Collection getTemplates(Label label) { * Gets the {@link KeyPairInfo} used for the launch. */ @CheckForNull - public synchronized KeyPair getKeyPair() throws AmazonClientException, IOException { + public synchronized KeyPair getKeyPair() throws SdkException, IOException { if (usableKeyPair == null) { EC2PrivateKey ec2PrivateKey = this.resolvePrivateKey(); if (ec2PrivateKey != null) { @@ -433,8 +689,8 @@ public synchronized KeyPair getKeyPair() throws AmazonClientException, IOExcepti * Debug command to attach to a running instance. */ @RequirePOST - public void doAttach(StaplerRequest req, StaplerResponse rsp, @QueryParameter String id) - throws ServletException, IOException, AmazonClientException { + public void doAttach(StaplerRequest2 req, StaplerResponse2 rsp, @QueryParameter String id) + throws ServletException, IOException, SdkException { checkPermission(PROVISION); SlaveTemplate t = getTemplates().get(0); @@ -450,35 +706,45 @@ public void doAttach(StaplerRequest req, StaplerResponse rsp, @QueryParameter St public HttpResponse doProvision(@QueryParameter String template) throws ServletException, IOException { checkPermission(PROVISION); if (template == null) { - throw HttpResponses.error(SC_BAD_REQUEST, "The 'template' query parameter is missing"); + throw HttpResponses.error(HttpServletResponse.SC_BAD_REQUEST, "The 'template' query parameter is missing"); } SlaveTemplate t = getTemplate(template); if (t == null) { - throw HttpResponses.error(SC_BAD_REQUEST, "No such template: " + template); + throw HttpResponses.error(HttpServletResponse.SC_BAD_REQUEST, "No such template: " + template); } final Jenkins jenkinsInstance = Jenkins.get(); if (jenkinsInstance.isQuietingDown()) { - throw HttpResponses.error(SC_BAD_REQUEST, "Jenkins instance is quieting down"); + throw HttpResponses.error(HttpServletResponse.SC_BAD_REQUEST, "Jenkins instance is quieting down"); } if (jenkinsInstance.isTerminating()) { - throw HttpResponses.error(SC_BAD_REQUEST, "Jenkins instance is terminating"); + throw HttpResponses.error(HttpServletResponse.SC_BAD_REQUEST, "Jenkins instance is terminating"); } try { List nodes = getNewOrExistingAvailableSlave(t, 1, true); - if (nodes == null || nodes.isEmpty()) - throw HttpResponses.error(SC_BAD_REQUEST, "Cloud or AMI instance cap would be exceeded for: " + template); + if (nodes == null || nodes.isEmpty()) { + throw HttpResponses.error( + HttpServletResponse.SC_BAD_REQUEST, + "Cloud or AMI instance cap would be exceeded for: " + template); + } - //Reconnect a stopped instance, the ADD is invoking the connect only for the node creation + // Reconnect a stopped instance, the ADD is invoking the connect only for the node creation Computer c = nodes.get(0).toComputer(); - if (nodes.get(0).getStopOnTerminate() && c != null) { - c.connect(false); + if (nodes.get(0).getStopOnTerminate() && c != null) { + PROVISIONING_EXECUTOR.execute(() -> { + try { + c.connect(false); + } catch (Exception e) { + LOGGER.log(Level.FINE, "Error connecting " + c.getName(), e); + } + }); } jenkinsInstance.addNode(nodes.get(0)); - return HttpResponses.redirectViaContextPath("/computer/" + nodes.get(0).getNodeName()); - } catch (AmazonClientException e) { - throw HttpResponses.error(SC_INTERNAL_SERVER_ERROR, e); + return HttpResponses.redirectViaContextPath( + "/computer/" + nodes.get(0).getNodeName()); + } catch (SdkException e) { + throw HttpResponses.error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e); } } @@ -488,39 +754,55 @@ public HttpResponse doProvision(@QueryParameter String template) throws ServletE * * @param template If left null, then all instances are counted. */ - private int countCurrentEC2Slaves(SlaveTemplate template) throws AmazonClientException { + private int countCurrentEC2Slaves(SlaveTemplate template) throws SdkException { String jenkinsServerUrl = JenkinsLocationConfiguration.get().getUrl(); if (jenkinsServerUrl == null) { - LOGGER.log(Level.WARNING, "No Jenkins server URL specified, it is strongly recommended to open /configure and set the server URL. " + - "Not having has disabled the per-controller instance cap counting (cf. https://github.com/jenkinsci/ec2-plugin/pull/310)"); + LOGGER.log( + Level.WARNING, + "No Jenkins server URL specified, it is strongly recommended to open /configure and set the server URL. " + + "Not having has disabled the per-controller instance cap counting (cf. https://github.com/jenkinsci/ec2-plugin/pull/310)"); } - LOGGER.log(Level.FINE, "Counting current agents: " - + (template != null ? (" AMI: " + template.getAmi() + " TemplateDesc: " + template.description) : " All AMIS") - + " Jenkins Server: " + jenkinsServerUrl); + LOGGER.log( + Level.FINE, + "Counting current agents: " + + (template != null + ? (" AMI: " + template.getAmi() + " TemplateDesc: " + template.description) + : " All AMIS") + + " Jenkins Server: " + jenkinsServerUrl); int n = 0; Set instanceIds = new HashSet<>(); String description = template != null ? template.description : null; List filters = getGenericFilters(jenkinsServerUrl, template); - filters.add(new Filter("instance-state-name").withValues("running", "pending", "stopping")); - DescribeInstancesRequest dir = new DescribeInstancesRequest().withFilters(filters); - DescribeInstancesResult result = null; + filters.add(Filter.builder() + .name("instance-state-name") + .values("running", "pending", "stopping") + .build()); + DescribeInstancesRequest dir = + DescribeInstancesRequest.builder().filters(filters).build(); + DescribeInstancesResponse result = null; do { result = connect().describeInstances(dir); - dir.setNextToken(result.getNextToken()); - for (Reservation r : result.getReservations()) { - for (Instance i : r.getInstances()) { - if (isEc2ProvisionedAmiSlave(i.getTags(), description)) { - LOGGER.log(Level.FINE, "Existing instance found: " + i.getInstanceId() + " AMI: " + i.getImageId() - + (template != null ? (" Template: " + description) : "") + " Jenkins Server: " + jenkinsServerUrl); + dir = DescribeInstancesRequest.builder() + .filters(filters) + .nextToken(result.nextToken()) + .build(); + for (Reservation r : result.reservations()) { + for (Instance i : r.instances()) { + if (isEc2ProvisionedAmiSlave(i.tags(), description)) { + LOGGER.log( + Level.FINE, + "Existing instance found: " + i.instanceId() + " AMI: " + i.imageId() + + (template != null ? (" Template: " + description) : "") + " Jenkins Server: " + + jenkinsServerUrl); n++; - instanceIds.add(i.getInstanceId()); + instanceIds.add(i.instanceId()); } } } - } while(result.getNextToken() != null); + } while (result.nextToken() != null); n += countCurrentEC2SpotSlaves(template, jenkinsServerUrl, instanceIds); @@ -533,80 +815,97 @@ private int countCurrentEC2Slaves(SlaveTemplate template) throws AmazonClientExc * * @param template If left null, then all spot instances are counted. */ - private int countCurrentEC2SpotSlaves(SlaveTemplate template, String jenkinsServerUrl, Set instanceIds) throws AmazonClientException { + private int countCurrentEC2SpotSlaves(SlaveTemplate template, String jenkinsServerUrl, Set instanceIds) + throws SdkException { int n = 0; String description = template != null ? template.description : null; List sirs = null; List filters = getGenericFilters(jenkinsServerUrl, template); if (template != null) { - filters.add(new Filter("launch.image-id").withValues(template.getAmi())); + filters.add(Filter.builder() + .name("launch.image-id") + .values(template.getAmi()) + .build()); } - DescribeSpotInstanceRequestsRequest dsir = new DescribeSpotInstanceRequestsRequest().withFilters(filters).withMaxResults(100); + DescribeSpotInstanceRequestsRequest dsir = DescribeSpotInstanceRequestsRequest.builder() + .filters(filters) + .maxResults(100) + .build(); Set sirSet = new HashSet<>(); - DescribeSpotInstanceRequestsResult sirResp = null; + DescribeSpotInstanceRequestsResponse sirResp = null; do { - try { - sirResp = connect().describeSpotInstanceRequests(dsir); - sirs = sirResp.getSpotInstanceRequests(); - dsir.setNextToken(sirResp.getNextToken()); - } catch (Exception ex) { - // Some ec2 implementations don't implement spot requests (Eucalyptus) - LOGGER.log(Level.FINEST, "Describe spot instance requests failed", ex); - break; - } + sirResp = connect().describeSpotInstanceRequests(dsir); + sirs = sirResp.spotInstanceRequests(); + dsir = DescribeSpotInstanceRequestsRequest.builder() + .filters(filters) + .maxResults(100) + .nextToken(sirResp.nextToken()) + .build(); if (sirs != null) { for (SpotInstanceRequest sir : sirs) { sirSet.add(sir); - if (sir.getState().equals("open") || sir.getState().equals("active")) { - if (sir.getInstanceId() != null && instanceIds.contains(sir.getInstanceId())) + if (sir.state() == SpotInstanceState.OPEN || sir.state() == SpotInstanceState.ACTIVE) { + if (sir.instanceId() != null && instanceIds.contains(sir.instanceId())) { continue; + } - if (isEc2ProvisionedAmiSlave(sir.getTags(), description)) { - LOGGER.log(Level.FINE, "Spot instance request found: " + sir.getSpotInstanceRequestId() + " AMI: " - + sir.getInstanceId() + " state: " + sir.getState() + " status: " + sir.getStatus()); + if (isEc2ProvisionedAmiSlave(sir.tags(), description)) { + LOGGER.log( + Level.FINE, + "Spot instance request found: " + sir.spotInstanceRequestId() + " AMI: " + + sir.instanceId() + " state: " + sir.state() + " status: " + + sir.status()); n++; - if (sir.getInstanceId() != null) - instanceIds.add(sir.getInstanceId()); + if (sir.instanceId() != null) { + instanceIds.add(sir.instanceId()); + } } } else { // Cancelled or otherwise dead for (Node node : Jenkins.get().getNodes()) { try { - if (!(node instanceof EC2SpotSlave)) + if (!(node instanceof EC2SpotSlave ec2Slave)) { continue; - EC2SpotSlave ec2Slave = (EC2SpotSlave) node; - if (ec2Slave.getSpotInstanceRequestId().equals(sir.getSpotInstanceRequestId())) { - LOGGER.log(Level.INFO, "Removing dead request: " + sir.getSpotInstanceRequestId() + " AMI: " - + sir.getInstanceId() + " state: " + sir.getState() + " status: " + sir.getStatus()); + } + if (ec2Slave.getSpotInstanceRequestId().equals(sir.spotInstanceRequestId())) { + LOGGER.log( + Level.INFO, + "Removing dead request: " + sir.spotInstanceRequestId() + " AMI: " + + sir.instanceId() + " state: " + sir.state() + " status: " + + sir.status()); Jenkins.get().removeNode(node); break; } } catch (IOException e) { - LOGGER.log(Level.WARNING, "Failed to remove node for dead request: " + sir.getSpotInstanceRequestId() - + " AMI: " + sir.getInstanceId() + " state: " + sir.getState() + " status: " + sir.getStatus(), + LOGGER.log( + Level.WARNING, + "Failed to remove node for dead request: " + sir.spotInstanceRequestId() + + " AMI: " + sir.instanceId() + " state: " + sir.state() + + " status: " + sir.status(), e); } } } } } - } while(sirResp.getNextToken() != null); + } while (sirResp.nextToken() != null); n += countJenkinsNodeSpotInstancesWithoutRequests(template, sirSet, instanceIds); return n; } // Count nodes where the spot request does not yet exist (sometimes it takes time for the request to appear // in the EC2 API) - private int countJenkinsNodeSpotInstancesWithoutRequests(SlaveTemplate template, Set sirSet, Set instanceIds) throws AmazonClientException { + private int countJenkinsNodeSpotInstancesWithoutRequests( + SlaveTemplate template, Set sirSet, Set instanceIds) throws SdkException { int n = 0; for (Node node : Jenkins.get().getNodes()) { - if (!(node instanceof EC2SpotSlave)) + if (!(node instanceof EC2SpotSlave ec2Slave)) { continue; - EC2SpotSlave ec2Slave = (EC2SpotSlave) node; + } SpotInstanceRequest sir = ec2Slave.getSpotRequest(); if (sir == null) { @@ -615,26 +914,35 @@ private int countJenkinsNodeSpotInstancesWithoutRequests(SlaveTemplate template, continue; } - if (sirSet.contains(sir)) + if (sirSet.contains(sir)) { continue; + } sirSet.add(sir); - if (sir.getState().equals("open") || sir.getState().equals("active")) { + if (sir.state() == SpotInstanceState.OPEN || sir.state() == SpotInstanceState.ACTIVE) { if (template != null) { - List instanceTags = sir.getTags(); + List instanceTags = sir.tags(); for (Tag tag : instanceTags) { - if (StringUtils.equals(tag.getKey(), EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE) && StringUtils.equals(tag.getValue(), getSlaveTypeTagValue(EC2_SLAVE_TYPE_SPOT, template.description)) && sir.getLaunchSpecification().getImageId().equals(template.getAmi())) { + if (StringUtils.equals(tag.key(), EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE) + && StringUtils.equals( + tag.value(), getSlaveTypeTagValue(EC2_SLAVE_TYPE_SPOT, template.description)) + && sir.launchSpecification().imageId().equals(template.getAmi())) { - if (sir.getInstanceId() != null && instanceIds.contains(sir.getInstanceId())) + if (sir.instanceId() != null && instanceIds.contains(sir.instanceId())) { continue; + } - LOGGER.log(Level.FINE, "Spot instance request found (from node): " + sir.getSpotInstanceRequestId() + " AMI: " - + sir.getInstanceId() + " state: " + sir.getState() + " status: " + sir.getStatus()); + LOGGER.log( + Level.FINE, + "Spot instance request found (from node): " + sir.spotInstanceRequestId() + + " AMI: " + sir.instanceId() + " state: " + sir.state() + " status: " + + sir.status()); n++; - if (sir.getInstanceId() != null) - instanceIds.add(sir.getInstanceId()); + if (sir.instanceId() != null) { + instanceIds.add(sir.instanceId()); + } } } } @@ -643,15 +951,23 @@ private int countJenkinsNodeSpotInstancesWithoutRequests(SlaveTemplate template, return n; } - private List getGenericFilters(String jenkinsServerUrl, SlaveTemplate template) { List filters = new ArrayList<>(); - filters.add(new Filter("tag-key").withValues(EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE)); + filters.add(Filter.builder() + .name("tag-key") + .values(EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE) + .build()); if (jenkinsServerUrl != null) { // The instances must match the jenkins server url - filters.add(new Filter("tag:" + EC2Tag.TAG_NAME_JENKINS_SERVER_URL).withValues(jenkinsServerUrl)); + filters.add(Filter.builder() + .name("tag:" + EC2Tag.TAG_NAME_JENKINS_SERVER_URL) + .values(jenkinsServerUrl) + .build()); } else { - filters.add(new Filter("tag-key").withValues(EC2Tag.TAG_NAME_JENKINS_SERVER_URL)); + filters.add(Filter.builder() + .name("tag-key") + .values(EC2Tag.TAG_NAME_JENKINS_SERVER_URL) + .build()); } if (template != null) { @@ -659,7 +975,10 @@ private List getGenericFilters(String jenkinsServerUrl, SlaveTemplate te if (tags != null) { for (EC2Tag tag : tags) { if (tag.getName() != null && tag.getValue() != null) { - filters.add(new Filter("tag:" + tag.getName()).withValues(tag.getValue())); + filters.add(Filter.builder() + .name("tag:" + tag.getName()) + .values(tag.getValue()) + .build()); } } } @@ -669,15 +988,17 @@ private List getGenericFilters(String jenkinsServerUrl, SlaveTemplate te private boolean isEc2ProvisionedAmiSlave(List tags, String description) { for (Tag tag : tags) { - if (StringUtils.equals(tag.getKey(), EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE)) { + if (StringUtils.equals(tag.key(), EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE)) { if (description == null) { return true; - } else if (StringUtils.equals(tag.getValue(), EC2Cloud.EC2_SLAVE_TYPE_DEMAND) - || StringUtils.equals(tag.getValue(), EC2Cloud.EC2_SLAVE_TYPE_SPOT)) { + } else if (StringUtils.equals(tag.value(), EC2Cloud.EC2_SLAVE_TYPE_DEMAND) + || StringUtils.equals(tag.value(), EC2Cloud.EC2_SLAVE_TYPE_SPOT)) { // To handle cases where description is null and also upgrade cases for existing agent nodes. return true; - } else if (StringUtils.equals(tag.getValue(), getSlaveTypeTagValue(EC2Cloud.EC2_SLAVE_TYPE_DEMAND, description)) - || StringUtils.equals(tag.getValue(), getSlaveTypeTagValue(EC2Cloud.EC2_SLAVE_TYPE_SPOT, description))) { + } else if (StringUtils.equals( + tag.value(), getSlaveTypeTagValue(EC2Cloud.EC2_SLAVE_TYPE_DEMAND, description)) + || StringUtils.equals( + tag.value(), getSlaveTypeTagValue(EC2Cloud.EC2_SLAVE_TYPE_SPOT, description))) { return true; } else { return false; @@ -689,24 +1010,51 @@ private boolean isEc2ProvisionedAmiSlave(List tags, String description) { /** * Returns the maximum number of possible agents that can be created. + * Uses cached instance counts when fresh (within TTL) to avoid repeated EC2 API calls. */ - private int getPossibleNewSlavesCount(SlaveTemplate template) throws AmazonClientException { + private int getPossibleNewSlavesCount(SlaveTemplate template) throws SdkException { + long now = System.currentTimeMillis(); + String templateKey = Objects.toString(template.description, "") + ":" + template.getAmi(); + + if (now - instanceCountCacheTimestamp < INSTANCE_COUNT_CACHE_TTL_MS) { + int total = cachedTotalSlaves; + Integer templateCount = cachedTemplateSlaves.get(templateKey); + if (total >= 0 && templateCount != null && templateCount >= 0) { + int availableTotalSlaves = instanceCap - total; + int availableAmiSlaves = template.getInstanceCap() - templateCount; + return Math.min(availableAmiSlaves, availableTotalSlaves); + } + } + int estimatedTotalSlaves = countCurrentEC2Slaves(null); int estimatedAmiSlaves = countCurrentEC2Slaves(template); + instanceCountCacheTimestamp = now; + cachedTotalSlaves = estimatedTotalSlaves; + cachedTemplateSlaves.put(templateKey, estimatedAmiSlaves); + int availableTotalSlaves = instanceCap - estimatedTotalSlaves; int availableAmiSlaves = template.getInstanceCap() - estimatedAmiSlaves; - LOGGER.log(Level.FINE, "Available Total Agents: " + availableTotalSlaves + " Available AMI agents: " + availableAmiSlaves - + " AMI: " + template.getAmi() + " TemplateDesc: " + template.description); + LOGGER.log( + Level.FINE, + "Available Total Agents: " + availableTotalSlaves + " Available AMI agents: " + availableAmiSlaves + + " AMI: " + template.getAmi() + " TemplateDesc: " + template.description); return Math.min(availableAmiSlaves, availableTotalSlaves); } + private void invalidateInstanceCountCache() { + instanceCountCacheTimestamp = 0; + cachedTotalSlaves = -1; + cachedTemplateSlaves.clear(); + } + /** * Obtains a agent whose AMI matches the AMI of the given template, and that also has requiredLabel (if requiredLabel is non-null) * forceCreateNew specifies that the creation of a new agent is required. Otherwise, an existing matching agent may be re-used */ - private List getNewOrExistingAvailableSlave(SlaveTemplate t, int number, boolean forceCreateNew) throws IOException { + private List getNewOrExistingAvailableSlave(SlaveTemplate t, int number, boolean forceCreateNew) + throws IOException { try { slaveCountingLock.lock(); int possibleSlavesCount = getPossibleNewSlavesCount(t); @@ -716,19 +1064,26 @@ private List getNewOrExistingAvailableSlave(SlaveTemplate t, i } EnumSet provisionOptions; - if (forceCreateNew) + if (forceCreateNew) { provisionOptions = EnumSet.of(SlaveTemplate.ProvisionOptions.FORCE_CREATE); - else + } else { provisionOptions = EnumSet.of(SlaveTemplate.ProvisionOptions.ALLOW_CREATE); + } if (number > possibleSlavesCount) { - LOGGER.log(Level.INFO, String.format("%d nodes were requested for the template %s, " + - "but because of instance cap only %d can be provisioned", number, t, possibleSlavesCount)); + LOGGER.log( + Level.INFO, + String.format( + "%d nodes were requested for the template %s, " + + "but because of instance cap only %d can be provisioned", + number, t, possibleSlavesCount)); number = possibleSlavesCount; } return t.provision(number, provisionOptions); - } finally { slaveCountingLock.unlock(); } + } finally { + slaveCountingLock.unlock(); + } } @Override @@ -745,50 +1100,156 @@ public Collection provision(final Label label, int excessWorkload) return Collections.emptyList(); } - for (SlaveTemplate t : matchingTemplates) { - try { - LOGGER.log(Level.INFO, "{0}. Attempting to provision agent needed by excess workload of " + excessWorkload + " units", t); - int number = Math.max(excessWorkload / t.getNumExecutors(), 1); - final List slaves = getNewOrExistingAvailableSlave(t, number, false); - - if (slaves == null || slaves.isEmpty()) { - LOGGER.warning("Can't raise nodes for " + t); - continue; - } - - for (final EC2AbstractSlave slave : slaves) { - if (slave == null) { - LOGGER.warning("Can't raise node for " + t); - continue; - } + for (final SlaveTemplate t : matchingTemplates) { + LOGGER.log( + Level.INFO, + "{0}. Attempting to provision agent needed by excess workload of " + excessWorkload + " units", + t); + final int number = Math.max(excessWorkload / t.getNumExecutors(), 1); - plannedNodes.add(createPlannedNode(t, slave)); - excessWorkload -= t.getNumExecutors(); - } + // Defer runInstances to background; return PlannedNodes immediately for fast NodeProvisioner response + CompletableFuture> provisionFuture = CompletableFuture.supplyAsync( + () -> { + try { + return getNewOrExistingAvailableSlave(t, number, false); + } catch (AwsServiceException e) { + LOGGER.log(Level.WARNING, t + ". Exception during provisioning", e); + if ("RequestExpired".equals(e.awsErrorDetails().errorCode()) + || "ExpiredToken".equals(e.awsErrorDetails().errorCode())) { + LOGGER.log( + Level.INFO, "Reconnecting to EC2 due to RequestExpired or ExpiredToken error"); + try { + reconnectToEc2(); + } catch (IOException e2) { + LOGGER.log(Level.WARNING, "Failed to reconnect ec2", e2); + } + } + return null; + } catch (SdkException | IOException e) { + LOGGER.log(Level.WARNING, t + ". Exception during provisioning", e); + return null; + } + }, + PROVISIONING_EXECUTOR); - LOGGER.log(Level.INFO, "{0}. Attempting provision finished, excess workload: " + excessWorkload, t); - if (excessWorkload == 0) break; - } catch (AmazonServiceException e) { - LOGGER.log(Level.WARNING, t + ". Exception during provisioning", e); - if (e.getErrorCode().equals("RequestExpired")) { - // JENKINS-71554: A RequestExpired error can indicate that credentials have expired so reconnect - LOGGER.log(Level.INFO, "[JENKINS-71554] Reconnecting to EC2 due to RequestExpired error"); - try { - reconnectToEc2(); - } catch (IOException e2) { - LOGGER.log(Level.WARNING, "Failed to reconnect ec2", e2); - } + provisionFuture.whenComplete((slaves, ex) -> { + if (slaves != null && !slaves.isEmpty()) { + invalidateInstanceCountCache(); } - } catch (AmazonClientException | IOException e) { - LOGGER.log(Level.WARNING, t + ". Exception during provisioning", e); + scheduleQueueMaintenance(); + }); + + for (int i = 0; i < number; i++) { + final int index = i; + CompletableFuture nodeFuture = provisionFuture + .thenApplyAsync( + slaves -> slaves != null && index < slaves.size() ? slaves.get(index) : null, + PROVISIONING_EXECUTOR) + .thenComposeAsync( + slave -> slave != null + ? waitForRunningAndConnectAsync(t, slave) + : CompletableFuture.completedFuture(null), + Computer.threadPoolForRemoting); + + plannedNodes.add(new PlannedNode(t.getDisplayName(), nodeFuture, t.getNumExecutors())); } + + LOGGER.log( + Level.INFO, "{0}. Provision scheduled for {1} nodes, returning immediately", new Object[] {t, number + }); + LOGGER.log(Level.INFO, "We have now {0} computers, waiting for {1} more", new Object[] { + jenkinsInstance.getComputers().length, plannedNodes.size() + }); + return plannedNodes; } - LOGGER.log(Level.INFO, "We have now {0} computers, waiting for {1} more", - new Object[]{jenkinsInstance.getComputers().length, plannedNodes.size()}); return plannedNodes; } - private static void attachSlavesToJenkins(Jenkins jenkins, List slaves, SlaveTemplate t) throws IOException { + /** + * Waits for the instance to reach RUNNING, adds the node to Jenkins, connects, then returns it. + * Runs on Computer.threadPoolForRemoting to avoid blocking the provisioning executor. + */ + private CompletableFuture waitForRunningAndConnectAsync(final SlaveTemplate t, final EC2AbstractSlave slave) { + return CompletableFuture.supplyAsync( + () -> { + int retryCount = 0; + final int describeLimit = 2; + while (true) { + String instanceId = slave.getInstanceId(); + if (slave instanceof EC2SpotSlave) { + if (((EC2SpotSlave) slave).isSpotRequestDead()) { + LOGGER.log( + Level.WARNING, + "{0} Spot request died, can't do anything. Terminate provisioning", + t); + return null; + } + if (StringUtils.isEmpty(instanceId)) { + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } + continue; + } + } + + try { + Instance instance = CloudHelper.getInstanceWithRetry(instanceId, slave.getCloud()); + if (instance == null) { + LOGGER.log( + Level.WARNING, + "{0} Can't find instance with instance id `{1}` in cloud {2}. Terminate provisioning ", + new Object[] {t, instanceId, slave.cloudName}); + return null; + } + + InstanceStateName state = instance.state().name(); + if (state.equals(InstanceStateName.RUNNING)) { + Computer c = slave.toComputer(); + if (c != null) { + c.connect(false); + } + long secondsSinceStart = Instant.now().until(instance.launchTime(), ChronoUnit.SECONDS); + LOGGER.log( + Level.INFO, + "{0} Node {1} moved to RUNNING state in {2} seconds and is ready to be connected by Jenkins", + new Object[] {t, slave.getNodeName(), secondsSinceStart}); + scheduleQueueMaintenance(); + return slave; + } + + if (!state.equals(InstanceStateName.PENDING)) { + if (retryCount >= describeLimit) { + LOGGER.log( + Level.WARNING, + "Instance {0} did not move to running after {1} attempts, terminating provisioning", + new Object[] {instanceId, retryCount}); + return null; + } + LOGGER.log( + Level.INFO, + "Attempt {0}: {1}. Node {2} is neither pending, neither running, it''s {3}. Will try again after 5s", + new Object[] {retryCount, t, slave.getNodeName(), state}); + retryCount++; + } + + Thread.sleep(5000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } catch (SdkException e) { + LOGGER.log(Level.WARNING, t + ". Exception waiting for instance", e); + return null; + } + } + }, + Computer.threadPoolForRemoting); + } + + private static void attachSlavesToJenkins(Jenkins jenkins, List slaves, SlaveTemplate t) + throws IOException { for (final EC2AbstractSlave slave : slaves) { if (slave == null) { LOGGER.warning("Can't raise node for " + t); @@ -797,7 +1258,13 @@ private static void attachSlavesToJenkins(Jenkins jenkins, List { + try { + c.connect(false); + } catch (Exception e) { + LOGGER.log(Level.FINE, "Error connecting " + c.getName(), e); + } + }); } jenkins.addNode(slave); } @@ -815,7 +1282,7 @@ public void provision(SlaveTemplate t, int number) { } try { - LOGGER.log(Level.INFO, "{0}. Attempting to provision {1} agent(s)", new Object[]{t, number}); + LOGGER.log(Level.INFO, "{0}. Attempting to provision {1} agent(s)", new Object[] {t, number}); final List slaves = getNewOrExistingAvailableSlave(t, number, false); if (slaves == null || slaves.isEmpty()) { @@ -824,11 +1291,13 @@ public void provision(SlaveTemplate t, int number) { } attachSlavesToJenkins(jenkinsInstance, slaves, t); + invalidateInstanceCountCache(); LOGGER.log(Level.INFO, "{0}. Attempting provision finished", t); - LOGGER.log(Level.INFO, "We have now {0} computers, waiting for {1} more", - new Object[]{Jenkins.get().getComputers().length, number}); - } catch (AmazonClientException | IOException e) { + LOGGER.log(Level.INFO, "We have now {0} computers, waiting for {1} more", new Object[] { + Jenkins.get().getComputers().length, number + }); + } catch (SdkException | IOException e) { LOGGER.log(Level.WARNING, t + ". Exception during provisioning", e); } } @@ -840,10 +1309,11 @@ public void provision(SlaveTemplate t, int number) { * @param template The corresponding SlaveTemplate of the nodes that are to be re-attached * @param requestedNum The requested number of nodes to re-attach. We don't go above this in the case its value corresponds to an instance cap. */ - void attemptReattachOrphanOrStoppedNodes(Jenkins jenkinsInstance, SlaveTemplate template, int requestedNum) throws IOException { + void attemptReattachOrphanOrStoppedNodes(Jenkins jenkinsInstance, SlaveTemplate template, int requestedNum) + throws IOException { LOGGER.info("Attempting to wake & re-attach orphan/stopped nodes"); - AmazonEC2 ec2 = this.connect(); - DescribeInstancesResult diResult = template.getDescribeInstanceResult(ec2,true); + Ec2Client ec2 = this.connect(); + DescribeInstancesResponse diResult = template.getDescribeInstanceResult(ec2, true); List orphansOrStopped = template.findOrphansOrStopped(diResult, requestedNum); template.wakeOrphansOrStoppedUp(ec2, orphansOrStopped); /* If the number of possible nodes to re-attach is greater than the number of nodes requested, will only attempt to re-attach up to the number requested */ @@ -851,117 +1321,73 @@ void attemptReattachOrphanOrStoppedNodes(Jenkins jenkinsInstance, SlaveTemplate orphansOrStopped.remove(0); } attachSlavesToJenkins(jenkinsInstance, template.toSlaves(orphansOrStopped), template); - if (orphansOrStopped.size() > 0) { + if (!orphansOrStopped.isEmpty()) { + invalidateInstanceCountCache(); LOGGER.info("Found and re-attached " + orphansOrStopped.size() + " orphan/stopped nodes"); } } - private PlannedNode createPlannedNode(final SlaveTemplate t, final EC2AbstractSlave slave) { - return new PlannedNode(t.getDisplayName(), - Computer.threadPoolForRemoting.submit(new Callable() { - int retryCount = 0; - private static final int DESCRIBE_LIMIT = 2; - public Node call() throws Exception { - while (true) { - String instanceId = slave.getInstanceId(); - if (slave instanceof EC2SpotSlave) { - if (((EC2SpotSlave) slave).isSpotRequestDead()) { - LOGGER.log(Level.WARNING, "{0} Spot request died, can't do anything. Terminate provisioning", t); - return null; - } - - // Spot Instance does not have instance id yet. - if (StringUtils.isEmpty(instanceId)) { - Thread.sleep(5000); - continue; - } - } - - Instance instance = CloudHelper.getInstanceWithRetry(instanceId, slave.getCloud()); - if (instance == null) { - LOGGER.log(Level.WARNING, "{0} Can't find instance with instance id `{1}` in cloud {2}. Terminate provisioning ", - new Object[]{t, instanceId, slave.cloudName}); - return null; - } - - InstanceStateName state = InstanceStateName.fromValue(instance.getState().getName()); - if (state.equals(InstanceStateName.Running)) { - //Spot instance are not reconnected automatically, - // but could be new orphans that has the option enable - Computer c = slave.toComputer(); - if (slave.getStopOnTerminate() && (c != null )) { - c.connect(false); - } - - long startTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - instance.getLaunchTime().getTime()); - LOGGER.log(Level.INFO, "{0} Node {1} moved to RUNNING state in {2} seconds and is ready to be connected by Jenkins", - new Object[]{t, slave.getNodeName(), startTime}); - return slave; - } - - if (!state.equals(InstanceStateName.Pending)) { - - if (retryCount >= DESCRIBE_LIMIT){ - LOGGER.log(Level.WARNING,"Instance {0} did not move to running after {1} attempts, terminating provisioning", - new Object[]{instanceId, retryCount}); - return null; - } - - LOGGER.log(Level.INFO, "Attempt {0}: {1}. Node {2} is neither pending, neither running, it''s {3}. Will try again after 5s", - new Object[]{retryCount, t, slave.getNodeName(), state}); - retryCount++; - } - - Thread.sleep(5000); - } - } - }) - , t.getNumExecutors()); - } - - @Override public boolean canProvision(Label label) { return !getTemplates(label).isEmpty(); } - protected AWSCredentialsProvider createCredentialsProvider() { - return createCredentialsProvider(useInstanceProfileForCredentials, credentialsId); + protected AwsCredentialsProvider createCredentialsProvider() { + return createCredentialsProvider( + isUseInstanceProfileForCredentials(), + getCredentialsId(), + getRoleArn(), + getRoleSessionName(), + getRegion()); } public static String getSlaveTypeTagValue(String slaveType, String templateDescription) { return templateDescription != null ? slaveType + "_" + templateDescription : slaveType; } - public static AWSCredentialsProvider createCredentialsProvider(final boolean useInstanceProfileForCredentials, final String credentialsId) { + public static AwsCredentialsProvider createCredentialsProvider( + final boolean useInstanceProfileForCredentials, final String credentialsId) { if (useInstanceProfileForCredentials) { - return new InstanceProfileCredentialsProvider(false); + return InstanceProfileCredentialsProvider.create(); } else if (StringUtils.isBlank(credentialsId)) { - return new DefaultAWSCredentialsProviderChain(); + return DefaultCredentialsProvider.builder().build(); } else { AmazonWebServicesCredentials credentials = getCredentials(credentialsId); - if (credentials != null) - return new AWSStaticCredentialsProvider(credentials.getCredentials()); + if (credentials != null) { + return StaticCredentialsProvider.create(credentials.resolveCredentials()); + } } - return new DefaultAWSCredentialsProviderChain(); + return DefaultCredentialsProvider.builder().build(); } - public static AWSCredentialsProvider createCredentialsProvider( + public static AwsCredentialsProvider createCredentialsProvider( final boolean useInstanceProfileForCredentials, final String credentialsId, final String roleArn, final String roleSessionName, final String region) { - AWSCredentialsProvider provider = createCredentialsProvider(useInstanceProfileForCredentials, credentialsId); + AwsCredentialsProvider provider = createCredentialsProvider(useInstanceProfileForCredentials, credentialsId); + + if (StringUtils.isNotEmpty(roleArn)) { + AssumeRoleRequest assumeRoleRequest = AssumeRoleRequest.builder() + .roleArn(roleArn) + .roleSessionName(StringUtils.defaultIfBlank(roleSessionName, "Jenkins")) + .build(); + + StsClientBuilder stsClientBuilder = StsClient.builder() + .credentialsProvider(provider) + .httpClient(getHttpClient()) + .overrideConfiguration(createClientOverrideConfiguration()); + Region parsed = parseRegion(region); + if (parsed != null) { + stsClientBuilder.region(parsed); + } + StsClient stsClient = stsClientBuilder.build(); - if (StringUtils.isNotEmpty(roleArn) && StringUtils.isNotEmpty(roleSessionName)) { - return new STSAssumeRoleSessionCredentialsProvider.Builder(roleArn, roleSessionName) - .withStsClient(AWSSecurityTokenServiceClientBuilder.standard() - .withCredentials(provider) - .withRegion(region) - .withClientConfiguration(createClientConfiguration(convertHostName(region))) - .build()) + return StsAssumeRoleCredentialsProvider.builder() + .stsClient(stsClient) + .refreshRequest(assumeRoleRequest) .build(); } @@ -973,102 +1399,65 @@ private static AmazonWebServicesCredentials getCredentials(@CheckForNull String if (StringUtils.isBlank(credentialsId)) { return null; } - return (AmazonWebServicesCredentials) CredentialsMatchers.firstOrNull( - CredentialsProvider.lookupCredentials(AmazonWebServicesCredentials.class, Jenkins.get(), - ACL.SYSTEM, Collections.emptyList()), + return CredentialsMatchers.firstOrNull( + CredentialsProvider.lookupCredentialsInItemGroup( + AmazonWebServicesCredentials.class, Jenkins.get(), ACL.SYSTEM2, Collections.emptyList()), CredentialsMatchers.withId(credentialsId)); } - private AmazonEC2 reconnectToEc2() throws IOException { - synchronized(this) { - connection = AmazonEC2Factory.getInstance().connect(createCredentialsProvider(), getEc2EndpointUrl()); + private Ec2Client reconnectToEc2() throws IOException { + synchronized (this) { + connection = AmazonEC2Factory.getInstance() + .connect(createCredentialsProvider(), parseRegion(getRegion()), parseEndpoint(getAltEC2Endpoint())); return connection; } } /** - * Connects to EC2 and returns {@link AmazonEC2}, which can then be used to communicate with EC2. + * Connects to EC2 and returns {@link Ec2Client}, which can then be used to communicate with EC2. */ - public AmazonEC2 connect() throws AmazonClientException { + public Ec2Client connect() throws SdkException { try { if (connection != null) { return connection; - } - else { + } else { return reconnectToEc2(); } } catch (IOException e) { - throw new AmazonClientException("Failed to retrieve the endpoint", e); + throw SdkException.create("Failed to retrieve the endpoint", e); } } - public static ClientConfiguration createClientConfiguration(final String host) { - ClientConfiguration config = new ClientConfiguration(); - config.setMaxErrorRetry(16); // Default retry limit (3) is low and often - // cause problems. Raise it a bit. - // See: https://issues.jenkins-ci.org/browse/JENKINS-26800 - config.setSignerOverride("AWS4SignerType"); - ProxyConfiguration proxyConfig = Jenkins.get().proxy; - Proxy proxy = proxyConfig == null ? Proxy.NO_PROXY : proxyConfig.createProxy(host); - if (!proxy.equals(Proxy.NO_PROXY) && proxy.address() instanceof InetSocketAddress) { - InetSocketAddress address = (InetSocketAddress) proxy.address(); - config.setProxyHost(address.getHostName()); - config.setProxyPort(address.getPort()); - if (null != proxyConfig.getUserName()) { - config.setProxyUsername(proxyConfig.getUserName()); - config.setProxyPassword(proxyConfig.getPassword()); + public static SdkHttpClient getHttpClient() { + Jenkins instance = Jenkins.getInstanceOrNull(); + + ProxyConfiguration proxy = instance != null ? instance.proxy : null; + ApacheHttpClient.Builder builder = ApacheHttpClient.builder(); + if (proxy != null && proxy.name != null && !proxy.name.isEmpty()) { + software.amazon.awssdk.http.apache.ProxyConfiguration.Builder proxyConfiguration = + software.amazon.awssdk.http.apache.ProxyConfiguration.builder() + .endpoint(URI.create(String.format("http://%s:%s", proxy.name, proxy.port))); + if (proxy.getUserName() != null) { + proxyConfiguration.username(proxy.getUserName()); + proxyConfiguration.password(Secret.toString(proxy.getSecretPassword())); } + List patterns = proxy.getNoProxyHostPatterns(); + if (patterns != null && !patterns.isEmpty()) { + patterns.stream().map(Pattern::pattern).forEach(proxyConfiguration::addNonProxyHost); + } + builder.proxyConfiguration(proxyConfiguration.build()); } - return config; - } - - /*** - * Returns the DNS endpoint for a AWS service based on region provided - */ - public static String getAwsPartitionHostForService(String region, String service) { - String host; - if (region != null && region.startsWith("cn-")) { - host = service + "." + region + "." + AWS_CN_URL_HOST; - } else { - host = service + "." + region + "." + AWS_URL_HOST; - } - return host; - } - - /*** - * 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 = DEFAULT_EC2_HOST; - if (!ec2HostName.contains(".")) { - ec2HostName = getAwsPartitionHostForService(ec2HostName, "ec2"); - } - return ec2HostName; - } - - /*** - * 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; - return Integer.parseInt(ec2Port); + return builder.build(); } - /** - * 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 AmazonClientException { - AWSCredentialsProvider provider = createCredentialsProvider(); - AWSCredentials credentials = provider.getCredentials(); - long expires = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(60); - GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(path, credentials.getAWSSecretKey()); - request.setExpiration(new Date(expires)); - AmazonS3 s3 = AmazonS3ClientBuilder.standard().withCredentials(provider).build(); - return s3.generatePresignedUrl(request); + public static ClientOverrideConfiguration createClientOverrideConfiguration() { + // Default retry limit (3) is low and often cause problems. Raise it a bit. + // See: https://issues.jenkins-ci.org/browse/JENKINS-26800 + ClientOverrideConfiguration config = ClientOverrideConfiguration.builder() + .putAdvancedOption(SdkAdvancedClientOption.SIGNER, Aws4Signer.create()) + .retryPolicy(RetryPolicy.builder().numRetries(16).build()) + .build(); + return config; } /* Parse a url or return a sensible error */ @@ -1081,24 +1470,34 @@ public static URL checkEndPoint(String url) throws FormValidation { } @CheckForNull - private static SSHUserPrivateKey getSshCredential(String id, ItemGroup context){ + private static SSHUserPrivateKey getSshCredential(String id, ItemGroup context) { SSHUserPrivateKey credential = CredentialsMatchers.firstOrNull( - CredentialsProvider.lookupCredentials( - SSHUserPrivateKey.class, // (1) - context, - null, - Collections.emptyList()), - CredentialsMatchers.withId(id)); - - if (credential == null){ - LOGGER.log(Level.WARNING, "EC2 Plugin could not find the specified credentials ({0}) in the Jenkins Global Credentials Store, EC2 Plugin for cloud must be manually reconfigured", new String[]{id}); + CredentialsProvider.lookupCredentialsInItemGroup( + SSHUserPrivateKey.class, // (1) + context, + null, + Collections.emptyList()), + CredentialsMatchers.withId(id)); + + if (credential == null) { + LOGGER.log( + Level.WARNING, + "EC2 Plugin could not find the specified credentials ({0}) in the Jenkins Global Credentials Store, EC2 Plugin for cloud must be manually reconfigured", + new String[] {id}); } return credential; } - public static abstract class DescriptorImpl extends Descriptor { + @Extension + @Symbol("amazonEC2") + public static class DescriptorImpl extends Descriptor { + + @Override + public String getDisplayName() { + return "Amazon EC2"; + } public InstanceType[] getInstanceTypes() { return InstanceType.values(); @@ -1110,107 +1509,222 @@ public FormValidation doCheckUseInstanceProfileForCredentials(@QueryParameter bo return FormValidation.ok(); } try { - new InstanceProfileCredentialsProvider(false).getCredentials(); + InstanceProfileCredentialsProvider.builder().build().resolveCredentials(); return FormValidation.ok(); - } catch (AmazonClientException e) { + } catch (SdkException e) { return FormValidation.error(Messages.EC2Cloud_FailedToObtainCredentialsFromEC2(), e.getMessage()); } } @POST - public ListBoxModel doFillSshKeysCredentialsIdItems(@AncestorInPath ItemGroup context, @QueryParameter String sshKeysCredentialsId) { + public ListBoxModel doFillSshKeysCredentialsIdItems( + @AncestorInPath ItemGroup context, @QueryParameter String sshKeysCredentialsId) { AbstractIdCredentialsListBoxModel result = new StandardListBoxModel(); if (Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { - result = result - .includeMatchingAs(Jenkins.getAuthentication(), context, SSHUserPrivateKey.class, Collections.emptyList(), CredentialsMatchers.always()) - .includeMatchingAs(ACL.SYSTEM, context, SSHUserPrivateKey.class, Collections.emptyList(), CredentialsMatchers.always()) + result = result.includeEmptyValue() + .includeMatchingAs( + Jenkins.getAuthentication2(), + context, + SSHUserPrivateKey.class, + Collections.emptyList(), + CredentialsMatchers.always()) + .includeMatchingAs( + ACL.SYSTEM2, + context, + SSHUserPrivateKey.class, + Collections.emptyList(), + CredentialsMatchers.always()) .includeCurrentValue(sshKeysCredentialsId); } return result; } + @NonNull + @RequirePOST + @Restricted(NoExternalUse.class) + @VisibleForTesting + public FormValidation doCheckRoleSessionName( + @QueryParameter String roleArn, @QueryParameter String roleSessionName) { + // Don't do anything if the user is only reading the configuration + if (Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + if (StringUtils.isNotEmpty(roleArn) && StringUtils.isBlank(roleSessionName)) { + return FormValidation.warning( + "Session Name is recommended when specifying an Arn Role. If empty, 'Jenkins' will be used."); + } + } + return FormValidation.ok(); + } + @RequirePOST - public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup context, @QueryParameter String value) throws IOException, ServletException { + public FormValidation doCheckSshKeysCredentialsId( + @AncestorInPath ItemGroup context, @QueryParameter String value) throws IOException, ServletException { if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { // Don't do anything if the user is only reading the configuration return FormValidation.ok(); } - if (value == null || value.isEmpty()){ - return FormValidation.error("No ssh credentials selected"); - } - SSHUserPrivateKey sshCredential = getSshCredential(value, context); - String privateKey = ""; - if (sshCredential != null) { - privateKey = sshCredential.getPrivateKey(); + String privateKey; + List validations = new ArrayList<>(); + + if (System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { + // not using a static ssh key file + if (value == null || value.isEmpty()) { + return FormValidation.error("No ssh credentials selected and no private key file defined"); + } + + SSHUserPrivateKey sshCredential = getSshCredential(value, context); + if (sshCredential != null) { + privateKey = sshCredential.getPrivateKey(); + } else { + return FormValidation.error("Failed to find credential \"" + value + "\" in store."); + } } else { - return FormValidation.error("Failed to find credential \"" + value + "\" in store."); + EC2PrivateKey k = EC2PrivateKey.fetchFromDisk(); + if (k == null) { + validations.add(FormValidation.error( + "Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH))); + if (!StringUtils.isEmpty(value)) { + validations.add(FormValidation.warning( + "Private key file path defined, selected credential will be ignored")); + } + return FormValidation.aggregate(validations); + } + privateKey = k.getPrivateKey(); } boolean hasStart = false, hasEnd = false; BufferedReader br = new BufferedReader(new StringReader(privateKey)); String line; while ((line = br.readLine()) != null) { - if (line.equals("-----BEGIN RSA PRIVATE KEY-----") || - line.equals("-----BEGIN OPENSSH PRIVATE KEY-----")) + if (line.equals("-----BEGIN RSA PRIVATE KEY-----") + || line.equals("-----BEGIN OPENSSH PRIVATE KEY-----")) { hasStart = true; - if (line.equals("-----END RSA PRIVATE KEY-----") || - line.equals("-----END OPENSSH PRIVATE KEY-----")) + } + if (line.equals("-----END RSA PRIVATE KEY-----") || line.equals("-----END OPENSSH 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(); + if (!hasStart) { + validations.add(FormValidation.error("This doesn't look like a private key at all")); + } + if (!hasEnd) { + validations.add(FormValidation.error( + "The private key is missing the trailing 'END RSA PRIVATE KEY' marker. Copy&paste error?")); + } + + if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { + if (!StringUtils.isEmpty(value)) { + validations.add(FormValidation.warning("Using private key file instead of selected credential")); + } else { + validations.add(FormValidation.ok("Using private key file")); + } + } + + try { + FIPS140Utils.ensurePrivateKeyInFipsMode(privateKey); + } catch (IllegalArgumentException ex) { + validations.add(FormValidation.error(ex, ex.getLocalizedMessage())); + } + + validations.add(FormValidation.ok("SSH key validation successful")); + return FormValidation.aggregate(validations); } /** * Tests the connection settings. - * + *

* Overriding needs to {@code @RequirePOST} - * @param ec2endpoint + * @param region * @param useInstanceProfileForCredentials * @param credentialsId * @param sshKeysCredentialsId * @param roleArn * @param roleSessionName - * @param region * @return the validation result * @throws IOException * @throws ServletException */ - @POST - protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL ec2endpoint, boolean useInstanceProfileForCredentials, String credentialsId, String sshKeysCredentialsId, String roleArn, String roleSessionName, String region) + @RequirePOST + public FormValidation doTestConnection( + @AncestorInPath ItemGroup context, + @QueryParameter String region, + @QueryParameter String altEC2Endpoint, + @QueryParameter boolean useInstanceProfileForCredentials, + @QueryParameter String credentialsId, + @QueryParameter String sshKeysCredentialsId, + @QueryParameter String roleArn, + @QueryParameter String roleSessionName) throws IOException, ServletException { if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { return FormValidation.ok(); } try { - SSHUserPrivateKey sshCredential = getSshCredential(sshKeysCredentialsId, context); + List validations = new ArrayList<>(); + + LOGGER.fine(() -> "begin doTestConnection()"); String privateKey = ""; - if (sshCredential != null) { - privateKey = sshCredential.getPrivateKey(); + if (System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { + LOGGER.fine(() -> "static credential is in use"); + SSHUserPrivateKey sshCredential = getSshCredential(sshKeysCredentialsId, context); + if (sshCredential != null) { + privateKey = sshCredential.getPrivateKey(); + } else { + return FormValidation.error( + "Failed to find credential \"" + sshKeysCredentialsId + "\" in store."); + } } else { - return FormValidation.error("Failed to find credential \"" + sshKeysCredentialsId + "\" in store."); + EC2PrivateKey k = EC2PrivateKey.fetchFromDisk(); + if (k == null) { + validations.add(FormValidation.error( + "Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH))); + if (!StringUtils.isEmpty(sshKeysCredentialsId)) { + validations.add(FormValidation.warning( + "Private key file path defined, selected credential will be ignored")); + } + return FormValidation.aggregate(validations); + } + privateKey = k.getPrivateKey(); + } + LOGGER.fine(() -> "private key found ok"); + + if (Util.fixEmpty(region) == null) { + region = DEFAULT_EC2_HOST; } - AWSCredentialsProvider credentialsProvider = createCredentialsProvider(useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); - AmazonEC2 ec2 = AmazonEC2Factory.getInstance().connect(credentialsProvider, ec2endpoint); + AwsCredentialsProvider credentialsProvider = createCredentialsProvider( + useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region); + Ec2Client ec2 = AmazonEC2Factory.getInstance() + .connect(credentialsProvider, parseRegion(region), parseEndpoint(altEC2Endpoint)); ec2.describeInstances(); - if (privateKey.trim().length() > 0) { + if (!privateKey.trim().isEmpty()) { // check if this key exists EC2PrivateKey pk = new EC2PrivateKey(privateKey); - if (pk.find(ec2) == null) - return FormValidation - .error("The EC2 key pair private key isn't registered to this EC2 region (fingerprint is " - + pk.getFingerprint() + ")"); + if (pk.find(ec2) == null) { + validations.add(FormValidation.error( + "The EC2 key pair private key isn't registered to this EC2 region (fingerprint is " + + pk.getFingerprint() + ")")); + } + } + + if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) { + if (!StringUtils.isEmpty(sshKeysCredentialsId)) { + validations.add( + FormValidation.warning("Using private key file instead of selected credential")); + } else { + validations.add(FormValidation.ok("Using private key file")); + } } - return FormValidation.ok(Messages.EC2Cloud_Success()); - } catch (AmazonClientException e) { + try { + FIPS140Utils.ensurePrivateKeyInFipsMode(privateKey); + } catch (IllegalArgumentException ex) { + validations.add(FormValidation.error(ex, ex.getLocalizedMessage())); + } + + validations.add(FormValidation.ok(Messages.EC2Cloud_Success())); + return FormValidation.aggregate(validations); + } catch (SdkException e) { LOGGER.log(Level.WARNING, "Failed to check EC2 credential", e); return FormValidation.error(e.getMessage()); } @@ -1223,7 +1737,66 @@ public ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup context) } return new StandardListBoxModel() .includeEmptyValue() - .includeMatchingAs(ACL.SYSTEM, context, AmazonWebServicesCredentials.class, Collections.emptyList(), CredentialsMatchers.always()); + .includeMatchingAs( + ACL.SYSTEM2, + context, + AmazonWebServicesCredentials.class, + Collections.emptyList(), + CredentialsMatchers.always()); + } + + @POST + public FormValidation doCheckCloudName(@QueryParameter String value) { + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + return FormValidation.ok(); + } + try { + Jenkins.checkGoodName(value); + } catch (Failure e) { + return FormValidation.error(e.getMessage()); + } + return FormValidation.ok(); + } + + @POST + public FormValidation doCheckAltEC2Endpoint(@QueryParameter String value) { + if (Util.fixEmpty(value) != null) { + try { + new URL(value); + } catch (MalformedURLException ignored) { + return FormValidation.error(Messages.EC2Cloud_MalformedUrl()); + } + } + return FormValidation.ok(); + } + + @RequirePOST + public ListBoxModel doFillRegionItems( + @QueryParameter String altEC2Endpoint, + @QueryParameter boolean useInstanceProfileForCredentials, + @QueryParameter String credentialsId) + throws IOException, ServletException { + ListBoxModel model = new ListBoxModel(); + if (Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + try { + AwsCredentialsProvider credentialsProvider = + createCredentialsProvider(useInstanceProfileForCredentials, credentialsId); + URI endpoint = parseEndpoint(altEC2Endpoint); + Ec2Client client = AmazonEC2Factory.getInstance() + .connect(credentialsProvider, getBootstrapRegion(endpoint), endpoint); + DescribeRegionsResponse regions = client.describeRegions(); + List regionList = regions.regions(); + for (software.amazon.awssdk.services.ec2.model.Region r : regionList) { + String name = r.regionName(); + Region rr = Region.of(name); + RegionMetadata regionMetadata = rr != null ? RegionMetadata.of(rr) : null; + model.add(regionMetadata != null ? regionMetadata.description() : name, name); + } + } catch (SdkClientException ex) { + // Ignore, as this may happen before the credentials are specified + } + } + return model; } } @@ -1234,8 +1807,9 @@ public static void log(Logger logger, Level level, TaskListener listener, String public static void log(Logger logger, Level level, TaskListener listener, String message, Throwable exception) { logger.log(level, message, exception); if (listener != null) { - if (exception != null) + if (exception != null) { message += " Exception: " + exception; + } LogRecord lr = new LogRecord(level, message); lr.setLoggerName(LOGGER.getName()); PrintStream printStream = listener.getLogger(); @@ -1255,17 +1829,21 @@ protected void doRun() throws IOException { Jenkins instance = Jenkins.get(); if (instance.clouds != null) { for (Cloud cloud : instance.clouds) { - if (cloud instanceof EC2Cloud) { - EC2Cloud ec2_cloud = (EC2Cloud) cloud; + if (cloud instanceof EC2Cloud ec2_cloud) { LOGGER.finer(() -> "Checking EC2 Connection on: " + ec2_cloud.getDisplayName()); try { - if(ec2_cloud.connection != null) { + if (ec2_cloud.connection != null) { List filters = new ArrayList<>(); - filters.add(new Filter("tag-key").withValues("bogus-EC2ConnectionKeepalive")); - DescribeInstancesRequest dir = new DescribeInstancesRequest().withFilters(filters); + filters.add(Filter.builder() + .name("tag-key") + .values("bogus-EC2ConnectionKeepalive") + .build()); + DescribeInstancesRequest dir = DescribeInstancesRequest.builder() + .filters(filters) + .build(); ec2_cloud.connection.describeInstances(dir); } - } catch (AmazonClientException e) { + } catch (SdkException e) { LOGGER.finer(() -> "Reconnecting to EC2 on: " + ec2_cloud.getDisplayName()); ec2_cloud.reconnectToEc2(); } diff --git a/src/main/java/hudson/plugins/ec2/EC2Computer.java b/src/main/java/hudson/plugins/ec2/EC2Computer.java index 489b2f938..2bc7abe9c 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Computer.java +++ b/src/main/java/hudson/plugins/ec2/EC2Computer.java @@ -23,22 +23,29 @@ */ package hudson.plugins.ec2; -import com.amazonaws.services.ec2.model.*; import edu.umd.cs.findbugs.annotations.CheckForNull; import hudson.Util; import hudson.model.Node; import hudson.slaves.SlaveComputer; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Base64; import java.util.Collections; import java.util.logging.Level; import java.util.logging.Logger; - - import org.kohsuke.stapler.HttpRedirect; import org.kohsuke.stapler.HttpResponse; -import com.amazonaws.AmazonClientException; -import com.amazonaws.services.ec2.AmazonEC2; import org.kohsuke.stapler.verb.POST; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.ec2.Ec2Client; +import software.amazon.awssdk.services.ec2.model.DescribeInstanceTypesRequest; +import software.amazon.awssdk.services.ec2.model.DescribeInstanceTypesResponse; +import software.amazon.awssdk.services.ec2.model.GetConsoleOutputRequest; +import software.amazon.awssdk.services.ec2.model.GetConsoleOutputResponse; +import software.amazon.awssdk.services.ec2.model.Instance; +import software.amazon.awssdk.services.ec2.model.InstanceTypeHypervisor; /** * @author Kohsuke Kawaguchi @@ -52,6 +59,16 @@ public class EC2Computer extends SlaveComputer { */ private volatile Instance ec2InstanceDescription; + /** + * Timestamp when {@link #ec2InstanceDescription} was last fetched. + * Used for TTL cache to avoid repeated EC2 API calls during SSH verification retries. + */ + private volatile long instanceCacheTimestamp; + + /** TTL in ms for instance/state cache. Configurable via system property. */ + private static final long INSTANCE_CACHE_TTL_MS = + Long.getLong(EC2Computer.class.getName() + ".instanceCacheTTLMs", 30_000); + private volatile Boolean isNitro; public EC2Computer(EC2AbstractSlave slave) { @@ -99,9 +116,9 @@ public SlaveTemplate getSlaveTemplate() { /** * Gets the EC2 console output. */ - public String getConsoleOutput() throws AmazonClientException { + public String getConsoleOutput() throws SdkException { try { - return getDecodedConsoleOutputResponse().getOutput(); + return getDecodedConsoleOutputResponse().output(); } catch (InterruptedException e) { return null; } @@ -111,44 +128,49 @@ public String getConsoleOutput() throws AmazonClientException { * Gets the EC2 decoded console output. * @since TODO */ - public String getDecodedConsoleOutput() throws AmazonClientException { + public String getDecodedConsoleOutput() throws SdkException { try { - return getDecodedConsoleOutputResponse().getDecodedOutput(); + String encodedOutput = getDecodedConsoleOutputResponse().output(); + byte[] decoded = Base64.getDecoder().decode(encodedOutput); + return new String(decoded, StandardCharsets.UTF_8); } catch (InterruptedException e) { return null; } } - private GetConsoleOutputResult getDecodedConsoleOutputResponse() throws AmazonClientException, InterruptedException { - AmazonEC2 ec2 = getCloud().connect(); - GetConsoleOutputRequest request = new GetConsoleOutputRequest(getInstanceId()); + private GetConsoleOutputResponse getDecodedConsoleOutputResponse() throws SdkException, InterruptedException { + Ec2Client ec2 = getCloud().connect(); + GetConsoleOutputRequest.Builder requestBuilder = + GetConsoleOutputRequest.builder().instanceId(getInstanceId()); if (checkIfNitro()) { - //Can only be used if instance has hypervisor Nitro - request.setLatest(true); + // Can only be used if instance has hypervisor Nitro + requestBuilder.latest(true); } - return ec2.getConsoleOutput(request); + return ec2.getConsoleOutput(requestBuilder.build()); } /** * Check if instance has hypervisor Nitro */ - private boolean checkIfNitro() throws AmazonClientException, InterruptedException { + private boolean checkIfNitro() throws SdkException, InterruptedException { try { if (isNitro == null) { - DescribeInstanceTypesRequest request = new DescribeInstanceTypesRequest(); - request.setInstanceTypes(Collections.singletonList(describeInstance().getInstanceType())); - AmazonEC2 ec2 = getCloud().connect(); - DescribeInstanceTypesResult result = ec2.describeInstanceTypes(request); - if (result.getInstanceTypes().size() == 1) { - String hypervisor = result.getInstanceTypes().get(0).getHypervisor(); - isNitro = hypervisor.equals("nitro"); + DescribeInstanceTypesRequest request = DescribeInstanceTypesRequest.builder() + .instanceTypes( + Collections.singletonList(describeInstance().instanceType())) + .build(); + Ec2Client ec2 = getCloud().connect(); + DescribeInstanceTypesResponse result = ec2.describeInstanceTypes(request); + if (result.instanceTypes().size() == 1) { + InstanceTypeHypervisor hypervisor = + result.instanceTypes().get(0).hypervisor(); + isNitro = hypervisor == InstanceTypeHypervisor.NITRO; } else { isNitro = false; } - } return isNitro; - } catch (AmazonClientException e) { + } catch (SdkException e) { LOGGER.log(Level.WARNING, "Could not describe-instance-types to check if instance is nitro based", e); isNitro = false; return isNitro; @@ -159,56 +181,61 @@ private boolean checkIfNitro() throws AmazonClientException, InterruptedExceptio * Obtains the instance state description in EC2. * *

- * This method returns a cached state, so it's not suitable to check {@link Instance#getState()} from the returned - * instance (but all the other fields are valid as it won't change.) - * + * This method returns a cached state (with TTL), so it's not suitable to check {@link Instance#state()} from the + * returned instance (but all the other fields are valid as it won't change.) + *

* The cache can be flushed using {@link #updateInstanceDescription()} */ - public Instance describeInstance() throws AmazonClientException, InterruptedException { - if (ec2InstanceDescription == null) - ec2InstanceDescription = CloudHelper.getInstanceWithRetry(getInstanceId(), getCloud()); + public Instance describeInstance() throws SdkException, InterruptedException { + long now = System.currentTimeMillis(); + if (ec2InstanceDescription != null && (now - instanceCacheTimestamp) < INSTANCE_CACHE_TTL_MS) { + return ec2InstanceDescription; + } + ec2InstanceDescription = CloudHelper.getInstanceWithRetry(getInstanceId(), getCloud()); + instanceCacheTimestamp = now; return ec2InstanceDescription; } /** * This will flush any cached description held by {@link #describeInstance()}. */ - public Instance updateInstanceDescription() throws AmazonClientException, InterruptedException { - return ec2InstanceDescription = CloudHelper.getInstanceWithRetry(getInstanceId(), getCloud()); + public Instance updateInstanceDescription() throws SdkException, InterruptedException { + ec2InstanceDescription = CloudHelper.getInstanceWithRetry(getInstanceId(), getCloud()); + instanceCacheTimestamp = System.currentTimeMillis(); + return ec2InstanceDescription; } /** * Gets the current state of the instance. * *

- * Unlike {@link #describeInstance()}, this method always return the current status by calling EC2. + * Uses a short TTL cache to avoid repeated EC2 API calls during SSH verification retries. */ - public InstanceState getState() throws AmazonClientException, InterruptedException { - ec2InstanceDescription = CloudHelper.getInstanceWithRetry(getInstanceId(), getCloud()); - return InstanceState.find(ec2InstanceDescription.getState().getName()); + public InstanceState getState() throws SdkException, InterruptedException { + return InstanceState.find(describeInstance().state().name().toString()); } /** * Number of milli-secs since the instance was started. */ - public long getUptime() throws AmazonClientException, InterruptedException { - return System.currentTimeMillis() - describeInstance().getLaunchTime().getTime(); + public long getUptime() throws SdkException, InterruptedException { + return describeInstance().launchTime().until(Instant.now(), ChronoUnit.MILLIS); } /** * Returns uptime in the human readable form. */ - public String getUptimeString() throws AmazonClientException, InterruptedException { + public String getUptimeString() throws SdkException, InterruptedException { return Util.getTimeSpanString(getUptime()); } /** - * Return the time this instance was launched in ms since the epoch. + * Return the Instant this instance was launched * - * @return Time this instance was launched, in ms since the epoch. + * @return Instant this instance was launched */ - public long getLaunchTime() throws InterruptedException { - return this.describeInstance().getLaunchTime().getTime(); + public Instant getLaunchTime() throws InterruptedException { + return this.describeInstance().launchTime(); } /** @@ -219,8 +246,9 @@ public long getLaunchTime() throws InterruptedException { public HttpResponse doDoDelete() throws IOException { checkPermission(DELETE); EC2AbstractSlave node = getNode(); - if (node != null) + if (node != null) { node.terminate(); + } return new HttpRedirect(".."); } @@ -261,5 +289,4 @@ public void onConnected() { node.onConnected(); } } - } diff --git a/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java b/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java index 4d95a7b01..23c3b43d8 100644 --- a/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java +++ b/src/main/java/hudson/plugins/ec2/EC2ComputerLauncher.java @@ -23,15 +23,22 @@ */ package hudson.plugins.ec2; +import static org.apache.sshd.client.session.ClientSession.REMOTE_COMMAND_WAIT_EVENTS; + import hudson.model.TaskListener; import hudson.slaves.ComputerLauncher; import hudson.slaves.SlaveComputer; - import java.io.IOException; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; - -import com.amazonaws.AmazonClientException; +import org.apache.sshd.client.channel.ClientChannel; +import org.apache.sshd.client.channel.ClientChannelEvent; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.scp.client.CloseableScpClient; +import org.apache.sshd.scp.client.ScpClient; +import org.apache.sshd.scp.client.ScpClientCreator; +import software.amazon.awssdk.core.exception.SdkException; /** * {@link ComputerLauncher} for EC2 that wraps the real user-specified {@link ComputerLauncher}. @@ -46,33 +53,50 @@ public void launch(SlaveComputer slaveComputer, TaskListener listener) { try { EC2Computer computer = (EC2Computer) slaveComputer; launchScript(computer, listener); - } catch (AmazonClientException | IOException e) { + } catch (SdkException | IOException e) { e.printStackTrace(listener.error(e.getMessage())); - if (slaveComputer.getNode() instanceof EC2AbstractSlave) { - LOGGER.log(Level.FINE, String.format("Terminating the ec2 agent %s due a problem launching or connecting to it", slaveComputer.getName()), e); - EC2AbstractSlave ec2AbstractSlave = (EC2AbstractSlave) slaveComputer.getNode(); - if (ec2AbstractSlave != null) { - ec2AbstractSlave.terminate(); - } + if (slaveComputer.getNode() instanceof EC2AbstractSlave ec2AbstractSlave) { + LOGGER.log( + Level.FINE, + String.format( + "Terminating the ec2 agent %s due a problem launching or connecting to it", + slaveComputer.getName()), + e); + ec2AbstractSlave.terminate(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); e.printStackTrace(listener.error(e.getMessage())); - if (slaveComputer.getNode() instanceof EC2AbstractSlave) { - LOGGER.log(Level.FINE, String.format("Terminating the ec2 agent %s due a problem launching or connecting to it", slaveComputer.getName()), e); - EC2AbstractSlave ec2AbstractSlave = (EC2AbstractSlave) slaveComputer.getNode(); - if (ec2AbstractSlave != null) { - ec2AbstractSlave.terminate(); - } + if (slaveComputer.getNode() instanceof EC2AbstractSlave ec2AbstractSlave) { + LOGGER.log( + Level.FINE, + String.format( + "Terminating the ec2 agent %s due a problem launching or connecting to it", + slaveComputer.getName()), + e); + ec2AbstractSlave.terminate(); } } - } /** * Stage 2 of the launch. Called after the EC2 instance comes up. */ protected abstract void launchScript(EC2Computer computer, TaskListener listener) - throws AmazonClientException, IOException, InterruptedException; + throws SdkException, IOException, InterruptedException; + protected int waitCompletion(ClientChannel clientChannel, long timeout) { + Set clientChannelEvents = clientChannel.waitFor(REMOTE_COMMAND_WAIT_EVENTS, timeout); + if (clientChannelEvents.contains(ClientChannelEvent.TIMEOUT)) { + return -1; + } else { + return clientChannel.getExitStatus(); + } + } + + protected CloseableScpClient createScpClient(ClientSession session) { + ScpClientCreator creator = ScpClientCreator.instance(); + ScpClient client = creator.createScpClient(session); + return CloseableScpClient.singleSessionInstance(client); + } } diff --git a/src/main/java/hudson/plugins/ec2/EC2ComputerListener.java b/src/main/java/hudson/plugins/ec2/EC2ComputerListener.java index db79e2380..9cd62873a 100644 --- a/src/main/java/hudson/plugins/ec2/EC2ComputerListener.java +++ b/src/main/java/hudson/plugins/ec2/EC2ComputerListener.java @@ -1,9 +1,10 @@ package hudson.plugins.ec2; import hudson.Extension; -import hudson.model.TaskListener; import hudson.model.Computer; +import hudson.model.TaskListener; import hudson.slaves.ComputerListener; +import jenkins.model.Jenkins; @Extension public class EC2ComputerListener extends ComputerListener { @@ -13,5 +14,9 @@ public void onOnline(Computer c, TaskListener listener) { if (c instanceof EC2Computer) { ((EC2Computer) c).onConnected(); } + Jenkins j = Jenkins.getInstanceOrNull(); + if (j != null) { + j.getQueue().scheduleMaintenance(); + } } } diff --git a/src/main/java/hudson/plugins/ec2/EC2Filter.java b/src/main/java/hudson/plugins/ec2/EC2Filter.java index b432d27b0..c7f82229a 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Filter.java +++ b/src/main/java/hudson/plugins/ec2/EC2Filter.java @@ -23,21 +23,18 @@ */ package hudson.plugins.ec2; -import hudson.model.Descriptor; -import hudson.model.AbstractDescribableImpl; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.Util; -import org.kohsuke.stapler.DataBoundConstructor; - +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; - -import edu.umd.cs.findbugs.annotations.CheckForNull; -import edu.umd.cs.findbugs.annotations.NonNull; - -import com.amazonaws.services.ec2.model.Filter; +import org.kohsuke.stapler.DataBoundConstructor; +import software.amazon.awssdk.services.ec2.model.Filter; public class EC2Filter extends AbstractDescribableImpl { @NonNull @@ -65,10 +62,12 @@ public String toString() { @Override public boolean equals(Object o) { - if (o == null) + if (o == null) { return false; - if (this.getClass() != o.getClass()) + } + if (this.getClass() != o.getClass()) { return false; + } EC2Filter other = (EC2Filter) o; return name.equals(other.name) && getValuesList().equals(other.getValuesList()); @@ -91,22 +90,19 @@ public String getValues() { @NonNull private List getValuesList() { - return Stream.of(Util.tokenize(values)) - .collect(Collectors.toList()); + return Stream.of(Util.tokenize(values)).collect(Collectors.toList()); } /* Helper method to convert EC2Filter to Filter */ @NonNull public Filter toFilter() { - return new Filter(name, getValuesList()); + return Filter.builder().name(name).values(getValuesList()).build(); } /* Helper method to convert list of EC2Filter to list of Filter */ @NonNull public static List toFilterList(@CheckForNull List filters) { - return Util.fixNull(filters).stream() - .map(EC2Filter::toFilter) - .collect(Collectors.toList()); + return Util.fixNull(filters).stream().map(EC2Filter::toFilter).collect(Collectors.toList()); } @Extension diff --git a/src/main/java/hudson/plugins/ec2/EC2HostAddressProvider.java b/src/main/java/hudson/plugins/ec2/EC2HostAddressProvider.java index 0be9d394c..7c0c4786d 100644 --- a/src/main/java/hudson/plugins/ec2/EC2HostAddressProvider.java +++ b/src/main/java/hudson/plugins/ec2/EC2HostAddressProvider.java @@ -1,11 +1,8 @@ package hudson.plugins.ec2; -import com.amazonaws.services.ec2.model.Instance; -import org.apache.commons.lang.StringUtils; - import java.util.Optional; - -import static hudson.plugins.ec2.ConnectionStrategy.*; +import org.apache.commons.lang.StringUtils; +import software.amazon.awssdk.services.ec2.model.Instance; public class EC2HostAddressProvider { public static String unix(Instance instance, ConnectionStrategy strategy) { @@ -34,34 +31,34 @@ public static String mac(Instance instance, ConnectionStrategy strategy) { case PRIVATE_IP: return getPrivateIpAddress(instance); default: - throw new IllegalArgumentException("Could not mac host address for strategy = " + strategy.toString()); + throw new IllegalArgumentException("Could not mac host address for strategy = " + strategy); } } public static String windows(Instance instance, ConnectionStrategy strategy) { - if (strategy.equals(PRIVATE_DNS) || strategy.equals(PRIVATE_IP)) { + if (strategy.equals(ConnectionStrategy.PRIVATE_DNS) || strategy.equals(ConnectionStrategy.PRIVATE_IP)) { return getPrivateIpAddress(instance); - } else if (strategy.equals(PUBLIC_DNS) || strategy.equals(PUBLIC_IP)) { + } else if (strategy.equals(ConnectionStrategy.PUBLIC_DNS) || strategy.equals(ConnectionStrategy.PUBLIC_IP)) { return getPublicIpAddress(instance); } else { - throw new IllegalArgumentException("Could not windows host address for strategy = " + strategy.toString()); + throw new IllegalArgumentException("Could not windows host address for strategy = " + strategy); } } private static String getPublicDnsName(Instance instance) { - return instance.getPublicDnsName(); + return instance.publicDnsName(); } private static String getPublicIpAddress(Instance instance) { - return instance.getPublicIpAddress(); + return instance.publicIpAddress(); } private static String getPrivateDnsName(Instance instance) { - return instance.getPrivateDnsName(); + return instance.privateDnsName(); } private static String getPrivateIpAddress(Instance instance) { - return instance.getPrivateIpAddress(); + return instance.privateIpAddress(); } private static Optional filterNonEmpty(String value) { diff --git a/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java b/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java index d648d8d10..1d8b51b57 100644 --- a/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java +++ b/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java @@ -1,30 +1,28 @@ package hudson.plugins.ec2; import hudson.Extension; -import hudson.model.Descriptor.FormException; import hudson.model.Computer; +import hudson.model.Descriptor.FormException; import hudson.model.Node; +import hudson.plugins.ec2.ssh.EC2MacLauncher; import hudson.plugins.ec2.ssh.EC2UnixLauncher; +import hudson.plugins.ec2.ssh.EC2WindowsSSHLauncher; import hudson.plugins.ec2.win.EC2WindowsLauncher; -import hudson.plugins.ec2.ssh.EC2MacLauncher; import hudson.slaves.NodeProperty; - import java.io.IOException; import java.util.Collections; import java.util.List; -import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; - import jenkins.model.Jenkins; import net.sf.json.JSONObject; - import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.StaplerRequest; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.model.*; +import org.kohsuke.stapler.StaplerRequest2; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.ec2.Ec2Client; +import software.amazon.awssdk.services.ec2.model.TerminateInstancesRequest; /** * Agent running on EC2. @@ -35,46 +33,440 @@ public class EC2OndemandSlave extends EC2AbstractSlave { private static final Logger LOGGER = Logger.getLogger(EC2OndemandSlave.class.getName()); @Deprecated - public EC2OndemandSlave(String instanceId, String templateDescription, String remoteFS, int numExecutors, String labelString, Mode mode, String initScript, String tmpDir, String remoteAdmin, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, String publicDNS, String privateDNS, List tags, String cloudName, int launchTimeout, AMITypeData amiType) + public EC2OndemandSlave( + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + String labelString, + Mode mode, + String initScript, + String tmpDir, + String remoteAdmin, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + String publicDNS, + String privateDNS, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType) + throws FormException, IOException { + this( + instanceId, + templateDescription, + remoteFS, + numExecutors, + labelString, + mode, + initScript, + tmpDir, + remoteAdmin, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + publicDNS, + privateDNS, + tags, + cloudName, + false, + launchTimeout, + amiType); + } + + @Deprecated + public EC2OndemandSlave( + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + String labelString, + Mode mode, + String initScript, + String tmpDir, + String remoteAdmin, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + String publicDNS, + String privateDNS, + List tags, + String cloudName, + boolean useDedicatedTenancy, + int launchTimeout, + AMITypeData amiType) throws FormException, IOException { - this(instanceId, templateDescription, remoteFS, numExecutors, labelString, mode, initScript, tmpDir, remoteAdmin, jvmopts, stopOnTerminate, idleTerminationMinutes, publicDNS, privateDNS, tags, cloudName, false, launchTimeout, amiType); + this( + instanceId, + templateDescription, + remoteFS, + numExecutors, + labelString, + mode, + initScript, + tmpDir, + remoteAdmin, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + publicDNS, + privateDNS, + tags, + cloudName, + false, + useDedicatedTenancy, + launchTimeout, + amiType); } @Deprecated - public EC2OndemandSlave(String instanceId, String templateDescription, String remoteFS, int numExecutors, String labelString, Mode mode, String initScript, String tmpDir, String remoteAdmin, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, String publicDNS, String privateDNS, List tags, String cloudName, boolean useDedicatedTenancy, int launchTimeout, AMITypeData amiType) + public EC2OndemandSlave( + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + String labelString, + Mode mode, + String initScript, + String tmpDir, + String remoteAdmin, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + String publicDNS, + String privateDNS, + List tags, + String cloudName, + boolean usePrivateDnsName, + boolean useDedicatedTenancy, + int launchTimeout, + AMITypeData amiType) throws FormException, IOException { - this(instanceId, templateDescription, remoteFS, numExecutors, labelString, mode, initScript, tmpDir, remoteAdmin, jvmopts, stopOnTerminate, idleTerminationMinutes, publicDNS, privateDNS, tags, cloudName, false, useDedicatedTenancy, launchTimeout, amiType); + this( + templateDescription + " (" + instanceId + ")", + instanceId, + templateDescription, + remoteFS, + numExecutors, + labelString, + mode, + initScript, + tmpDir, + Collections.emptyList(), + remoteAdmin, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + publicDNS, + privateDNS, + tags, + cloudName, + useDedicatedTenancy, + launchTimeout, + amiType, + ConnectionStrategy.backwardsCompatible(usePrivateDnsName, false, false), + -1); } @Deprecated - public EC2OndemandSlave(String instanceId, String templateDescription, String remoteFS, int numExecutors, String labelString, Mode mode, String initScript, String tmpDir, String remoteAdmin, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, String publicDNS, String privateDNS, List tags, String cloudName, boolean usePrivateDnsName, boolean useDedicatedTenancy, int launchTimeout, AMITypeData amiType) + public EC2OndemandSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + String labelString, + Mode mode, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + String publicDNS, + String privateDNS, + List tags, + String cloudName, + boolean useDedicatedTenancy, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses) throws FormException, IOException { - this(templateDescription + " (" + instanceId + ")", instanceId, templateDescription, remoteFS, numExecutors, labelString, mode, initScript, tmpDir, Collections.emptyList(), remoteAdmin, jvmopts, stopOnTerminate, idleTerminationMinutes, publicDNS, privateDNS, tags, cloudName, useDedicatedTenancy, launchTimeout, amiType, ConnectionStrategy.backwardsCompatible(usePrivateDnsName, false, false), -1); + this( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + labelString, + mode, + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + publicDNS, + privateDNS, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + Tenancy.backwardsCompatible(useDedicatedTenancy)); } @Deprecated - public EC2OndemandSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, String labelString, Mode mode, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, String publicDNS, String privateDNS, List tags, String cloudName, boolean useDedicatedTenancy, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses) + public EC2OndemandSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + String labelString, + Mode mode, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + String publicDNS, + String privateDNS, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + Tenancy tenancy) throws FormException, IOException { - this(name, instanceId, templateDescription, remoteFS, numExecutors, labelString, mode, initScript, tmpDir, nodeProperties, remoteAdmin, jvmopts, stopOnTerminate, idleTerminationMinutes, publicDNS, privateDNS, tags, cloudName, launchTimeout, amiType, connectionStrategy, maxTotalUses, Tenancy.backwardsCompatible(useDedicatedTenancy)); + this( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + labelString, + mode, + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + DEFAULT_JAVA_PATH, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + publicDNS, + privateDNS, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + tenancy, + DEFAULT_METADATA_ENDPOINT_ENABLED, + DEFAULT_METADATA_TOKENS_REQUIRED, + DEFAULT_METADATA_HOPS_LIMIT); } @Deprecated - public EC2OndemandSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, String labelString, Mode mode, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, String publicDNS, String privateDNS, List tags, String cloudName, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses, Tenancy tenancy) + public EC2OndemandSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + String labelString, + Mode mode, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String javaPath, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + String publicDNS, + String privateDNS, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + Tenancy tenancy, + Boolean metadataEndpointEnabled, + Boolean metadataTokensRequired, + Integer metadataHopsLimit) throws FormException, IOException { - this(name, instanceId, templateDescription, remoteFS, numExecutors, labelString, mode, initScript, tmpDir, nodeProperties, remoteAdmin, DEFAULT_JAVA_PATH, jvmopts, stopOnTerminate, idleTerminationMinutes, publicDNS, privateDNS, tags, cloudName, launchTimeout, amiType, connectionStrategy, maxTotalUses, tenancy, DEFAULT_METADATA_ENDPOINT_ENABLED, DEFAULT_METADATA_TOKENS_REQUIRED, DEFAULT_METADATA_HOPS_LIMIT); + this( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + labelString, + mode, + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + DEFAULT_JAVA_PATH, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + publicDNS, + privateDNS, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + tenancy, + metadataEndpointEnabled, + metadataTokensRequired, + metadataHopsLimit, + DEFAULT_METADATA_SUPPORTED); } @Deprecated - public EC2OndemandSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, String labelString, Mode mode, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String javaPath, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, String publicDNS, String privateDNS, List tags, String cloudName, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses, Tenancy tenancy, Boolean metadataEndpointEnabled, Boolean metadataTokensRequired, Integer metadataHopsLimit) + public EC2OndemandSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + String labelString, + Mode mode, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String javaPath, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + String publicDNS, + String privateDNS, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + Tenancy tenancy, + Boolean metadataEndpointEnabled, + Boolean metadataTokensRequired, + Integer metadataHopsLimit, + Boolean metadataSupported) throws FormException, IOException { - this(name, instanceId, templateDescription, remoteFS, numExecutors, labelString, mode, initScript, tmpDir, nodeProperties, remoteAdmin, DEFAULT_JAVA_PATH, jvmopts, stopOnTerminate, idleTerminationMinutes, publicDNS, privateDNS, tags, cloudName, launchTimeout, amiType, connectionStrategy, maxTotalUses, tenancy, metadataEndpointEnabled, metadataTokensRequired, metadataHopsLimit, DEFAULT_METADATA_SUPPORTED); + this( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + labelString, + mode, + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + DEFAULT_JAVA_PATH, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + publicDNS, + privateDNS, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + tenancy, + metadataEndpointEnabled, + metadataTokensRequired, + metadataHopsLimit, + metadataSupported, + DEFAULT_ENCLAVE_ENABLED); } @DataBoundConstructor - public EC2OndemandSlave(String name, String instanceId, String templateDescription, String remoteFS, int numExecutors, String labelString, Mode mode, String initScript, String tmpDir, List> nodeProperties, String remoteAdmin, String javaPath, String jvmopts, boolean stopOnTerminate, String idleTerminationMinutes, String publicDNS, String privateDNS, List tags, String cloudName, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses, Tenancy tenancy, Boolean metadataEndpointEnabled, Boolean metadataTokensRequired, Integer metadataHopsLimit, Boolean metadataSupported) + public EC2OndemandSlave( + String name, + String instanceId, + String templateDescription, + String remoteFS, + int numExecutors, + String labelString, + Mode mode, + String initScript, + String tmpDir, + List> nodeProperties, + String remoteAdmin, + String javaPath, + String jvmopts, + boolean stopOnTerminate, + String idleTerminationMinutes, + String publicDNS, + String privateDNS, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses, + Tenancy tenancy, + Boolean metadataEndpointEnabled, + Boolean metadataTokensRequired, + Integer metadataHopsLimit, + Boolean metadataSupported, + Boolean enclaveEnabled) throws FormException, IOException { - super(name, instanceId, templateDescription, remoteFS, numExecutors, mode, labelString, (amiType.isWindows() ? new EC2WindowsLauncher() : (amiType.isMac() ? new EC2MacLauncher(): - new EC2UnixLauncher())), new EC2RetentionStrategy(idleTerminationMinutes), initScript, tmpDir, nodeProperties, remoteAdmin, javaPath, jvmopts, stopOnTerminate, idleTerminationMinutes, tags, cloudName, launchTimeout, amiType, connectionStrategy, maxTotalUses, tenancy, metadataEndpointEnabled, metadataTokensRequired, metadataHopsLimit, metadataSupported); + super( + name, + instanceId, + templateDescription, + remoteFS, + numExecutors, + mode, + labelString, + (amiType.isWinRMAgent() + ? new EC2WindowsLauncher() + : (amiType.isWindows() + ? new EC2WindowsSSHLauncher() + : (amiType.isMac() ? new EC2MacLauncher() : new EC2UnixLauncher()))), + new EC2RetentionStrategy(idleTerminationMinutes), + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + javaPath, + jvmopts, + stopOnTerminate, + idleTerminationMinutes, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + tenancy, + metadataEndpointEnabled, + metadataTokensRequired, + metadataHopsLimit, + metadataSupported, + enclaveEnabled); this.publicDNS = publicDNS; this.privateDNS = privateDNS; } @@ -83,47 +475,75 @@ public EC2OndemandSlave(String name, String instanceId, String templateDescripti * Constructor for debugging. */ public EC2OndemandSlave(String instanceId) throws FormException, IOException { - this(instanceId, instanceId, "debug", "/tmp/hudson", 1, "debug", Mode.NORMAL, "", "/tmp", Collections.emptyList(), null, null, false, null, "Fake public", "Fake private", null, null, false, 0, new UnixData(null, null, null, null, null), ConnectionStrategy.PRIVATE_IP, -1); + this( + instanceId, + instanceId, + "debug", + "/tmp/hudson", + 1, + "debug", + Mode.NORMAL, + "", + "/tmp", + Collections.emptyList(), + null, + null, + false, + null, + "Fake public", + "Fake private", + null, + null, + false, + 0, + new UnixData(null, null, null, null, null), + ConnectionStrategy.PRIVATE_IP, + -1); } /** * Terminates the instance in EC2. */ - public void terminate() { + @Override + public Future terminate() { if (terminateScheduled.getCount() == 0) { - synchronized(terminateScheduled) { + synchronized (terminateScheduled) { if (terminateScheduled.getCount() == 0) { - Computer.threadPoolForRemoting.submit(() -> { + Future f = Computer.threadPoolForRemoting.submit(() -> { try { if (!isAlive(true)) { /* - * The node has been killed externally, so we've nothing to do here - */ + * The node has been killed externally, so we've nothing to do here + */ LOGGER.info("EC2 instance already terminated: " + getInstanceId()); } else { - AmazonEC2 ec2 = getCloud().connect(); - TerminateInstancesRequest request = new TerminateInstancesRequest(Collections.singletonList(getInstanceId())); + Ec2Client ec2 = getCloud().connect(); + TerminateInstancesRequest request = TerminateInstancesRequest.builder() + .instanceIds(Collections.singletonList(getInstanceId())) + .build(); ec2.terminateInstances(request); LOGGER.info("Terminated EC2 instance (terminated): " + getInstanceId()); } Jenkins.get().removeNode(this); LOGGER.info("Removed EC2 instance from jenkins controller: " + getInstanceId()); - } catch (AmazonClientException | IOException e) { + } catch (SdkException | IOException e) { LOGGER.log(Level.WARNING, "Failed to terminate EC2 instance: " + getInstanceId(), e); } finally { - synchronized(terminateScheduled) { + synchronized (terminateScheduled) { terminateScheduled.countDown(); } } }); terminateScheduled.reset(); + return f; } } } + return CompletableFuture.completedFuture(null); } @Override - public Node reconfigure(final StaplerRequest req, JSONObject form) throws FormException { + public Node reconfigure(final StaplerRequest2 req, JSONObject form) throws FormException { if (form == null) { return null; } @@ -133,8 +553,10 @@ public Node reconfigure(final StaplerRequest req, JSONObject form) throws FormEx try { Jenkins.get().removeNode(this); } catch (IOException ioe) { - LOGGER.log(Level.WARNING, "Attempt to reconfigure EC2 instance which has been externally terminated: " - + getInstanceId(), ioe); + LOGGER.log( + Level.WARNING, + "Attempt to reconfigure EC2 instance which has been externally terminated: " + getInstanceId(), + ioe); } return null; diff --git a/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java b/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java index a15728cac..5d52b7ee4 100644 --- a/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java +++ b/src/main/java/hudson/plugins/ec2/EC2PrivateKey.java @@ -23,34 +23,39 @@ */ package hudson.plugins.ec2; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.plugins.ec2.util.KeyPair; +import hudson.util.Secret; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; import java.security.UnrecoverableKeyException; import java.util.Base64; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.model.KeyPairInfo; - -import hudson.util.Secret; -import jenkins.bouncycastle.api.PEMEncodable; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.crypto.Cipher; -import java.nio.charset.Charset; - +import jenkins.bouncycastle.api.PEMEncodable; import org.apache.commons.lang.StringUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.ec2.Ec2Client; +import software.amazon.awssdk.services.ec2.model.KeyPairInfo; /** * RSA private key (the one that you generate with ec2-add-keypair.) - * + *

* Starts with "----- BEGIN RSA PRIVATE KEY------\n". * * @author Kohsuke Kawaguchi */ public class EC2PrivateKey { + private static final Logger LOGGER = Logger.getLogger(EC2PrivateKey.class.getName()); + private final Secret privateKey; EC2PrivateKey(String privateKey) { @@ -100,8 +105,9 @@ public boolean isPrivateKey() throws IOException { BufferedReader br = new BufferedReader(new StringReader(privateKey.getPlainText())); String line; while ((line = br.readLine()) != null) { - if (line.equals("-----BEGIN RSA PRIVATE KEY-----")) + if (line.equals("-----BEGIN RSA PRIVATE KEY-----")) { return true; + } } return false; } @@ -109,40 +115,63 @@ public boolean isPrivateKey() throws IOException { /** * Finds the {@link KeyPairInfo} that corresponds to this key in EC2. */ - public com.amazonaws.services.ec2.model.KeyPair find(AmazonEC2 ec2) throws IOException, AmazonClientException { + public KeyPair find(Ec2Client ec2) throws IOException, SdkException { String fp = getFingerprint(); String pfp = getPublicFingerprint(); - for (KeyPairInfo kp : ec2.describeKeyPairs().getKeyPairs()) { - if (kp.getKeyFingerprint().equalsIgnoreCase(fp)) { - com.amazonaws.services.ec2.model.KeyPair keyPair = new com.amazonaws.services.ec2.model.KeyPair(); - keyPair.setKeyName(kp.getKeyName()); - keyPair.setKeyFingerprint(fp); - keyPair.setKeyMaterial(Secret.toString(privateKey)); - return keyPair; + for (KeyPairInfo kp : ec2.describeKeyPairs().keyPairs()) { + if (kp.keyFingerprint().equalsIgnoreCase(fp)) { + return new KeyPair( + KeyPairInfo.builder() + .keyName(kp.keyName()) + .keyFingerprint(fp) + .build(), + Secret.toString(privateKey)); } - if (kp.getKeyFingerprint().equalsIgnoreCase(pfp)) { - com.amazonaws.services.ec2.model.KeyPair keyPair = new com.amazonaws.services.ec2.model.KeyPair(); - keyPair.setKeyName(kp.getKeyName()); - keyPair.setKeyFingerprint(pfp); - keyPair.setKeyMaterial(Secret.toString(privateKey)); - return keyPair; + if (kp.keyFingerprint().equalsIgnoreCase(pfp)) { + return new KeyPair( + KeyPairInfo.builder() + .keyName(kp.keyName()) + .keyFingerprint(pfp) + .build(), + Secret.toString(privateKey)); } } return null; } - public String decryptWindowsPassword(String encodedPassword) throws AmazonClientException { + public String decryptWindowsPassword(String encodedPassword) throws SdkException { try { Cipher cipher = Cipher.getInstance("RSA/NONE/PKCS1Padding"); - cipher.init(Cipher.DECRYPT_MODE, PEMEncodable.decode(privateKey.getPlainText()).toPrivateKey()); + cipher.init( + Cipher.DECRYPT_MODE, + PEMEncodable.decode(privateKey.getPlainText()).toPrivateKey()); byte[] cipherText = Base64.getDecoder().decode(StringUtils.deleteWhitespace(encodedPassword)); byte[] plainText = cipher.doFinal(cipherText); - return new String(plainText, Charset.forName("ASCII")); + return new String(plainText, StandardCharsets.US_ASCII); } catch (Exception e) { - throw new AmazonClientException("Unable to decode password:\n" + e.toString()); + throw SdkException.create("Unable to decode password", e); } } + /* visible for testing */ + @CheckForNull + public static EC2PrivateKey fetchFromDisk() { + return fetchFromDisk(System.getProperty(EC2Cloud.SSH_PRIVATE_KEY_FILEPATH, "")); + } + + @CheckForNull + public static EC2PrivateKey fetchFromDisk(String filepath) { + if (StringUtils.isNotEmpty(filepath)) { + try { + return new EC2PrivateKey(Files.readString(Paths.get(filepath), StandardCharsets.UTF_8)); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "unable to read private key from file " + filepath, e); + return null; + } + } + return null; + } + @Override public int hashCode() { return privateKey.hashCode(); @@ -150,8 +179,9 @@ public int hashCode() { @Override public boolean equals(Object that) { - if (that != null && this.getClass() == that.getClass()) + if (that != null && this.getClass() == that.getClass()) { return this.privateKey.equals(((EC2PrivateKey) that).privateKey); + } return false; } diff --git a/src/main/java/hudson/plugins/ec2/EC2Readiness.java b/src/main/java/hudson/plugins/ec2/EC2Readiness.java index 3f3f112e3..c65dd7afe 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Readiness.java +++ b/src/main/java/hudson/plugins/ec2/EC2Readiness.java @@ -1,8 +1,9 @@ package hudson.plugins.ec2; -import com.amazonaws.AmazonClientException; +import software.amazon.awssdk.core.exception.SdkException; public interface EC2Readiness { - public boolean isReady(); - public String getEc2ReadinessStatus() throws AmazonClientException; + boolean isReady(); + + String getEc2ReadinessStatus() throws SdkException; } diff --git a/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java b/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java index 356e7bb47..a03956fbd 100644 --- a/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java +++ b/src/main/java/hudson/plugins/ec2/EC2RetentionStrategy.java @@ -23,27 +23,26 @@ */ package hudson.plugins.ec2; -import com.amazonaws.AmazonClientException; - - import hudson.init.InitMilestone; import hudson.model.Descriptor; import hudson.model.Executor; import hudson.model.ExecutorListener; +import hudson.model.Label; import hudson.model.Queue; import hudson.plugins.ec2.util.MinimumInstanceChecker; -import hudson.model.Label; import hudson.slaves.RetentionStrategy; -import jenkins.model.Jenkins; - import java.time.Clock; +import java.time.Instant; import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; - +import jenkins.model.Jenkins; import org.kohsuke.stapler.DataBoundConstructor; +import software.amazon.awssdk.core.exception.SdkException; /** * {@link RetentionStrategy} for EC2. @@ -53,9 +52,20 @@ public class EC2RetentionStrategy extends RetentionStrategy implements ExecutorListener { private static final Logger LOGGER = Logger.getLogger(EC2RetentionStrategy.class.getName()); + /** + * Executor for heavy retention work (EC2 API calls, idle timeout, reconnect). + * Runs outside the Queue lock so the Queue can complete its periodic routine in under a second. + * Package-private and non-final so tests can replace with a direct (same-thread) executor. + */ + static ExecutorService HEAVY_WORK_EXECUTOR = Executors.newCachedThreadPool(r -> { + Thread t = new Thread(r, "EC2RetentionStrategy-heavy"); + t.setDaemon(true); + return t; + }); + public static final boolean DISABLED = Boolean.getBoolean(EC2RetentionStrategy.class.getName() + ".disabled"); - private long nextCheckAfter = -1; + private volatile long nextCheckAfter = -1; private transient Clock clock; /** @@ -87,7 +97,6 @@ public EC2RetentionStrategy(String idleTerminationMinutes) { } } - EC2RetentionStrategy(String idleTerminationMinutes, Clock clock, long nextCheckAfter) { this(idleTerminationMinutes); this.clock = clock; @@ -98,44 +107,66 @@ long getNextCheckAfter() { return this.nextCheckAfter; } + /** + * Lightweight path: returns immediately so the Queue lock is not held during EC2 API calls. + * Heavy work (getState, getUptime, idle timeout, reconnect) is scheduled to run asynchronously + * outside the Queue lock. See docs/EC2_QUEUE_AUDIT.md. + */ @Override public long check(EC2Computer c) { if (!checkLock.tryLock()) { return CHECK_INTERVAL_MINUTES; - } else { - try { - long currentTime = this.clock.millis(); - - if (currentTime > nextCheckAfter) { - long intervalMins = internalCheck(c); - nextCheckAfter = currentTime + TimeUnit.MINUTES.toMillis(intervalMins); - return intervalMins; - } else { - return CHECK_INTERVAL_MINUTES; - } - } finally { - checkLock.unlock(); + } + try { + long currentTime = this.clock.millis(); + if (currentTime <= nextCheckAfter) { + return CHECK_INTERVAL_MINUTES; } + // Schedule heavy work to run outside the Queue lock; return immediately. + nextCheckAfter = currentTime + TimeUnit.MINUTES.toMillis(CHECK_INTERVAL_MINUTES); + final EC2Computer computer = c; + HEAVY_WORK_EXECUTOR.execute(() -> runHeavyCheck(computer)); + return CHECK_INTERVAL_MINUTES; + } finally { + checkLock.unlock(); + } + } + + /** + * Runs outside the Queue lock. Performs EC2 API calls and retention logic. + * State-changing operations (disconnect, terminate) use Queue.withLock for brief sections. + */ + private void runHeavyCheck(EC2Computer computer) { + if (!checkLock.tryLock()) { + return; + } + try { + attemptReconnectIfOffline(computer); + internalCheck(computer); + } finally { + checkLock.unlock(); } } private long internalCheck(EC2Computer computer) { /* - * If we've been told never to terminate, or node is null(deleted), no checks to perform - */ + * If we've been told never to terminate, or node is null(deleted), no checks to perform + */ if (idleTerminationMinutes == 0 || computer.getNode() == null) { return CHECK_INTERVAL_MINUTES; } /* - * If we have equal or less number of agents than the template's minimum instance count, don't perform check. - */ + * If we have equal or less number of agents than the template's minimum instance count, don't perform check. + */ SlaveTemplate slaveTemplate = computer.getSlaveTemplate(); if (slaveTemplate != null) { long numberOfCurrentInstancesForTemplate = MinimumInstanceChecker.countCurrentNumberOfAgents(slaveTemplate); - if (numberOfCurrentInstancesForTemplate > 0 && numberOfCurrentInstancesForTemplate <= slaveTemplate.getMinimumNumberOfInstances()) { - //Check if we're in an active time-range for keeping minimum number of instances - if (MinimumInstanceChecker.minimumInstancesActive(slaveTemplate.getMinimumNumberOfInstancesTimeRangeConfig())) { + if (numberOfCurrentInstancesForTemplate > 0 + && numberOfCurrentInstancesForTemplate <= slaveTemplate.getMinimumNumberOfInstances()) { + // Check if we're in an active time-range for keeping minimum number of instances + if (MinimumInstanceChecker.minimumInstancesActive( + slaveTemplate.getMinimumNumberOfInstancesTimeRangeConfig())) { return CHECK_INTERVAL_MINUTES; } } @@ -143,39 +174,50 @@ private long internalCheck(EC2Computer computer) { if (computer.isIdle() && !DISABLED) { final long uptime; - final long launchedAtMs; + final Instant launchedAt; InstanceState state; try { - state = computer.getState(); //Get State before Uptime because getState will refresh the cached EC2 info + state = computer.getState(); // Get State before Uptime because getState will refresh the cached EC2 + // info uptime = computer.getUptime(); - launchedAtMs = computer.getLaunchTime(); - } catch (AmazonClientException | InterruptedException e) { + launchedAt = computer.getLaunchTime(); + } catch (SdkException | InterruptedException e) { // We'll just retry next time we test for idleness. LOGGER.fine("Exception while checking host uptime for " + computer.getName() + ", will retry next check. Exception: " + e); return CHECK_INTERVAL_MINUTES; } - //Don't bother checking anything else if the instance is already in the desired state: + // Don't bother checking anything else if the instance is already in the desired state: // * Already Terminated // * We use stop-on-terminate and the instance is currently stopped or stopping if (InstanceState.TERMINATED.equals(state) - || (slaveTemplate != null && slaveTemplate.stopOnTerminate) && (InstanceState.STOPPED.equals(state) || InstanceState.STOPPING.equals(state))) { + || (slaveTemplate != null && slaveTemplate.stopOnTerminate) + && (InstanceState.STOPPED.equals(state) || InstanceState.STOPPING.equals(state))) { if (computer.isOnline()) { - LOGGER.info("External Stop of " + computer.getName() + " detected - disconnecting. instance status" + state.toString()); - computer.disconnect(null); + LOGGER.info("External Stop of " + computer.getName() + " detected - disconnecting. instance status" + + state); + try { + Queue.withLock(() -> computer.disconnect(null)); + } catch (Exception e) { + LOGGER.log(Level.FINE, "Error disconnecting " + computer.getName(), e); + } } return CHECK_INTERVAL_MINUTES; } - //on rare occasions, AWS may return fault instance which shows running in AWS console but can not be connected. - //need terminate such fault instance. + // on rare occasions, AWS may return fault instance which shows running in AWS console but can not be + // connected. + // need terminate such fault instance. // An instance may also fail running user data scripts and // need to be cleaned up. - if (computer.isOffline()){ + if (computer.isOffline()) { if (computer.isConnecting()) { - LOGGER.log(Level.FINE, "Computer {0} connecting and still offline, will check if the launch timeout has expired", computer.getInstanceId()); + LOGGER.log( + Level.FINE, + "Computer {0} connecting and still offline, will check if the launch timeout has expired", + computer.getInstanceId()); EC2AbstractSlave node = computer.getNode(); if (Objects.isNull(node)) { @@ -185,51 +227,82 @@ private long internalCheck(EC2Computer computer) { if (launchTimeout > 0 && uptime > launchTimeout) { // Computer is offline and startup time has expired LOGGER.info("Startup timeout of " + computer.getName() + " after " - + uptime + - " milliseconds (timeout: " + launchTimeout + " milliseconds), instance status: " + state.toString()); - node.launchTimeout(); + + uptime + " milliseconds (timeout: " + + launchTimeout + " milliseconds), instance status: " + state.toString()); + try { + Queue.withLock(node::launchTimeout); + } catch (Exception e) { + LOGGER.log(Level.FINE, "Error launching timeout for " + computer.getName(), e); + } } return CHECK_INTERVAL_MINUTES; } else { - LOGGER.log(Level.FINE, "Computer {0} offline but not connecting, will check if it should be terminated because of the idle time configured", computer.getInstanceId()); + LOGGER.log( + Level.FINE, + "Computer {0} offline but not connecting, will check if it should be terminated because of the idle time configured", + computer.getInstanceId()); } } - final long idleMilliseconds = this.clock.millis() - Math.max(computer.getIdleStartMilliseconds(), launchedAtMs); - + final long idleMilliseconds = + this.clock.millis() - Math.max(computer.getIdleStartMilliseconds(), launchedAt.toEpochMilli()); if (idleTerminationMinutes > 0) { // TODO: really think about the right strategy here, see // JENKINS-23792 - if (idleMilliseconds > TimeUnit.MINUTES.toMillis(idleTerminationMinutes) && - !itemsInQueueForThisSlave(computer)){ + boolean queueHasItemsForSlave; + try { + queueHasItemsForSlave = Queue.withLock(() -> itemsInQueueForThisSlave(computer)); + } catch (Exception e) { + LOGGER.log(Level.FINE, "Error checking queue for " + computer.getName(), e); + queueHasItemsForSlave = true; // safe default: do not terminate + } + if (idleMilliseconds > TimeUnit.MINUTES.toMillis(idleTerminationMinutes) && !queueHasItemsForSlave) { LOGGER.info("Idle timeout of " + computer.getName() + " after " - + TimeUnit.MILLISECONDS.toMinutes(idleMilliseconds) + - " idle minutes, instance status"+state.toString()); + + TimeUnit.MILLISECONDS.toMinutes(idleMilliseconds) + " idle minutes, instance status" + + state.toString()); EC2AbstractSlave slaveNode = computer.getNode(); if (slaveNode != null) { - slaveNode.idleTimeout(); + try { + Queue.withLock(slaveNode::idleTimeout); + } catch (Exception e) { + LOGGER.log(Level.FINE, "Error idle timeout for " + computer.getName(), e); + } } } } else { final int oneHourSeconds = (int) TimeUnit.SECONDS.convert(1, TimeUnit.HOURS); - // AWS bills by the hour for EC2 Instances, so calculate the remaining seconds left in the "billing hour" - // Note: Since October 2017, this isn't true for Linux instances, but the logic hasn't yet been updated for this + // AWS bills by the hour for EC2 Instances, so calculate the remaining seconds left in the "billing + // hour" + // Note: Since October 2017, this isn't true for Linux instances, but the logic hasn't yet been updated + // for this final int freeSecondsLeft = oneHourSeconds - (int) (TimeUnit.SECONDS.convert(uptime, TimeUnit.MILLISECONDS) % oneHourSeconds); // if we have less "free" (aka already paid for) time left than // our idle time, stop/terminate the instance // See JENKINS-23821 - if (freeSecondsLeft <= TimeUnit.MINUTES.toSeconds(Math.abs(idleTerminationMinutes)) && !itemsInQueueForThisSlave(computer)) { + boolean queueHasItemsForSlaveBilling; + try { + queueHasItemsForSlaveBilling = Queue.withLock(() -> itemsInQueueForThisSlave(computer)); + } catch (Exception e) { + LOGGER.log(Level.FINE, "Error checking queue for " + computer.getName(), e); + queueHasItemsForSlaveBilling = true; + } + if (freeSecondsLeft <= TimeUnit.MINUTES.toSeconds(Math.abs(idleTerminationMinutes)) + && !queueHasItemsForSlaveBilling) { LOGGER.info("Idle timeout of " + computer.getName() + " after " + TimeUnit.MILLISECONDS.toMinutes(idleMilliseconds) + " idle minutes, with " + TimeUnit.SECONDS.toMinutes(freeSecondsLeft) + " minutes remaining in billing period"); EC2AbstractSlave slaveNode = computer.getNode(); if (slaveNode != null) { - slaveNode.idleTimeout(); + try { + Queue.withLock(slaveNode::idleTimeout); + } catch (Exception e) { + LOGGER.log(Level.FINE, "Error idle timeout for " + computer.getName(), e); + } } } } @@ -237,6 +310,30 @@ private long internalCheck(EC2Computer computer) { return CHECK_INTERVAL_MINUTES; } + /** + * Try to reconnect the EC2 Instance if it's offline but the status is running. + * This could mean unstable ssh connection, so instead of failing the build, + * we try to reconnect as soon as the EC2 Instance is running again. + */ + private void attemptReconnectIfOffline(EC2Computer computer) { + try { + if (computer.getState() == InstanceState.RUNNING && computer.isOffline()) { + LOGGER.warning("EC2Computer " + computer.getName() + " is offline"); + if (!computer.isConnecting()) { + // Keep retrying connection to agent until the job times out + LOGGER.warning("Attempting to reconnect EC2Computer " + computer.getName()); + try { + computer.connect(false); + } catch (Exception e) { + LOGGER.log(Level.FINE, "Error reconnecting " + computer.getName(), e); + } + } + } + } catch (SdkException | InterruptedException e) { + LOGGER.log(Level.FINE, "Error getting EC2 instance state for " + computer.getName(), e); + } + } + /* * Checks if there are any items in the queue that are waiting for this node explicitly. * This prevents a node from being taken offline while there are Ivy/Maven Modules waiting to build. @@ -250,11 +347,12 @@ private boolean itemsInQueueForThisSlave(EC2Computer c) { * doesn't have a node it will return null. In this case we want to * return false because there's no slave to prevent a timeout of. */ - if (selfNode == null) return false; + if (selfNode == null) { + return false; + } final Label selfLabel = selfNode.getSelfLabel(); - Queue.Item[] items = Jenkins.getInstance().getQueue().getItems(); - for (int i = 0; i < items.length; i++) { - Queue.Item item = items[i]; + Queue.Item[] items = Jenkins.get().getQueue().getItems(); + for (Queue.Item item : items) { final Label assignedLabel = item.getAssignedLabel(); if (assignedLabel == selfLabel) { LOGGER.fine("Preventing idle timeout of " + c.getName() @@ -266,32 +364,33 @@ private boolean itemsInQueueForThisSlave(EC2Computer c) { } /** - * Called when a new {@link EC2Computer} object is introduced (such as when Hudson started, or when - * a new agent is added.) - * - * When Jenkins has just started, we don't want to spin up all the instances, so we only start if - * the EC2 instance is already running + * Lightweight path: returns immediately. Heavy work (getState, connect) runs asynchronously + * so the caller is not blocked by EC2 API calls. */ @Override public void start(EC2Computer c) { - //Jenkins is in the process of starting up - if (Jenkins.get().getInitLevel() != InitMilestone.COMPLETED) { - InstanceState state = null; + final EC2Computer computer = c; + HEAVY_WORK_EXECUTOR.execute(() -> { + if (Jenkins.get().getInitLevel() != InitMilestone.COMPLETED) { + InstanceState state = null; + try { + state = computer.getState(); + } catch (SdkException | InterruptedException e) { + LOGGER.log(Level.FINE, "Error getting EC2 instance state for " + computer.getName(), e); + } + if (!(InstanceState.PENDING.equals(state) || InstanceState.RUNNING.equals(state))) { + LOGGER.info("Ignoring start request for " + computer.getName() + + " during Jenkins startup due to EC2 instance state of " + state); + return; + } + } + LOGGER.info("Start requested for " + computer.getName()); try { - state = c.getState(); - } catch (AmazonClientException | InterruptedException e) { - LOGGER.log(Level.FINE, "Error getting EC2 instance state for " + c.getName(), e); + computer.connect(false); + } catch (Exception e) { + LOGGER.log(Level.FINE, "Error connecting " + computer.getName(), e); } - if (!(InstanceState.PENDING.equals(state) || InstanceState.RUNNING.equals(state))) { - LOGGER.info("Ignoring start request for " + c.getName() - + " during Jenkins startup due to EC2 instance state of " + state); - return; - } - } - - - LOGGER.info("Start requested for " + c.getName()); - c.connect(false); + }); } // no registration since this retention strategy is used only for EC2 nodes @@ -310,6 +409,7 @@ protected Object readResolve() { return this; } + @Override public void taskAccepted(Executor executor, Queue.Task task) { EC2Computer computer = (EC2Computer) executor.getOwner(); if (computer != null) { @@ -317,11 +417,12 @@ public void taskAccepted(Executor executor, Queue.Task task) { if (slaveNode != null) { int maxTotalUses = slaveNode.maxTotalUses; if (maxTotalUses <= -1) { - LOGGER.fine("maxTotalUses set to unlimited (" + slaveNode.maxTotalUses + ") for agent " + slaveNode.instanceId); - return; + LOGGER.fine("maxTotalUses set to unlimited (" + slaveNode.maxTotalUses + ") for agent " + + slaveNode.instanceId); } else if (maxTotalUses <= 1) { LOGGER.info("maxTotalUses drained - suspending agent " + slaveNode.instanceId); computer.setAcceptingTasks(false); + MinimumInstanceChecker.scheduleCheck(); } else { slaveNode.maxTotalUses = slaveNode.maxTotalUses - 1; LOGGER.info("Agent " + slaveNode.instanceId + " has " + slaveNode.maxTotalUses + " builds left"); @@ -330,10 +431,12 @@ public void taskAccepted(Executor executor, Queue.Task task) { } } + @Override public void taskCompleted(Executor executor, Queue.Task task, long durationMS) { postJobAction(executor); } + @Override public void taskCompletedWithProblems(Executor executor, Queue.Task task, long durationMS, Throwable problems) { postJobAction(executor); } @@ -343,13 +446,16 @@ private void postJobAction(Executor executor) { if (computer != null) { EC2AbstractSlave slaveNode = computer.getNode(); if (slaveNode != null) { - // At this point, if agent is in suspended state and has 1 last executer running, it is safe to terminate. + // At this point, if agent is in suspended state and has 1 last executer running, it is safe to + // terminate. if (computer.countBusy() <= 1 && !computer.isAcceptingTasks()) { - LOGGER.info("Agent " + slaveNode.instanceId + " is terminated due to maxTotalUses (" + slaveNode.maxTotalUses + ")"); + LOGGER.info("Agent " + slaveNode.instanceId + " is terminated due to maxTotalUses (" + + slaveNode.maxTotalUses + ")"); slaveNode.terminate(); } else { if (slaveNode.maxTotalUses == 1) { - LOGGER.info("Agent " + slaveNode.instanceId + " is still in use by more than one (" + computer.countBusy() + ") executers."); + LOGGER.info("Agent " + slaveNode.instanceId + " is still in use by more than one (" + + computer.countBusy() + ") executers."); } } } diff --git a/src/main/java/hudson/plugins/ec2/EC2SlaveMonitor.java b/src/main/java/hudson/plugins/ec2/EC2SlaveMonitor.java index daa622a39..3c8ba83a3 100644 --- a/src/main/java/hudson/plugins/ec2/EC2SlaveMonitor.java +++ b/src/main/java/hudson/plugins/ec2/EC2SlaveMonitor.java @@ -2,21 +2,23 @@ import hudson.Extension; import hudson.model.AsyncPeriodicWork; -import hudson.model.TaskListener; import hudson.model.Node; - +import hudson.model.TaskListener; +import hudson.plugins.ec2.util.MinimumInstanceChecker; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; - -import hudson.plugins.ec2.util.MinimumInstanceChecker; import jenkins.model.Jenkins; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.services.ec2.model.AmazonEC2Exception; - -import static hudson.plugins.ec2.EC2Cloud.EC2_REQUEST_EXPIRED_ERROR_CODE; +import org.apache.commons.lang.StringUtils; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.ec2.model.Ec2Exception; +import software.amazon.awssdk.services.ec2.model.Instance; +import software.amazon.awssdk.services.ec2.model.InstanceStateName; /** * @author Bruno Meneguello @@ -41,25 +43,82 @@ public long getRecurrencePeriod() { @Override protected void execute(TaskListener listener) throws IOException, InterruptedException { removeDeadNodes(); - MinimumInstanceChecker.checkForMinimumInstances(); + MinimumInstanceChecker.scheduleCheck(); } private void removeDeadNodes() { + Map> byCloud = new HashMap<>(); for (Node node : Jenkins.get().getNodes()) { - if (node instanceof EC2AbstractSlave) { - final EC2AbstractSlave ec2Slave = (EC2AbstractSlave) node; - try { - if (!ec2Slave.isAlive(true)) { - LOGGER.info("EC2 instance is dead: " + ec2Slave.getInstanceId()); - ec2Slave.terminate(); + if (node instanceof EC2AbstractSlave ec2Slave) { + String instanceId = ec2Slave.getInstanceId(); + if (StringUtils.isEmpty(instanceId)) { + continue; + } + EC2Cloud cloud = ec2Slave.getCloud(); + if (cloud == null) { + continue; + } + byCloud.computeIfAbsent(cloud, k -> new ArrayList<>()).add(ec2Slave); + } + } + + for (Map.Entry> entry : byCloud.entrySet()) { + EC2Cloud cloud = entry.getKey(); + List slaves = entry.getValue(); + List instanceIds = new ArrayList<>(slaves.size()); + for (EC2AbstractSlave s : slaves) { + instanceIds.add(s.getInstanceId()); + } + + try { + Map instances = CloudHelper.getInstancesBatch(instanceIds, cloud); + for (EC2AbstractSlave ec2Slave : slaves) { + try { + Instance inst = instances.get(ec2Slave.getInstanceId()); + if (inst == null) { + LOGGER.info("EC2 instance not found (likely terminated): " + ec2Slave.getInstanceId()); + ec2Slave.terminate(); + } else if (InstanceStateName.TERMINATED.equals( + inst.state().name())) { + LOGGER.info("EC2 instance is dead: " + ec2Slave.getInstanceId()); + ec2Slave.terminate(); + } else { + ec2Slave.updateFromFetchedInstance(inst); + } + } catch (SdkException e) { + if (e instanceof Ec2Exception + && EC2Cloud.EC2_REQUEST_EXPIRED_ERROR_CODE.equals( + ((Ec2Exception) e).awsErrorDetails().errorCode())) { + LOGGER.info("EC2 request expired, skipping consideration of " + ec2Slave.getInstanceId() + + " due to unknown state."); + } else { + LOGGER.info("EC2 instance is dead and failed to terminate: " + ec2Slave.getInstanceId()); + removeNode(ec2Slave); + } } - } catch (AmazonClientException e) { - if (e instanceof AmazonEC2Exception && - EC2_REQUEST_EXPIRED_ERROR_CODE.equals(((AmazonEC2Exception) e).getErrorCode())) { - LOGGER.info("EC2 request expired, skipping consideration of " + ec2Slave.getInstanceId() + " due to unknown state."); - } else { - LOGGER.info("EC2 instance is dead and failed to terminate: " + ec2Slave.getInstanceId()); - removeNode(ec2Slave); + } + } catch (SdkException e) { + LOGGER.log( + Level.WARNING, + "Batch describeInstances failed for cloud " + cloud.getName() + + ", falling back to per-node check", + e); + for (EC2AbstractSlave ec2Slave : slaves) { + try { + if (!ec2Slave.isAlive(true)) { + LOGGER.info("EC2 instance is dead: " + ec2Slave.getInstanceId()); + ec2Slave.terminate(); + } + } catch (SdkException ex) { + if (ex instanceof Ec2Exception + && EC2Cloud.EC2_REQUEST_EXPIRED_ERROR_CODE.equals( + ((Ec2Exception) ex).awsErrorDetails().errorCode())) { + LOGGER.info("EC2 request expired, skipping consideration of " + ec2Slave.getInstanceId() + + " due to unknown state."); + } else { + LOGGER.info("EC2 instance is dead and failed to terminate: " + ec2Slave.getInstanceId()); + removeNode(ec2Slave); + } } } } @@ -73,5 +132,4 @@ private void removeNode(EC2AbstractSlave ec2Slave) { LOGGER.log(Level.WARNING, "Failed to remove node: " + ec2Slave.getInstanceId()); } } - } diff --git a/src/main/java/hudson/plugins/ec2/EC2SpotSlave.java b/src/main/java/hudson/plugins/ec2/EC2SpotSlave.java index 04c4b861b..7256dfe57 100644 --- a/src/main/java/hudson/plugins/ec2/EC2SpotSlave.java +++ b/src/main/java/hudson/plugins/ec2/EC2SpotSlave.java @@ -1,33 +1,31 @@ package hudson.plugins.ec2; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.Extension; +import hudson.model.Computer; +import hudson.model.Descriptor.FormException; +import hudson.plugins.ec2.ssh.EC2UnixLauncher; +import hudson.plugins.ec2.ssh.EC2WindowsSSHLauncher; +import hudson.plugins.ec2.win.EC2WindowsLauncher; +import hudson.slaves.NodeProperty; import java.io.IOException; import java.util.Collections; import java.util.List; -import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; - import jenkins.model.Jenkins; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.DataBoundConstructor; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.model.CancelSpotInstanceRequestsRequest; -import com.amazonaws.services.ec2.model.DescribeSpotInstanceRequestsRequest; -import com.amazonaws.services.ec2.model.DescribeSpotInstanceRequestsResult; -import com.amazonaws.services.ec2.model.SpotInstanceRequest; -import com.amazonaws.services.ec2.model.SpotInstanceState; -import com.amazonaws.services.ec2.model.TerminateInstancesRequest; - -import hudson.Extension; -import hudson.model.Computer; -import hudson.model.Descriptor.FormException; -import hudson.plugins.ec2.ssh.EC2UnixLauncher; -import hudson.plugins.ec2.win.EC2WindowsLauncher; -import hudson.slaves.NodeProperty; - -import edu.umd.cs.findbugs.annotations.CheckForNull; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.ec2.Ec2Client; +import software.amazon.awssdk.services.ec2.model.CancelSpotInstanceRequestsRequest; +import software.amazon.awssdk.services.ec2.model.DescribeSpotInstanceRequestsRequest; +import software.amazon.awssdk.services.ec2.model.DescribeSpotInstanceRequestsResponse; +import software.amazon.awssdk.services.ec2.model.SpotInstanceRequest; +import software.amazon.awssdk.services.ec2.model.SpotInstanceState; +import software.amazon.awssdk.services.ec2.model.TerminateInstancesRequest; public class EC2SpotSlave extends EC2AbstractSlave implements EC2Readiness { private static final Logger LOGGER = Logger.getLogger(EC2SpotSlave.class.getName()); @@ -35,23 +33,143 @@ public class EC2SpotSlave extends EC2AbstractSlave implements EC2Readiness { private final String spotInstanceRequestId; @Deprecated - public EC2SpotSlave(String name, String spotInstanceRequestId, String templateDescription, String remoteFS, int numExecutors, Mode mode, String initScript, String tmpDir, String labelString, String remoteAdmin, String jvmopts, String idleTerminationMinutes, List tags, String cloudName, int launchTimeout, AMITypeData amiType) + public EC2SpotSlave( + String name, + String spotInstanceRequestId, + String templateDescription, + String remoteFS, + int numExecutors, + Mode mode, + String initScript, + String tmpDir, + String labelString, + String remoteAdmin, + String jvmopts, + String idleTerminationMinutes, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType) throws FormException, IOException { - this(name, spotInstanceRequestId, templateDescription, remoteFS, numExecutors, mode, initScript, tmpDir, labelString, remoteAdmin, jvmopts, idleTerminationMinutes, tags, cloudName, false, launchTimeout, amiType); + this( + name, + spotInstanceRequestId, + templateDescription, + remoteFS, + numExecutors, + mode, + initScript, + tmpDir, + labelString, + remoteAdmin, + jvmopts, + idleTerminationMinutes, + tags, + cloudName, + false, + launchTimeout, + amiType); } @Deprecated - public EC2SpotSlave(String name, String spotInstanceRequestId, String templateDescription, String remoteFS, int numExecutors, Mode mode, String initScript, String tmpDir, String labelString, String remoteAdmin, String jvmopts, String idleTerminationMinutes, List tags, String cloudName, boolean usePrivateDnsName, int launchTimeout, AMITypeData amiType) + public EC2SpotSlave( + String name, + String spotInstanceRequestId, + String templateDescription, + String remoteFS, + int numExecutors, + Mode mode, + String initScript, + String tmpDir, + String labelString, + String remoteAdmin, + String jvmopts, + String idleTerminationMinutes, + List tags, + String cloudName, + boolean usePrivateDnsName, + int launchTimeout, + AMITypeData amiType) throws FormException, IOException { - this(templateDescription + " (" + name + ")", spotInstanceRequestId, templateDescription, remoteFS, numExecutors, mode, initScript, tmpDir, labelString, Collections.emptyList(), remoteAdmin, DEFAULT_JAVA_PATH, jvmopts, idleTerminationMinutes, tags, cloudName, launchTimeout, amiType, ConnectionStrategy.backwardsCompatible(usePrivateDnsName, false, false), -1); + this( + templateDescription + " (" + name + ")", + spotInstanceRequestId, + templateDescription, + remoteFS, + numExecutors, + mode, + initScript, + tmpDir, + labelString, + Collections.emptyList(), + remoteAdmin, + DEFAULT_JAVA_PATH, + jvmopts, + idleTerminationMinutes, + tags, + cloudName, + launchTimeout, + amiType, + ConnectionStrategy.backwardsCompatible(usePrivateDnsName, false, false), + -1); } @DataBoundConstructor - public EC2SpotSlave(String name, String spotInstanceRequestId, String templateDescription, String remoteFS, int numExecutors, Mode mode, String initScript, String tmpDir, String labelString, List> nodeProperties, String remoteAdmin, String javaPath, String jvmopts, String idleTerminationMinutes, List tags, String cloudName, int launchTimeout, AMITypeData amiType, ConnectionStrategy connectionStrategy, int maxTotalUses) + public EC2SpotSlave( + String name, + String spotInstanceRequestId, + String templateDescription, + String remoteFS, + int numExecutors, + Mode mode, + String initScript, + String tmpDir, + String labelString, + List> nodeProperties, + String remoteAdmin, + String javaPath, + String jvmopts, + String idleTerminationMinutes, + List tags, + String cloudName, + int launchTimeout, + AMITypeData amiType, + ConnectionStrategy connectionStrategy, + int maxTotalUses) throws FormException, IOException { - super(name, "", templateDescription, remoteFS, numExecutors, mode, labelString, amiType.isWindows() ? new EC2WindowsLauncher() : - new EC2UnixLauncher(), new EC2RetentionStrategy(idleTerminationMinutes), initScript, tmpDir, nodeProperties, remoteAdmin, javaPath, jvmopts, false, idleTerminationMinutes, tags, cloudName, launchTimeout, amiType, connectionStrategy, maxTotalUses,null, DEFAULT_METADATA_ENDPOINT_ENABLED, DEFAULT_METADATA_TOKENS_REQUIRED, DEFAULT_METADATA_HOPS_LIMIT, DEFAULT_METADATA_SUPPORTED); + super( + name, + "", + templateDescription, + remoteFS, + numExecutors, + mode, + labelString, + (amiType.isWinRMAgent() + ? new EC2WindowsLauncher() + : (amiType.isWindows() ? new EC2WindowsSSHLauncher() : new EC2UnixLauncher())), + new EC2RetentionStrategy(idleTerminationMinutes), + initScript, + tmpDir, + nodeProperties, + remoteAdmin, + javaPath, + jvmopts, + false, + idleTerminationMinutes, + tags, + cloudName, + launchTimeout, + amiType, + connectionStrategy, + maxTotalUses, + null, + DEFAULT_METADATA_ENDPOINT_ENABLED, + DEFAULT_METADATA_TOKENS_REQUIRED, + DEFAULT_METADATA_HOPS_LIMIT, + DEFAULT_METADATA_SUPPORTED, + DEFAULT_ENCLAVE_ENABLED); this.name = name; this.spotInstanceRequestId = spotInstanceRequestId; @@ -66,46 +184,54 @@ protected boolean isAlive(boolean force) { * Cancel the spot request for the instance. Terminate the instance if it is up. Remove the agent from Jenkins. */ @Override - public void terminate() { + public Future terminate() { if (terminateScheduled.getCount() == 0) { - synchronized(terminateScheduled) { + synchronized (terminateScheduled) { if (terminateScheduled.getCount() == 0) { - Computer.threadPoolForRemoting.submit(() -> { + Future f = Computer.threadPoolForRemoting.submit(() -> { try { // Cancel the spot request - AmazonEC2 ec2 = getCloud().connect(); + Ec2Client ec2 = getCloud().connect(); String instanceId = getInstanceId(); List requestIds = Collections.singletonList(spotInstanceRequestId); - CancelSpotInstanceRequestsRequest cancelRequest = new CancelSpotInstanceRequestsRequest(requestIds); + CancelSpotInstanceRequestsRequest cancelRequest = + CancelSpotInstanceRequestsRequest.builder() + .spotInstanceRequestIds(requestIds) + .build(); try { ec2.cancelSpotInstanceRequests(cancelRequest); LOGGER.info("Cancelled Spot request: " + spotInstanceRequestId); - } catch (AmazonClientException e) { + } catch (SdkException e) { // Spot request is no longer valid LOGGER.log(Level.WARNING, "Failed to cancel Spot request: " + spotInstanceRequestId, e); } // Terminate the agent if it is running - if (instanceId != null && !instanceId.equals("")) { + if (instanceId != null && !instanceId.isEmpty()) { if (!super.isAlive(true)) { /* - * The node has been killed externally, so we've nothing to do here - */ + * The node has been killed externally, so we've nothing to do here + */ LOGGER.info("EC2 instance already terminated: " + instanceId); } else { - TerminateInstancesRequest request = new TerminateInstancesRequest(Collections.singletonList(instanceId)); + TerminateInstancesRequest request = TerminateInstancesRequest.builder() + .instanceIds(Collections.singletonList(instanceId)) + .build(); try { ec2.terminateInstances(request); LOGGER.info("Terminated EC2 instance (terminated): " + instanceId); - } catch (AmazonClientException e) { + } catch (SdkException e) { // Spot request is no longer valid - LOGGER.log(Level.WARNING, "Failed to terminate the Spot instance: " + instanceId, e); + LOGGER.log( + Level.WARNING, + "Failed to terminate the Spot instance: " + instanceId, + e); } } } } catch (Exception e) { - LOGGER.log(Level.WARNING,"Failed to remove agent: ", e); + LOGGER.log(Level.WARNING, "Failed to remove agent: ", e); } finally { // Remove the instance even if deletion failed, otherwise it will hang around forever in // the nodes page. One way for this to occur is that an instance was terminated @@ -116,15 +242,17 @@ public void terminate() { } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to remove agent: " + name, e); } - synchronized(terminateScheduled) { + synchronized (terminateScheduled) { terminateScheduled.countDown(); } } }); terminateScheduled.reset(); + return f; } } } + return CompletableFuture.completedFuture(null); } /** @@ -134,21 +262,25 @@ public void terminate() { */ @CheckForNull SpotInstanceRequest getSpotRequest() { - AmazonEC2 ec2 = getCloud().connect(); + Ec2Client ec2 = getCloud().connect(); if (this.spotInstanceRequestId == null) { return null; } - DescribeSpotInstanceRequestsRequest dsirRequest = new DescribeSpotInstanceRequestsRequest().withSpotInstanceRequestIds(this.spotInstanceRequestId); + DescribeSpotInstanceRequestsRequest dsirRequest = DescribeSpotInstanceRequestsRequest.builder() + .spotInstanceRequestIds(this.spotInstanceRequestId) + .build(); try { - DescribeSpotInstanceRequestsResult dsirResult = ec2.describeSpotInstanceRequests(dsirRequest); - List siRequests = dsirResult.getSpotInstanceRequests(); + DescribeSpotInstanceRequestsResponse dsirResult = ec2.describeSpotInstanceRequests(dsirRequest); + List siRequests = dsirResult.spotInstanceRequests(); return siRequests.get(0); - } catch (AmazonClientException e) { + } catch (SdkException e) { // Spot request is no longer valid - LOGGER.log(Level.WARNING, "Failed to fetch spot instance request for requestId: " + this.spotInstanceRequestId); + LOGGER.log( + Level.WARNING, + "Failed to fetch spot instance request for requestId: " + this.spotInstanceRequestId); } return null; @@ -160,10 +292,11 @@ public boolean isSpotRequestDead() { return true; } - SpotInstanceState requestState = SpotInstanceState.fromValue(spotRequest.getState()); - return requestState == SpotInstanceState.Cancelled - || requestState == SpotInstanceState.Closed - || requestState == SpotInstanceState.Failed; + SpotInstanceState requestState = + SpotInstanceState.fromValue(spotRequest.state().toString()); + return requestState == SpotInstanceState.CANCELLED + || requestState == SpotInstanceState.CLOSED + || requestState == SpotInstanceState.FAILED; } /** @@ -177,8 +310,9 @@ public String getSpotInstanceRequestId() { public String getInstanceId() { if (StringUtils.isEmpty(instanceId)) { SpotInstanceRequest sr = getSpotRequest(); - if (sr != null) - instanceId = sr.getInstanceId(); + if (sr != null) { + instanceId = sr.instanceId(); + } } return instanceId; } @@ -203,8 +337,9 @@ public String getDisplayName() { public String getEc2Type() { SpotInstanceRequest spotRequest = getSpotRequest(); if (spotRequest != null) { - String spotMaxBidPrice = spotRequest.getSpotPrice(); - return Messages.EC2SpotSlave_Spot1() + spotMaxBidPrice.substring(0, spotMaxBidPrice.length() - 3) + String spotMaxBidPrice = spotRequest.spotPrice(); + return Messages.EC2SpotSlave_Spot1() + + spotMaxBidPrice.substring(0, spotMaxBidPrice.length() - 3) + Messages.EC2SpotSlave_Spot2(); } return null; @@ -219,8 +354,8 @@ public boolean isReady() { public String getEc2ReadinessStatus() { SpotInstanceRequest sr = getSpotRequest(); if (sr != null) { - return sr.getStatus().getMessage(); + return sr.status().message(); } - throw new AmazonClientException("No spot instance request"); + throw SdkException.builder().message("No spot instance request").build(); } } diff --git a/src/main/java/hudson/plugins/ec2/EC2Step.java b/src/main/java/hudson/plugins/ec2/EC2Step.java index bd8967760..25099ed85 100644 --- a/src/main/java/hudson/plugins/ec2/EC2Step.java +++ b/src/main/java/hudson/plugins/ec2/EC2Step.java @@ -23,23 +23,30 @@ */ package hudson.plugins.ec2; -import com.amazonaws.services.ec2.model.Instance; import hudson.Extension; import hudson.Util; import hudson.model.TaskListener; import hudson.slaves.Cloud; import hudson.util.ListBoxModel; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; import jenkins.model.Jenkins; -import org.jenkinsci.plugins.workflow.steps.*; +import org.jenkinsci.plugins.workflow.steps.Step; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.StepDescriptor; +import org.jenkinsci.plugins.workflow.steps.StepExecution; +import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.verb.POST; - -import java.util.*; +import software.amazon.awssdk.services.ec2.model.Instance; /** * Returns the instance provisioned. - * + *

* Used like: * *

@@ -60,6 +67,7 @@ public EC2Step(String cloud, String template) {
         this.cloud = cloud;
         this.template = template;
     }
+
     public String getCloud() {
         return cloud;
     }
@@ -70,7 +78,7 @@ public String getTemplate() {
 
     @Override
     public StepExecution start(StepContext context) throws Exception {
-        return new EC2Step.Execution( this, context);
+        return new EC2Step.Execution(this, context);
     }
 
     @Extension
@@ -91,9 +99,7 @@ public ListBoxModel doFillCloudItems() {
             Jenkins.get().checkPermission(Jenkins.SYSTEM_READ);
             ListBoxModel r = new ListBoxModel();
             r.add("", "");
-            Jenkins.get().clouds
-                    .getAll(AmazonEC2Cloud.class)
-                    .forEach(c -> r.add(c.getDisplayName(), c.getDisplayName()));
+            Jenkins.get().clouds.getAll(EC2Cloud.class).forEach(c -> r.add(c.getDisplayName(), c.getDisplayName()));
             return r;
         }
 
@@ -102,30 +108,29 @@ public ListBoxModel doFillTemplateItems(@QueryParameter String cloudName) {
             Jenkins.get().checkPermission(Jenkins.SYSTEM_READ);
             ListBoxModel r = new ListBoxModel();
             Cloud cloud = Jenkins.get().getCloud(Util.fixEmpty(cloudName));
-            if (cloud instanceof AmazonEC2Cloud) {
-                AmazonEC2Cloud ec2Cloud = (AmazonEC2Cloud) cloud;
+            if (cloud instanceof EC2Cloud ec2Cloud) {
                 for (SlaveTemplate template : ec2Cloud.getTemplates()) {
                     for (String labelList : template.labels.split(" ")) {
-                        r.add(labelList + "  (AMI: " + template.getAmi() + ", REGION: " + ec2Cloud.getRegion() + ", TYPE: " + template.type.name() + ")", labelList);
+                        r.add(
+                                labelList + "  (AMI: " + template.getAmi() + ", REGION: " + ec2Cloud.getRegion()
+                                        + ", TYPE: " + template.type + ")",
+                                labelList);
                     }
                 }
             }
             return r;
         }
 
-
         @Override
         public Set> getRequiredContext() {
             return Collections.singleton(TaskListener.class);
         }
-
     }
 
     public static class Execution extends SynchronousNonBlockingStepExecution {
         private final String cloud;
         private final String template;
 
-
         Execution(EC2Step step, StepContext context) {
             super(context);
             this.cloud = step.cloud;
@@ -135,9 +140,9 @@ public static class Execution extends SynchronousNonBlockingStepExecution opt = EnumSet.noneOf(SlaveTemplate.ProvisionOptions.class);
@@ -145,16 +150,19 @@ protected Instance run() throws Exception {
 
                     List instances = t.provision(1, opt);
                     if (instances == null) {
-                        throw new IllegalArgumentException("Error in AWS Cloud. Please review AWS template defined in Jenkins configuration.");
+                        throw new IllegalArgumentException(
+                                "Error in AWS Cloud. Please review AWS template defined in Jenkins configuration.");
                     }
 
                     EC2AbstractSlave slave = instances.get(0);
-                    return CloudHelper.getInstanceWithRetry(slave.getInstanceId(), (AmazonEC2Cloud) cl);
+                    return CloudHelper.getInstanceWithRetry(slave.getInstanceId(), (EC2Cloud) cl);
                 } else {
-                    throw new IllegalArgumentException("Error in AWS Cloud. Please review AWS template defined in Jenkins configuration.");
+                    throw new IllegalArgumentException(
+                            "Error in AWS Cloud. Please review AWS template defined in Jenkins configuration.");
                 }
             } else {
-                throw new IllegalArgumentException("Error in AWS Cloud. Please review EC2 settings in Jenkins configuration.");
+                throw new IllegalArgumentException(
+                        "Error in AWS Cloud. Please review EC2 settings in Jenkins configuration.");
             }
         }
 
@@ -172,5 +180,4 @@ public Cloud getByDisplayName(Jenkins.CloudList clouds, String name) {
             return c;
         }
     }
-
 }
diff --git a/src/main/java/hudson/plugins/ec2/EC2Tag.java b/src/main/java/hudson/plugins/ec2/EC2Tag.java
index ccdaef430..302b0d6b7 100644
--- a/src/main/java/hudson/plugins/ec2/EC2Tag.java
+++ b/src/main/java/hudson/plugins/ec2/EC2Tag.java
@@ -23,17 +23,14 @@
  */
 package hudson.plugins.ec2;
 
-import hudson.model.Descriptor;
-import hudson.model.AbstractDescribableImpl;
 import hudson.Extension;
-import org.kohsuke.stapler.DataBoundConstructor;
-
-import java.util.List;
+import hudson.model.AbstractDescribableImpl;
+import hudson.model.Descriptor;
 import java.util.LinkedList;
+import java.util.List;
 import java.util.Objects;
-
-
-import com.amazonaws.services.ec2.model.Tag;
+import org.kohsuke.stapler.DataBoundConstructor;
+import software.amazon.awssdk.services.ec2.model.Tag;
 
 public class EC2Tag extends AbstractDescribableImpl {
     private final String name;
@@ -43,8 +40,11 @@ public class EC2Tag extends AbstractDescribableImpl {
      * Tag name for the specific jenkins agent type tag, used to identify the EC2 instances provisioned by this plugin.
      */
     public static final String TAG_NAME_JENKINS_SLAVE_TYPE = "jenkins_slave_type";
+
     public static final String TAG_NAME_JENKINS_SERVER_URL = "jenkins_server_url";
 
+    public static final String TAG_NAME_JENKINS_CLOUD_NAME = "jenkins_cloud_name";
+
     @DataBoundConstructor
     public EC2Tag(String name, String value) {
         this.name = name;
@@ -53,8 +53,8 @@ public EC2Tag(String name, String value) {
 
     /* Constructor from Amazon Tag */
     public EC2Tag(Tag t) {
-        name = t.getKey();
-        value = t.getValue();
+        name = t.key();
+        value = t.value();
     }
 
     public String getName() {
@@ -72,16 +72,20 @@ public String toString() {
 
     @Override
     public boolean equals(Object o) {
-        if (o == null)
+        if (o == null) {
             return false;
-        if (this.getClass() != o.getClass())
+        }
+        if (this.getClass() != o.getClass()) {
             return false;
+        }
 
         EC2Tag other = (EC2Tag) o;
-        if ((name == null && other.name != null) || (name != null && !name.equals(other.name)))
+        if ((name == null && other.name != null) || (name != null && !name.equals(other.name))) {
             return false;
-        if ((value == null && other.value != null) || (value != null && !value.equals(other.value)))
+        }
+        if ((value == null && other.value != null) || (value != null && !value.equals(other.value))) {
             return false;
+        }
 
         return true;
     }
@@ -105,7 +109,7 @@ public static List fromAmazonTags(List amazonTags) {
             return null;
         }
 
-        LinkedList result = new LinkedList();
+        LinkedList result = new LinkedList<>();
         for (Tag t : amazonTags) {
             result.add(new EC2Tag(t));
         }
diff --git a/src/main/java/hudson/plugins/ec2/EbsEncryptRootVolume.java b/src/main/java/hudson/plugins/ec2/EbsEncryptRootVolume.java
index 47ca5d15a..79d169b49 100644
--- a/src/main/java/hudson/plugins/ec2/EbsEncryptRootVolume.java
+++ b/src/main/java/hudson/plugins/ec2/EbsEncryptRootVolume.java
@@ -23,5 +23,4 @@ public String getDisplayText() {
     public Boolean getValue() {
         return value;
     }
-
 }
diff --git a/src/main/java/hudson/plugins/ec2/Eucalyptus.java b/src/main/java/hudson/plugins/ec2/Eucalyptus.java
deleted file mode 100644
index 68c6fa612..000000000
--- a/src/main/java/hudson/plugins/ec2/Eucalyptus.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * The MIT License
- *
- * Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-package hudson.plugins.ec2;
-
-import hudson.Extension;
-import hudson.model.ItemGroup;
-import hudson.util.FormValidation;
-
-import java.io.IOException;
-import java.net.URL;
-import java.util.List;
-
-import javax.servlet.ServletException;
-
-import org.kohsuke.stapler.AncestorInPath;
-import org.kohsuke.stapler.DataBoundConstructor;
-import org.kohsuke.stapler.QueryParameter;
-import org.kohsuke.stapler.interceptor.RequirePOST;
-
-/**
- * Eucalyptus.
- *
- * @author Kohsuke Kawaguchi
- */
-public class Eucalyptus extends EC2Cloud {
-    private final URL ec2endpoint;
-    private final URL s3endpoint;
-
-    @DataBoundConstructor
-    public Eucalyptus(String name, URL ec2EndpointUrl, URL s3EndpointUrl, boolean useInstanceProfileForCredentials, String credentialsId, String privateKey, String sshKeysCredentialsId, String instanceCapStr, List templates, String roleArn, String roleSessionName) {
-        super(name, useInstanceProfileForCredentials, credentialsId, privateKey, sshKeysCredentialsId, instanceCapStr, templates, roleArn, roleSessionName);
-        this.ec2endpoint = ec2EndpointUrl;
-        this.s3endpoint = s3EndpointUrl;
-    }
-
-    @Deprecated
-    public Eucalyptus(URL ec2EndpointUrl, URL s3EndpointUrl, boolean useInstanceProfileForCredentials, String credentialsId, String privateKey, String sshKeysCredentialsId, String instanceCapStr, List templates, String roleArn, String roleSessionName)
-            throws IOException {
-        this("eucalyptus", ec2EndpointUrl, s3EndpointUrl, useInstanceProfileForCredentials, credentialsId, privateKey, sshKeysCredentialsId, instanceCapStr, templates, roleArn, roleSessionName);
-    }
-
-    @Deprecated
-    public Eucalyptus(URL ec2EndpointUrl, URL s3EndpointUrl, boolean useInstanceProfileForCredentials, String credentialsId, String privateKey, String instanceCapStr, List templates, String roleArn, String roleSessionName)
-            throws IOException {
-        this("eucalyptus", ec2EndpointUrl, s3EndpointUrl, useInstanceProfileForCredentials, credentialsId, privateKey, null, instanceCapStr, templates, roleArn, roleSessionName);
-    }
-
-    @Override
-    public URL getEc2EndpointUrl() throws IOException {
-        return this.ec2endpoint;
-    }
-
-    @Override
-    public URL getS3EndpointUrl() throws IOException {
-        return this.s3endpoint;
-    }
-
-    @Extension
-    public static class DescriptorImpl extends EC2Cloud.DescriptorImpl {
-        @Override
-        public String getDisplayName() {
-            return "Eucalyptus";
-        }
-
-        @Override
-        @RequirePOST
-        public FormValidation doTestConnection(@AncestorInPath ItemGroup context, @QueryParameter URL ec2endpoint, @QueryParameter boolean useInstanceProfileForCredentials, @QueryParameter String credentialsId, @QueryParameter String sshKeysCredentialsId, @QueryParameter String roleArn, @QueryParameter String roleSessionName, @QueryParameter String region)
-                throws IOException, ServletException {
-            return super.doTestConnection(context, ec2endpoint, useInstanceProfileForCredentials, credentialsId, sshKeysCredentialsId, roleArn, roleSessionName, region);
-        }
-    }
-}
diff --git a/src/main/java/hudson/plugins/ec2/HostKeyVerificationStrategyEnum.java b/src/main/java/hudson/plugins/ec2/HostKeyVerificationStrategyEnum.java
index 69b8af71a..df4df25dd 100644
--- a/src/main/java/hudson/plugins/ec2/HostKeyVerificationStrategyEnum.java
+++ b/src/main/java/hudson/plugins/ec2/HostKeyVerificationStrategyEnum.java
@@ -35,12 +35,15 @@ public enum HostKeyVerificationStrategyEnum {
     CHECK_NEW_SOFT("check-new-soft", "accept-new", new CheckNewSoftStrategy()),
     ACCEPT_NEW("accept-new", "accept-new", new AcceptNewStrategy()),
     OFF("off", "no", new NonVerifyingKeyVerificationStrategy());
-    
+
     private final String displayText;
     private final SshHostKeyVerificationStrategy strategy;
     private final String sshCommandEquivalentFlag;
-    
-    HostKeyVerificationStrategyEnum(@NonNull String displayText, @NonNull String sshCommandEquivalentFlag, @NonNull SshHostKeyVerificationStrategy strategy) {
+
+    HostKeyVerificationStrategyEnum(
+            @NonNull String displayText,
+            @NonNull String sshCommandEquivalentFlag,
+            @NonNull SshHostKeyVerificationStrategy strategy) {
         this.displayText = displayText;
         this.sshCommandEquivalentFlag = sshCommandEquivalentFlag;
         this.strategy = strategy;
@@ -50,7 +53,7 @@ public enum HostKeyVerificationStrategyEnum {
     public SshHostKeyVerificationStrategy getStrategy() {
         return strategy;
     }
-    
+
     public boolean equalsDisplayText(String other) {
         return this.displayText.equals(other);
     }
diff --git a/src/main/java/hudson/plugins/ec2/InstanceState.java b/src/main/java/hudson/plugins/ec2/InstanceState.java
index 4384bdca2..afaf150e4 100644
--- a/src/main/java/hudson/plugins/ec2/InstanceState.java
+++ b/src/main/java/hudson/plugins/ec2/InstanceState.java
@@ -29,7 +29,12 @@
  * @author Kohsuke Kawaguchi
  */
 public enum InstanceState {
-    PENDING, RUNNING, SHUTTING_DOWN, TERMINATED, STOPPING, STOPPED;
+    PENDING,
+    RUNNING,
+    SHUTTING_DOWN,
+    TERMINATED,
+    STOPPING,
+    STOPPED;
 
     public String getCode() {
         return name().toLowerCase().replace('_', '-');
diff --git a/src/main/java/hudson/plugins/ec2/InstanceTypeConverter.java b/src/main/java/hudson/plugins/ec2/InstanceTypeConverter.java
index c659aa263..220d18169 100644
--- a/src/main/java/hudson/plugins/ec2/InstanceTypeConverter.java
+++ b/src/main/java/hudson/plugins/ec2/InstanceTypeConverter.java
@@ -23,15 +23,14 @@
  */
 package hudson.plugins.ec2;
 
-import java.util.HashMap;
-import java.util.Map;
-
-import com.amazonaws.services.ec2.model.InstanceType;
 import com.thoughtworks.xstream.converters.Converter;
 import com.thoughtworks.xstream.converters.MarshallingContext;
 import com.thoughtworks.xstream.converters.UnmarshallingContext;
 import com.thoughtworks.xstream.io.HierarchicalStreamReader;
 import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+import java.util.HashMap;
+import java.util.Map;
+import software.amazon.awssdk.services.ec2.model.InstanceType;
 
 /*
  * Note this is used only to handle the metadata for older versions of the ec2-plugin. The current
@@ -39,31 +38,34 @@
  */
 public class InstanceTypeConverter implements Converter {
 
-    private static final Map TYPICAL_INSTANCE_TYPES = new HashMap();
+    private static final Map TYPICAL_INSTANCE_TYPES = new HashMap<>();
 
     static {
-        TYPICAL_INSTANCE_TYPES.put("DEFAULT", InstanceType.M1Small);
-        TYPICAL_INSTANCE_TYPES.put("LARGE", InstanceType.M1Large);
-        TYPICAL_INSTANCE_TYPES.put("XLARGE", InstanceType.M1Xlarge);
-        TYPICAL_INSTANCE_TYPES.put("MEDIUM_HCPU", InstanceType.C1Medium);
-        TYPICAL_INSTANCE_TYPES.put("XLARGE_HCPU", InstanceType.C1Xlarge);
-        TYPICAL_INSTANCE_TYPES.put("XLARGE_HMEM", InstanceType.M2Xlarge);
-        TYPICAL_INSTANCE_TYPES.put("XLARGE_HMEM_M3", InstanceType.M3Xlarge);
-        TYPICAL_INSTANCE_TYPES.put("XLARGE_DOUBLE_HMEM", InstanceType.M22xlarge);
-        TYPICAL_INSTANCE_TYPES.put("XLARGE_QUAD_HMEM", InstanceType.M24xlarge);
-        TYPICAL_INSTANCE_TYPES.put("XLARGE_QUAD_HMEM_M3", InstanceType.M32xlarge);
-        TYPICAL_INSTANCE_TYPES.put("XLARGE_CLUSTER_COMPUTE", InstanceType.Cc14xlarge);
+        TYPICAL_INSTANCE_TYPES.put("DEFAULT", InstanceType.M1_SMALL);
+        TYPICAL_INSTANCE_TYPES.put("LARGE", InstanceType.M1_LARGE);
+        TYPICAL_INSTANCE_TYPES.put("XLARGE", InstanceType.M1_XLARGE);
+        TYPICAL_INSTANCE_TYPES.put("MEDIUM_HCPU", InstanceType.C1_MEDIUM);
+        TYPICAL_INSTANCE_TYPES.put("XLARGE_HCPU", InstanceType.C1_XLARGE);
+        TYPICAL_INSTANCE_TYPES.put("XLARGE_HMEM", InstanceType.M2_XLARGE);
+        TYPICAL_INSTANCE_TYPES.put("XLARGE_HMEM_M3", InstanceType.M3_XLARGE);
+        TYPICAL_INSTANCE_TYPES.put("XLARGE_DOUBLE_HMEM", InstanceType.M2_2_XLARGE);
+        TYPICAL_INSTANCE_TYPES.put("XLARGE_QUAD_HMEM", InstanceType.M2_4_XLARGE);
+        TYPICAL_INSTANCE_TYPES.put("XLARGE_QUAD_HMEM_M3", InstanceType.M3_2_XLARGE);
+        TYPICAL_INSTANCE_TYPES.put("XLARGE_CLUSTER_COMPUTE", InstanceType.CC1_4_XLARGE);
     }
 
+    @Override
     public boolean canConvert(Class type) {
         return InstanceType.class == type;
     }
 
+    @Override
     public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
         InstanceType instanceType = (InstanceType) source;
         writer.setValue(instanceType.name());
     }
 
+    @Override
     public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
         InstanceType instanceType = null;
 
@@ -77,5 +79,4 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co
 
         return instanceType;
     }
-
 }
diff --git a/src/main/java/hudson/plugins/ec2/MacData.java b/src/main/java/hudson/plugins/ec2/MacData.java
index 7fc77a5d9..155895457 100644
--- a/src/main/java/hudson/plugins/ec2/MacData.java
+++ b/src/main/java/hudson/plugins/ec2/MacData.java
@@ -6,48 +6,28 @@
 import org.apache.commons.lang.StringUtils;
 import org.kohsuke.stapler.DataBoundConstructor;
 
-public class MacData extends AMITypeData {
-    private final String rootCommandPrefix;
-    private final String slaveCommandPrefix;
-    private final String slaveCommandSuffix;
-    private final String sshPort;
-    private final String bootDelay;
-
+public class MacData extends SSHData {
     @DataBoundConstructor
-    public MacData(String rootCommandPrefix, String slaveCommandPrefix, String slaveCommandSuffix, String sshPort, String bootDelay) {
-        this.rootCommandPrefix = rootCommandPrefix;
-        this.slaveCommandPrefix = slaveCommandPrefix;
-        this.slaveCommandSuffix = slaveCommandSuffix;
-        this.sshPort = sshPort;
-        this.bootDelay = bootDelay;
-
-        this.readResolve();
+    public MacData(
+            String rootCommandPrefix,
+            String slaveCommandPrefix,
+            String slaveCommandSuffix,
+            String sshPort,
+            String bootDelay) {
+        super(rootCommandPrefix, slaveCommandPrefix, slaveCommandSuffix, sshPort, bootDelay);
     }
 
+    @Override
     protected Object readResolve() {
         Jenkins.get().checkPermission(Jenkins.ADMINISTER);
         return this;
     }
 
-    @Override
-    public boolean isWindows() {
-        return false;
-    }
-
-    @Override
-    public boolean isUnix() {
-        return false;
-    }
-
     @Override
     public boolean isMac() {
         return true;
     }
 
-    public String getBootDelay() {
-        return bootDelay;
-    }
-
     @Extension
     public static class DescriptorImpl extends Descriptor {
         @Override
@@ -56,22 +36,6 @@ public String getDisplayName() {
         }
     }
 
-    public String getRootCommandPrefix() {
-        return rootCommandPrefix;
-    }
-
-    public String getSlaveCommandPrefix() {
-        return slaveCommandPrefix;
-    }
-
-    public String getSlaveCommandSuffix() {
-        return slaveCommandSuffix;
-    }
-
-    public String getSshPort() {
-        return sshPort == null || sshPort.isEmpty() ? "22" : sshPort;
-    }
-
     @Override
     public int hashCode() {
         final int prime = 31;
@@ -85,33 +49,44 @@ public int hashCode() {
 
     @Override
     public boolean equals(Object obj) {
-        if (this == obj)
+        if (this == obj) {
             return true;
-        if (obj == null)
+        }
+        if (obj == null) {
             return false;
-        if (this.getClass() != obj.getClass())
+        }
+        if (this.getClass() != obj.getClass()) {
             return false;
+        }
         final MacData other = (MacData) obj;
         if (StringUtils.isEmpty(rootCommandPrefix)) {
-            if (!StringUtils.isEmpty(other.rootCommandPrefix))
+            if (!StringUtils.isEmpty(other.rootCommandPrefix)) {
                 return false;
-        } else if (!rootCommandPrefix.equals(other.rootCommandPrefix))
+            }
+        } else if (!rootCommandPrefix.equals(other.rootCommandPrefix)) {
             return false;
+        }
         if (StringUtils.isEmpty(slaveCommandPrefix)) {
-            if (!StringUtils.isEmpty(other.slaveCommandPrefix))
+            if (!StringUtils.isEmpty(other.slaveCommandPrefix)) {
                 return false;
-        } else if (!slaveCommandPrefix.equals(other.slaveCommandPrefix))
+            }
+        } else if (!slaveCommandPrefix.equals(other.slaveCommandPrefix)) {
             return false;
+        }
         if (StringUtils.isEmpty(slaveCommandSuffix)) {
-            if (!StringUtils.isEmpty(other.slaveCommandSuffix))
+            if (!StringUtils.isEmpty(other.slaveCommandSuffix)) {
                 return false;
-        } else if (!slaveCommandSuffix.equals(other.slaveCommandSuffix))
+            }
+        } else if (!slaveCommandSuffix.equals(other.slaveCommandSuffix)) {
             return false;
+        }
         if (StringUtils.isEmpty(sshPort)) {
-            if (!StringUtils.isEmpty(other.sshPort))
+            if (!StringUtils.isEmpty(other.sshPort)) {
                 return false;
-        } else if (!sshPort.equals(other.sshPort))
+            }
+        } else if (!sshPort.equals(other.sshPort)) {
             return false;
+        }
         return true;
     }
 }
diff --git a/src/main/java/hudson/plugins/ec2/NoDelayProvisionerStrategy.java b/src/main/java/hudson/plugins/ec2/NoDelayProvisionerStrategy.java
index a4af7a43a..d9993eaa3 100644
--- a/src/main/java/hudson/plugins/ec2/NoDelayProvisionerStrategy.java
+++ b/src/main/java/hudson/plugins/ec2/NoDelayProvisionerStrategy.java
@@ -5,11 +5,10 @@
 import hudson.model.LoadStatistics;
 import hudson.slaves.Cloud;
 import hudson.slaves.NodeProvisioner;
-import jenkins.model.Jenkins;
-
 import java.util.Collection;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import jenkins.model.Jenkins;
 
 /**
  * Implementation of {@link NodeProvisioner.Strategy} which will provision a new node immediately as
@@ -28,27 +27,42 @@ public NodeProvisioner.StrategyDecision apply(NodeProvisioner.StrategyState stra
         final Label label = strategyState.getLabel();
 
         LoadStatistics.LoadStatisticsSnapshot snapshot = strategyState.getSnapshot();
-        int availableCapacity =
-                  snapshot.getAvailableExecutors()   // live executors
-                + snapshot.getConnectingExecutors()  // executors present but not yet connected
-                + strategyState.getPlannedCapacitySnapshot()     // capacity added by previous strategies from previous rounds
-                + strategyState.getAdditionalPlannedCapacity();  // capacity added by previous strategies _this round_
+
+        int availableCapacity = snapshot.getAvailableExecutors() // live executors (idle)
+                + snapshot.getConnectingExecutors() // executors present but not yet connected
+                + strategyState
+                        .getPlannedCapacitySnapshot() // capacity added by previous strategies from previous rounds
+                + strategyState.getAdditionalPlannedCapacity(); // capacity added by previous strategies _this round_
         int currentDemand = snapshot.getQueueLength();
-        LOGGER.log(Level.FINE, "Available capacity={0}, currentDemand={1}",
-                new Object[]{availableCapacity, currentDemand});
+
+        LOGGER.log(
+                Level.FINE, "Available capacity={0}, currentDemand={1}", new Object[] {availableCapacity, currentDemand
+                });
         if (availableCapacity < currentDemand) {
             Jenkins jenkinsInstance = Jenkins.get();
             for (Cloud cloud : jenkinsInstance.clouds) {
-                if (!(cloud instanceof AmazonEC2Cloud)) continue;
-                if (!cloud.canProvision(label)) continue;
-                AmazonEC2Cloud ec2 = (AmazonEC2Cloud) cloud;
-                if (!ec2.isNoDelayProvisioning()) continue;
+                if (!(cloud instanceof EC2Cloud ec2)) {
+                    continue;
+                }
+                if (!cloud.canProvision(new Cloud.CloudState(label, 0))) {
+                    continue;
+                }
+                if (!ec2.isNoDelayProvisioning()) {
+                    continue;
+                }
+
+                int numToProvision = currentDemand - availableCapacity;
+                LOGGER.log(Level.FINE, "Planned {0} new nodes", numToProvision);
+
+                Collection plannedNodes =
+                        cloud.provision(new Cloud.CloudState(label, 0), numToProvision);
 
-                Collection plannedNodes = cloud.provision(label, currentDemand - availableCapacity);
                 LOGGER.log(Level.FINE, "Planned {0} new nodes", plannedNodes.size());
                 strategyState.recordPendingLaunches(plannedNodes);
                 availableCapacity += plannedNodes.size();
-                LOGGER.log(Level.FINE, "After provisioning, available capacity={0}, currentDemand={1}", new Object[]{availableCapacity, currentDemand});
+                LOGGER.log(Level.FINE, "After provisioning, available capacity={0}, currentDemand={1}", new Object[] {
+                    availableCapacity, currentDemand
+                });
                 break;
             }
         }
@@ -60,5 +74,4 @@ public NodeProvisioner.StrategyDecision apply(NodeProvisioner.StrategyState stra
             return NodeProvisioner.StrategyDecision.CONSULT_REMAINING_STRATEGIES;
         }
     }
-
-}
\ No newline at end of file
+}
diff --git a/src/main/java/hudson/plugins/ec2/PluginImpl.java b/src/main/java/hudson/plugins/ec2/PluginImpl.java
index 656cfff58..666046144 100644
--- a/src/main/java/hudson/plugins/ec2/PluginImpl.java
+++ b/src/main/java/hudson/plugins/ec2/PluginImpl.java
@@ -28,12 +28,9 @@
 import hudson.model.Describable;
 import hudson.model.Descriptor;
 import hudson.plugins.ec2.util.MinimumInstanceChecker;
-import jenkins.model.Jenkins;
-
 import java.io.IOException;
 import java.util.logging.Logger;
-
-import java.io.IOException;
+import jenkins.model.Jenkins;
 
 /**
  * Added to handle backwards compatibility of xstream class name mapping.
@@ -41,17 +38,18 @@
 @Extension
 public class PluginImpl extends Plugin implements Describable {
     private static final Logger LOGGER = Logger.getLogger(PluginImpl.class.getName());
-    
+
     // Whether the SshHostKeyVerificationAdministrativeMonitor should show messages when we have templates using
     // accept-new or check-new-soft strategies
-    private long dismissInsecureMessages; 
+    private long dismissInsecureMessages;
 
     public void saveDismissInsecureMessages(long dismissInsecureMessages) {
         this.dismissInsecureMessages = dismissInsecureMessages;
         try {
             save();
-        } catch(IOException io) {
-            LOGGER.warning("There was a problem saving that you want to dismiss all messages related to insecure EC2 templates");
+        } catch (IOException io) {
+            LOGGER.warning(
+                    "There was a problem saving that you want to dismiss all messages related to insecure EC2 templates");
         }
     }
 
@@ -59,6 +57,7 @@ public long getDismissInsecureMessages() {
         return dismissInsecureMessages;
     }
 
+    @Override
     public DescriptorImpl getDescriptor() {
         return (DescriptorImpl) Jenkins.get().getDescriptorOrDie(getClass());
     }
@@ -78,13 +77,13 @@ public String getDisplayName() {
     @Override
     public void postInitialize() throws IOException {
         // backward compatibility with the legacy class name
-        Jenkins.XSTREAM.alias("hudson.plugins.ec2.EC2Cloud", AmazonEC2Cloud.class);
+        Jenkins.XSTREAM.alias("hudson.plugins.ec2.EC2Cloud", AmazonEC2Cloud.class, EC2Cloud.class);
         Jenkins.XSTREAM.alias("hudson.plugins.ec2.EC2Slave", EC2OndemandSlave.class);
         // backward compatibility with the legacy instance type
         Jenkins.XSTREAM.registerConverter(new InstanceTypeConverter());
 
         load();
-        
+
         MinimumInstanceChecker.checkForMinimumInstances();
     }
 }
diff --git a/src/main/java/hudson/plugins/ec2/SSHData.java b/src/main/java/hudson/plugins/ec2/SSHData.java
new file mode 100644
index 000000000..5d7c5a00a
--- /dev/null
+++ b/src/main/java/hudson/plugins/ec2/SSHData.java
@@ -0,0 +1,143 @@
+package hudson.plugins.ec2;
+
+import jenkins.model.Jenkins;
+import org.apache.commons.lang.StringUtils;
+
+public abstract class SSHData extends AMITypeData {
+    protected final String rootCommandPrefix;
+    protected final String slaveCommandPrefix;
+    protected final String slaveCommandSuffix;
+    protected final String sshPort;
+    protected final String bootDelay;
+
+    protected SSHData(
+            String rootCommandPrefix,
+            String slaveCommandPrefix,
+            String slaveCommandSuffix,
+            String sshPort,
+            String bootDelay) {
+        this.rootCommandPrefix = rootCommandPrefix;
+        this.slaveCommandPrefix = slaveCommandPrefix;
+        this.slaveCommandSuffix = slaveCommandSuffix;
+        this.sshPort = sshPort;
+        this.bootDelay = bootDelay;
+
+        this.readResolve();
+    }
+
+    protected Object readResolve() {
+        Jenkins j = Jenkins.getInstanceOrNull();
+        if (j != null) {
+            j.checkPermission(Jenkins.ADMINISTER);
+        }
+        return this;
+    }
+
+    @Override
+    public boolean isWindows() {
+        return false;
+    }
+
+    @Override
+    public boolean isUnix() {
+        return false;
+    }
+
+    @Override
+    public boolean isMac() {
+        return false;
+    }
+
+    @Override
+    public boolean isSSHAgent() {
+        return true;
+    }
+
+    @Override
+    public boolean isWinRMAgent() {
+        return false;
+    }
+
+    public String getRootCommandPrefix() {
+        return rootCommandPrefix;
+    }
+
+    public String getSlaveCommandPrefix() {
+        return slaveCommandPrefix;
+    }
+
+    public String getSlaveCommandSuffix() {
+        return slaveCommandSuffix;
+    }
+
+    public String getSshPort() {
+        return sshPort == null || sshPort.isEmpty() ? "22" : sshPort;
+    }
+
+    @Override
+    public String getBootDelay() {
+        return bootDelay;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((rootCommandPrefix == null) ? 0 : rootCommandPrefix.hashCode());
+        result = prime * result + ((slaveCommandPrefix == null) ? 0 : slaveCommandPrefix.hashCode());
+        result = prime * result + ((slaveCommandSuffix == null) ? 0 : slaveCommandSuffix.hashCode());
+        result = prime * result + ((sshPort == null) ? 0 : sshPort.hashCode());
+        result = prime * result + ((bootDelay == null) ? 0 : bootDelay.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (this.getClass() != obj.getClass()) {
+            return false;
+        }
+        final SSHData other = (SSHData) obj;
+        if (StringUtils.isEmpty(rootCommandPrefix)) {
+            if (!StringUtils.isEmpty(other.rootCommandPrefix)) {
+                return false;
+            }
+        } else if (!rootCommandPrefix.equals(other.rootCommandPrefix)) {
+            return false;
+        }
+        if (StringUtils.isEmpty(slaveCommandPrefix)) {
+            if (!StringUtils.isEmpty(other.slaveCommandPrefix)) {
+                return false;
+            }
+        } else if (!slaveCommandPrefix.equals(other.slaveCommandPrefix)) {
+            return false;
+        }
+        if (StringUtils.isEmpty(slaveCommandSuffix)) {
+            if (!StringUtils.isEmpty(other.slaveCommandSuffix)) {
+                return false;
+            }
+        } else if (!slaveCommandSuffix.equals(other.slaveCommandSuffix)) {
+            return false;
+        }
+        if (StringUtils.isEmpty(sshPort)) {
+            if (!StringUtils.isEmpty(other.sshPort)) {
+                return false;
+            }
+        } else if (!sshPort.equals(other.sshPort)) {
+            return false;
+        }
+        if (bootDelay == null) {
+            if (other.bootDelay != null) {
+                return false;
+            }
+        } else if (!bootDelay.equals(other.bootDelay)) {
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java
index 337b2b78d..aeb8e5c4f 100644
--- a/src/main/java/hudson/plugins/ec2/SlaveTemplate.java
+++ b/src/main/java/hudson/plugins/ec2/SlaveTemplate.java
@@ -17,59 +17,8 @@
  * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 package hudson.plugins.ec2;
-import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED;
-import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED;
-import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED;
-import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT;
-import static hudson.plugins.ec2.EC2AbstractSlave.DEFAULT_JAVA_PATH;
-
-import com.amazonaws.AmazonClientException;
-import com.amazonaws.AmazonServiceException;
-import com.amazonaws.auth.AWSCredentialsProvider;
-import com.amazonaws.services.ec2.AmazonEC2;
-import com.amazonaws.services.ec2.model.AmazonEC2Exception;
-import com.amazonaws.services.ec2.model.BlockDeviceMapping;
-import com.amazonaws.services.ec2.model.CancelSpotInstanceRequestsRequest;
-import com.amazonaws.services.ec2.model.CreateTagsRequest;
-import com.amazonaws.services.ec2.model.CreditSpecificationRequest;
-import com.amazonaws.services.ec2.model.DescribeImagesRequest;
-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.DescribeSpotInstanceRequestsRequest;
-import com.amazonaws.services.ec2.model.DescribeSubnetsRequest;
-import com.amazonaws.services.ec2.model.DescribeSubnetsResult;
-import com.amazonaws.services.ec2.model.Filter;
-import com.amazonaws.services.ec2.model.HttpTokensState;
-import com.amazonaws.services.ec2.model.IamInstanceProfileSpecification;
-import com.amazonaws.services.ec2.model.Image;
-import com.amazonaws.services.ec2.model.Instance;
-import com.amazonaws.services.ec2.model.InstanceMarketOptionsRequest;
-import com.amazonaws.services.ec2.model.InstanceMetadataEndpointState;
-import com.amazonaws.services.ec2.model.InstanceMetadataOptionsRequest;
-import com.amazonaws.services.ec2.model.InstanceNetworkInterfaceSpecification;
-import com.amazonaws.services.ec2.model.InstanceStateName;
-import com.amazonaws.services.ec2.model.InstanceType;
-import com.amazonaws.services.ec2.model.KeyPair;
-import com.amazonaws.services.ec2.model.LaunchSpecification;
-import com.amazonaws.services.ec2.model.MarketType;
-import com.amazonaws.services.ec2.model.Placement;
-import com.amazonaws.services.ec2.model.RequestSpotInstancesRequest;
-import com.amazonaws.services.ec2.model.RequestSpotInstancesResult;
-import com.amazonaws.services.ec2.model.Reservation;
-import com.amazonaws.services.ec2.model.ResourceType;
-import com.amazonaws.services.ec2.model.RunInstancesRequest;
-import com.amazonaws.services.ec2.model.SecurityGroup;
-import com.amazonaws.services.ec2.model.ShutdownBehavior;
-import com.amazonaws.services.ec2.model.SpotInstanceRequest;
-import com.amazonaws.services.ec2.model.SpotMarketOptions;
-import com.amazonaws.services.ec2.model.SpotPlacement;
-import com.amazonaws.services.ec2.model.StartInstancesRequest;
-import com.amazonaws.services.ec2.model.StartInstancesResult;
-import com.amazonaws.services.ec2.model.Subnet;
-import com.amazonaws.services.ec2.model.Tag;
-import com.amazonaws.services.ec2.model.TagSpecification;
+
+import edu.umd.cs.findbugs.annotations.CheckForNull;
 import edu.umd.cs.findbugs.annotations.NonNull;
 import hudson.Extension;
 import hudson.Util;
@@ -89,6 +38,8 @@
 import hudson.plugins.ec2.util.DeviceMappingParser;
 import hudson.plugins.ec2.util.EC2AgentConfig;
 import hudson.plugins.ec2.util.EC2AgentFactory;
+import hudson.plugins.ec2.util.InstanceTypeCompat;
+import hudson.plugins.ec2.util.KeyPair;
 import hudson.plugins.ec2.util.MinimumInstanceChecker;
 import hudson.plugins.ec2.util.MinimumNumberOfInstancesTimeRangeConfig;
 import hudson.security.Permission;
@@ -98,25 +49,9 @@
 import hudson.util.FormValidation;
 import hudson.util.ListBoxModel;
 import hudson.util.Secret;
-import jenkins.model.Jenkins;
-import jenkins.model.JenkinsLocationConfiguration;
-import jenkins.slaves.iterators.api.NodeIterator;
-import org.apache.commons.lang.StringUtils;
-import org.kohsuke.accmod.Restricted;
-import org.kohsuke.accmod.restrictions.NoExternalUse;
-import org.kohsuke.stapler.DataBoundConstructor;
-import org.kohsuke.stapler.DataBoundSetter;
-import org.kohsuke.stapler.QueryParameter;
-import org.kohsuke.stapler.Stapler;
-import org.kohsuke.stapler.interceptor.RequirePOST;
-
-import edu.umd.cs.findbugs.annotations.CheckForNull;
-import org.kohsuke.stapler.verb.POST;
-
-import javax.servlet.ServletException;
+import jakarta.servlet.ServletException;
 import java.io.IOException;
 import java.io.PrintStream;
-import java.net.URL;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -136,6 +71,73 @@
 import java.util.logging.Logger;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
+import jenkins.model.Jenkins;
+import jenkins.model.JenkinsLocationConfiguration;
+import jenkins.slaves.iterators.api.NodeIterator;
+import org.apache.commons.lang.StringUtils;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.DataBoundSetter;
+import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.Stapler;
+import org.kohsuke.stapler.interceptor.RequirePOST;
+import org.kohsuke.stapler.verb.POST;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.core.exception.SdkException;
+import software.amazon.awssdk.services.ec2.Ec2Client;
+import software.amazon.awssdk.services.ec2.model.BlockDeviceMapping;
+import software.amazon.awssdk.services.ec2.model.CancelSpotInstanceRequestsRequest;
+import software.amazon.awssdk.services.ec2.model.CreateTagsRequest;
+import software.amazon.awssdk.services.ec2.model.CreditSpecificationRequest;
+import software.amazon.awssdk.services.ec2.model.DescribeImagesRequest;
+import software.amazon.awssdk.services.ec2.model.DescribeInstanceTypesRequest;
+import software.amazon.awssdk.services.ec2.model.DescribeInstanceTypesResponse;
+import software.amazon.awssdk.services.ec2.model.DescribeInstancesRequest;
+import software.amazon.awssdk.services.ec2.model.DescribeInstancesResponse;
+import software.amazon.awssdk.services.ec2.model.DescribeSecurityGroupsRequest;
+import software.amazon.awssdk.services.ec2.model.DescribeSecurityGroupsResponse;
+import software.amazon.awssdk.services.ec2.model.DescribeSpotInstanceRequestsRequest;
+import software.amazon.awssdk.services.ec2.model.DescribeSubnetsRequest;
+import software.amazon.awssdk.services.ec2.model.DescribeSubnetsResponse;
+import software.amazon.awssdk.services.ec2.model.DeviceType;
+import software.amazon.awssdk.services.ec2.model.EbsBlockDevice;
+import software.amazon.awssdk.services.ec2.model.Ec2Exception;
+import software.amazon.awssdk.services.ec2.model.EnclaveOptionsRequest;
+import software.amazon.awssdk.services.ec2.model.Filter;
+import software.amazon.awssdk.services.ec2.model.HttpTokensState;
+import software.amazon.awssdk.services.ec2.model.IamInstanceProfileSpecification;
+import software.amazon.awssdk.services.ec2.model.Image;
+import software.amazon.awssdk.services.ec2.model.Instance;
+import software.amazon.awssdk.services.ec2.model.InstanceMarketOptionsRequest;
+import software.amazon.awssdk.services.ec2.model.InstanceMetadataEndpointState;
+import software.amazon.awssdk.services.ec2.model.InstanceMetadataOptionsRequest;
+import software.amazon.awssdk.services.ec2.model.InstanceNetworkInterfaceSpecification;
+import software.amazon.awssdk.services.ec2.model.InstanceStateName;
+import software.amazon.awssdk.services.ec2.model.InstanceType;
+import software.amazon.awssdk.services.ec2.model.InstanceTypeHypervisor;
+import software.amazon.awssdk.services.ec2.model.InstanceTypeInfo;
+import software.amazon.awssdk.services.ec2.model.MarketType;
+import software.amazon.awssdk.services.ec2.model.NitroEnclavesSupport;
+import software.amazon.awssdk.services.ec2.model.Placement;
+import software.amazon.awssdk.services.ec2.model.RequestSpotInstancesRequest;
+import software.amazon.awssdk.services.ec2.model.RequestSpotInstancesResponse;
+import software.amazon.awssdk.services.ec2.model.RequestSpotLaunchSpecification;
+import software.amazon.awssdk.services.ec2.model.Reservation;
+import software.amazon.awssdk.services.ec2.model.ResourceType;
+import software.amazon.awssdk.services.ec2.model.RunInstancesMonitoringEnabled;
+import software.amazon.awssdk.services.ec2.model.RunInstancesRequest;
+import software.amazon.awssdk.services.ec2.model.SecurityGroup;
+import software.amazon.awssdk.services.ec2.model.ShutdownBehavior;
+import software.amazon.awssdk.services.ec2.model.SpotInstanceRequest;
+import software.amazon.awssdk.services.ec2.model.SpotMarketOptions;
+import software.amazon.awssdk.services.ec2.model.SpotPlacement;
+import software.amazon.awssdk.services.ec2.model.StartInstancesRequest;
+import software.amazon.awssdk.services.ec2.model.StartInstancesResponse;
+import software.amazon.awssdk.services.ec2.model.Subnet;
+import software.amazon.awssdk.services.ec2.model.Tag;
+import software.amazon.awssdk.services.ec2.model.TagSpecification;
 
 /**
  * Template of {@link EC2AbstractSlave} to launch.
@@ -159,7 +161,7 @@ public class SlaveTemplate implements Describable {
 
     public final String remoteFS;
 
-    public final InstanceType type;
+    public String type;
 
     public final boolean ebsOptimized;
 
@@ -189,6 +191,8 @@ public class SlaveTemplate implements Describable {
 
     public final String idleTerminationMinutes;
 
+    private boolean terminateIdleDuringShutdown;
+
     public final String iamInstanceProfile;
 
     public final boolean deleteRootOnTermination;
@@ -213,7 +217,10 @@ public class SlaveTemplate implements Describable {
 
     public HostKeyVerificationStrategyEnum hostKeyVerificationStrategy;
 
-    public final boolean associatePublicIp;
+    public AssociateIPStrategy associateIPStrategy;
+
+    @Deprecated
+    public transient boolean associatePublicIp;
 
     protected transient EC2Cloud parent;
 
@@ -225,6 +232,8 @@ public class SlaveTemplate implements Describable {
 
     public int maxTotalUses;
 
+    private boolean avoidUsingOrphanedNodes;
+
     private /* lazily initialized */ DescribableList, NodePropertyDescriptor> nodeProperties;
 
     public int nextSubnet;
@@ -243,9 +252,11 @@ public class SlaveTemplate implements Describable {
 
     private Integer metadataHopsLimit;
 
-    private transient/* almost final */ Set labelSet;
+    private Boolean enclaveEnabled;
+
+    private transient /* almost final */ Set labelSet;
 
-    private transient/* almost final */Set securityGroupSet;
+    private transient /* almost final */ Set securityGroupSet;
 
     /* FIXME: Ideally these would be List, but Jenkins currently
      * doesn't offer a usable way to represent those in forms. Instead
@@ -287,23 +298,62 @@ public class SlaveTemplate implements Describable {
     public transient boolean useDedicatedTenancy;
 
     @DataBoundConstructor
-    public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
-                         InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
-                         String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String javaPath, String jvmopts,
-                         boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, int minimumNumberOfInstances,
-                         int minimumNumberOfSpareInstances, String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination,
-                         boolean useEphemeralDevices, String launchTimeoutStr, boolean associatePublicIp,
-                         String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring,
-                         boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses,
-                         List> nodeProperties, HostKeyVerificationStrategyEnum hostKeyVerificationStrategy, Tenancy tenancy, EbsEncryptRootVolume ebsEncryptRootVolume,
-                         Boolean metadataEndpointEnabled, Boolean metadataTokensRequired, Integer metadataHopsLimit, Boolean metadataSupported) {
-
-        if(StringUtils.isNotBlank(remoteAdmin) || StringUtils.isNotBlank(jvmopts) || StringUtils.isNotBlank(tmpDir)){
-            LOGGER.log(Level.FINE, "As remoteAdmin, jvmopts or tmpDir is not blank, we must ensure the user has ADMINISTER rights.");
+    public SlaveTemplate(
+            String ami,
+            String zone,
+            SpotConfiguration spotConfig,
+            String securityGroups,
+            String remoteFS,
+            String type,
+            boolean ebsOptimized,
+            String labelString,
+            Node.Mode mode,
+            String description,
+            String initScript,
+            String tmpDir,
+            String userData,
+            String numExecutors,
+            String remoteAdmin,
+            AMITypeData amiType,
+            String javaPath,
+            String jvmopts,
+            boolean stopOnTerminate,
+            String subnetId,
+            List tags,
+            String idleTerminationMinutes,
+            int minimumNumberOfInstances,
+            int minimumNumberOfSpareInstances,
+            String instanceCapStr,
+            String iamInstanceProfile,
+            boolean deleteRootOnTermination,
+            boolean useEphemeralDevices,
+            String launchTimeoutStr,
+            AssociateIPStrategy associateIPStrategy,
+            String customDeviceMapping,
+            boolean connectBySSHProcess,
+            boolean monitoring,
+            boolean t2Unlimited,
+            ConnectionStrategy connectionStrategy,
+            int maxTotalUses,
+            List> nodeProperties,
+            HostKeyVerificationStrategyEnum hostKeyVerificationStrategy,
+            Tenancy tenancy,
+            EbsEncryptRootVolume ebsEncryptRootVolume,
+            Boolean metadataEndpointEnabled,
+            Boolean metadataTokensRequired,
+            Integer metadataHopsLimit,
+            Boolean metadataSupported,
+            Boolean enclaveEnabled) {
+
+        if (StringUtils.isNotBlank(remoteAdmin) || StringUtils.isNotBlank(jvmopts) || StringUtils.isNotBlank(tmpDir)) {
+            LOGGER.log(
+                    Level.FINE,
+                    "As remoteAdmin, jvmopts or tmpDir is not blank, we must ensure the user has ADMINISTER rights.");
             // Can be null during tests
             Jenkins j = Jenkins.getInstanceOrNull();
-            if (j != null)
+            if (j != null) {
                 j.checkPermission(Jenkins.ADMINISTER);
+            }
         }
 
         this.ami = ami;
@@ -312,7 +362,8 @@ public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, Stri
         this.securityGroups = securityGroups;
         this.remoteFS = remoteFS;
         this.amiType = amiType;
-        this.type = type;
+        this.type =
+                type != null && !type.isEmpty() ? InstanceTypeCompat.of(type).toString() : null;
         this.ebsOptimized = ebsOptimized;
         this.labels = Util.fixNull(labelString);
         this.mode = mode != null ? mode : Node.Mode.NORMAL;
@@ -326,7 +377,7 @@ public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, Stri
         if (StringUtils.isNotBlank(javaPath)) {
             this.javaPath = javaPath;
         } else {
-            this.javaPath = DEFAULT_JAVA_PATH;
+            this.javaPath = EC2AbstractSlave.DEFAULT_JAVA_PATH;
         }
 
         this.jvmopts = jvmopts;
@@ -334,7 +385,6 @@ public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, Stri
         this.subnetId = subnetId;
         this.tags = tags;
         this.idleTerminationMinutes = idleTerminationMinutes;
-        this.associatePublicIp = associatePublicIp;
         this.connectionStrategy = connectionStrategy == null ? ConnectionStrategy.PRIVATE_IP : connectionStrategy;
         this.useDedicatedTenancy = tenancy == Tenancy.Dedicated;
         this.connectBySSHProcess = connectBySSHProcess;
@@ -367,243 +417,1208 @@ public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, Stri
         this.customDeviceMapping = customDeviceMapping;
         this.t2Unlimited = t2Unlimited;
 
-        this.hostKeyVerificationStrategy = hostKeyVerificationStrategy != null ? hostKeyVerificationStrategy : HostKeyVerificationStrategyEnum.CHECK_NEW_SOFT;
+        this.hostKeyVerificationStrategy = hostKeyVerificationStrategy != null
+                ? hostKeyVerificationStrategy
+                : HostKeyVerificationStrategyEnum.CHECK_NEW_SOFT;
         this.tenancy = tenancy != null ? tenancy : Tenancy.Default;
         this.ebsEncryptRootVolume = ebsEncryptRootVolume != null ? ebsEncryptRootVolume : EbsEncryptRootVolume.DEFAULT;
-        this.metadataSupported = metadataSupported != null ? metadataSupported : DEFAULT_METADATA_SUPPORTED;
-        this.metadataEndpointEnabled = metadataEndpointEnabled != null ? metadataEndpointEnabled : DEFAULT_METADATA_ENDPOINT_ENABLED;
-        this.metadataTokensRequired = metadataTokensRequired != null ? metadataTokensRequired : DEFAULT_METADATA_TOKENS_REQUIRED;
-        this.metadataHopsLimit = metadataHopsLimit != null ? metadataHopsLimit : DEFAULT_METADATA_HOPS_LIMIT;
+        this.metadataSupported =
+                metadataSupported != null ? metadataSupported : EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED;
+        this.metadataEndpointEnabled = metadataEndpointEnabled != null
+                ? metadataEndpointEnabled
+                : EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED;
+        this.metadataTokensRequired = metadataTokensRequired != null
+                ? metadataTokensRequired
+                : EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED;
+        this.metadataHopsLimit =
+                metadataHopsLimit != null ? metadataHopsLimit : EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT;
+        this.enclaveEnabled = enclaveEnabled != null ? enclaveEnabled : EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED;
+        this.associateIPStrategy = associateIPStrategy != null ? associateIPStrategy : AssociateIPStrategy.DEFAULT;
+
         readResolve(); // initialize
     }
 
     @Deprecated
-    public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
-                         InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
-                         String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String javaPath, String jvmopts,
-                         boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, int minimumNumberOfInstances,
-                         int minimumNumberOfSpareInstances, String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination,
-                         boolean useEphemeralDevices, String launchTimeoutStr, boolean associatePublicIp,
-                         String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring,
-                         boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses,
-                         List> nodeProperties, HostKeyVerificationStrategyEnum hostKeyVerificationStrategy, Tenancy tenancy, EbsEncryptRootVolume ebsEncryptRootVolume,
-                         Boolean metadataSupported, Boolean metadataEndpointEnabled, Boolean metadataTokensRequired, Integer metadataHopsLimit) {
-        this(ami, zone, spotConfig, securityGroups, remoteFS,
-               type, ebsOptimized, labelString, mode, description, initScript,
-               tmpDir, userData, numExecutors, remoteAdmin, amiType, DEFAULT_JAVA_PATH, jvmopts,
-               stopOnTerminate, subnetId, tags, idleTerminationMinutes, minimumNumberOfInstances,
-               minimumNumberOfSpareInstances, instanceCapStr, iamInstanceProfile, deleteRootOnTermination,
-               useEphemeralDevices, launchTimeoutStr, associatePublicIp,
-               customDeviceMapping, connectBySSHProcess, monitoring,
-               t2Unlimited, connectionStrategy, maxTotalUses,
-               nodeProperties, hostKeyVerificationStrategy, tenancy, null, metadataEndpointEnabled,
-               metadataTokensRequired, metadataHopsLimit, DEFAULT_METADATA_SUPPORTED);
+    public SlaveTemplate(
+            String ami,
+            String zone,
+            SpotConfiguration spotConfig,
+            String securityGroups,
+            String remoteFS,
+            String type,
+            boolean ebsOptimized,
+            String labelString,
+            Node.Mode mode,
+            String description,
+            String initScript,
+            String tmpDir,
+            String userData,
+            String numExecutors,
+            String remoteAdmin,
+            AMITypeData amiType,
+            String javaPath,
+            String jvmopts,
+            boolean stopOnTerminate,
+            String subnetId,
+            List tags,
+            String idleTerminationMinutes,
+            int minimumNumberOfInstances,
+            int minimumNumberOfSpareInstances,
+            String instanceCapStr,
+            String iamInstanceProfile,
+            boolean deleteRootOnTermination,
+            boolean useEphemeralDevices,
+            String launchTimeoutStr,
+            boolean associatePublicIp,
+            String customDeviceMapping,
+            boolean connectBySSHProcess,
+            boolean monitoring,
+            boolean t2Unlimited,
+            ConnectionStrategy connectionStrategy,
+            int maxTotalUses,
+            List> nodeProperties,
+            HostKeyVerificationStrategyEnum hostKeyVerificationStrategy,
+            Tenancy tenancy,
+            EbsEncryptRootVolume ebsEncryptRootVolume,
+            Boolean metadataEndpointEnabled,
+            Boolean metadataTokensRequired,
+            Integer metadataHopsLimit,
+            Boolean metadataSupported,
+            Boolean enclaveEnabled) {
+        this(
+                ami,
+                zone,
+                spotConfig,
+                securityGroups,
+                remoteFS,
+                InstanceType.fromValue(type.toString()).toString(),
+                ebsOptimized,
+                labelString,
+                mode,
+                description,
+                initScript,
+                tmpDir,
+                userData,
+                numExecutors,
+                remoteAdmin,
+                amiType,
+                javaPath,
+                jvmopts,
+                stopOnTerminate,
+                subnetId,
+                tags,
+                idleTerminationMinutes,
+                minimumNumberOfInstances,
+                minimumNumberOfSpareInstances,
+                instanceCapStr,
+                iamInstanceProfile,
+                deleteRootOnTermination,
+                useEphemeralDevices,
+                launchTimeoutStr,
+                AssociateIPStrategy.backwardsCompatible(associatePublicIp),
+                customDeviceMapping,
+                connectBySSHProcess,
+                monitoring,
+                t2Unlimited,
+                connectionStrategy,
+                maxTotalUses,
+                nodeProperties,
+                hostKeyVerificationStrategy,
+                tenancy,
+                ebsEncryptRootVolume,
+                metadataEndpointEnabled,
+                metadataTokensRequired,
+                metadataHopsLimit,
+                metadataSupported,
+                enclaveEnabled);
+    }
+
+    @Deprecated
+    public SlaveTemplate(
+            String ami,
+            String zone,
+            SpotConfiguration spotConfig,
+            String securityGroups,
+            String remoteFS,
+            com.amazonaws.services.ec2.model.InstanceType type,
+            boolean ebsOptimized,
+            String labelString,
+            Node.Mode mode,
+            String description,
+            String initScript,
+            String tmpDir,
+            String userData,
+            String numExecutors,
+            String remoteAdmin,
+            AMITypeData amiType,
+            String javaPath,
+            String jvmopts,
+            boolean stopOnTerminate,
+            String subnetId,
+            List tags,
+            String idleTerminationMinutes,
+            int minimumNumberOfInstances,
+            int minimumNumberOfSpareInstances,
+            String instanceCapStr,
+            String iamInstanceProfile,
+            boolean deleteRootOnTermination,
+            boolean useEphemeralDevices,
+            String launchTimeoutStr,
+            boolean associatePublicIp,
+            String customDeviceMapping,
+            boolean connectBySSHProcess,
+            boolean monitoring,
+            boolean t2Unlimited,
+            ConnectionStrategy connectionStrategy,
+            int maxTotalUses,
+            List> nodeProperties,
+            HostKeyVerificationStrategyEnum hostKeyVerificationStrategy,
+            Tenancy tenancy,
+            EbsEncryptRootVolume ebsEncryptRootVolume,
+            Boolean metadataEndpointEnabled,
+            Boolean metadataTokensRequired,
+            Integer metadataHopsLimit,
+            Boolean metadataSupported) {
+        this(
+                ami,
+                zone,
+                spotConfig,
+                securityGroups,
+                remoteFS,
+                InstanceType.fromValue(type.toString()).toString(),
+                ebsOptimized,
+                labelString,
+                mode,
+                description,
+                initScript,
+                tmpDir,
+                userData,
+                numExecutors,
+                remoteAdmin,
+                amiType,
+                javaPath,
+                jvmopts,
+                stopOnTerminate,
+                subnetId,
+                tags,
+                idleTerminationMinutes,
+                minimumNumberOfInstances,
+                minimumNumberOfSpareInstances,
+                instanceCapStr,
+                iamInstanceProfile,
+                deleteRootOnTermination,
+                useEphemeralDevices,
+                launchTimeoutStr,
+                associatePublicIp,
+                customDeviceMapping,
+                connectBySSHProcess,
+                monitoring,
+                t2Unlimited,
+                connectionStrategy,
+                maxTotalUses,
+                nodeProperties,
+                hostKeyVerificationStrategy,
+                tenancy,
+                ebsEncryptRootVolume,
+                metadataEndpointEnabled,
+                metadataTokensRequired,
+                metadataHopsLimit,
+                metadataSupported,
+                EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED);
     }
 
     @Deprecated
-    public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
-                         InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
-                         String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts,
-                         boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, int minimumNumberOfInstances,
-                         int minimumNumberOfSpareInstances, String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination,
-                         boolean useEphemeralDevices, String launchTimeoutStr, boolean associatePublicIp,
-                         String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring,
-                         boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses,
-                         List> nodeProperties, HostKeyVerificationStrategyEnum hostKeyVerificationStrategy, Tenancy tenancy, EbsEncryptRootVolume ebsEncryptRootVolume) {
-        this(ami, zone, spotConfig, securityGroups, remoteFS,
-                type, ebsOptimized, labelString, mode, description, initScript,
-                tmpDir, userData, numExecutors, remoteAdmin, amiType, DEFAULT_JAVA_PATH, jvmopts,
-                stopOnTerminate, subnetId, tags, idleTerminationMinutes, minimumNumberOfInstances,
-                minimumNumberOfSpareInstances, instanceCapStr, iamInstanceProfile, deleteRootOnTermination,
-                useEphemeralDevices, launchTimeoutStr, associatePublicIp,
-                customDeviceMapping, connectBySSHProcess, monitoring,
-                t2Unlimited, connectionStrategy, maxTotalUses,
-                nodeProperties, hostKeyVerificationStrategy, tenancy, null, DEFAULT_METADATA_ENDPOINT_ENABLED,
-                DEFAULT_METADATA_TOKENS_REQUIRED, DEFAULT_METADATA_HOPS_LIMIT, DEFAULT_METADATA_SUPPORTED);
+    public SlaveTemplate(
+            String ami,
+            String zone,
+            SpotConfiguration spotConfig,
+            String securityGroups,
+            String remoteFS,
+            com.amazonaws.services.ec2.model.InstanceType type,
+            boolean ebsOptimized,
+            String labelString,
+            Node.Mode mode,
+            String description,
+            String initScript,
+            String tmpDir,
+            String userData,
+            String numExecutors,
+            String remoteAdmin,
+            AMITypeData amiType,
+            String javaPath,
+            String jvmopts,
+            boolean stopOnTerminate,
+            String subnetId,
+            List tags,
+            String idleTerminationMinutes,
+            int minimumNumberOfInstances,
+            int minimumNumberOfSpareInstances,
+            String instanceCapStr,
+            String iamInstanceProfile,
+            boolean deleteRootOnTermination,
+            boolean useEphemeralDevices,
+            String launchTimeoutStr,
+            boolean associatePublicIp,
+            String customDeviceMapping,
+            boolean connectBySSHProcess,
+            boolean monitoring,
+            boolean t2Unlimited,
+            ConnectionStrategy connectionStrategy,
+            int maxTotalUses,
+            List> nodeProperties,
+            HostKeyVerificationStrategyEnum hostKeyVerificationStrategy,
+            Tenancy tenancy,
+            EbsEncryptRootVolume ebsEncryptRootVolume,
+            Boolean metadataSupported,
+            Boolean metadataEndpointEnabled,
+            Boolean metadataTokensRequired,
+            Integer metadataHopsLimit) {
+        this(
+                ami,
+                zone,
+                spotConfig,
+                securityGroups,
+                remoteFS,
+                InstanceType.fromValue(type.toString()).toString(),
+                ebsOptimized,
+                labelString,
+                mode,
+                description,
+                initScript,
+                tmpDir,
+                userData,
+                numExecutors,
+                remoteAdmin,
+                amiType,
+                EC2AbstractSlave.DEFAULT_JAVA_PATH,
+                jvmopts,
+                stopOnTerminate,
+                subnetId,
+                tags,
+                idleTerminationMinutes,
+                minimumNumberOfInstances,
+                minimumNumberOfSpareInstances,
+                instanceCapStr,
+                iamInstanceProfile,
+                deleteRootOnTermination,
+                useEphemeralDevices,
+                launchTimeoutStr,
+                associatePublicIp,
+                customDeviceMapping,
+                connectBySSHProcess,
+                monitoring,
+                t2Unlimited,
+                connectionStrategy,
+                maxTotalUses,
+                nodeProperties,
+                hostKeyVerificationStrategy,
+                tenancy,
+                null,
+                metadataEndpointEnabled,
+                metadataTokensRequired,
+                metadataHopsLimit,
+                EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED,
+                EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED);
     }
 
     @Deprecated
-    public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
-                         InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
-                         String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts,
-                         boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, int minimumNumberOfInstances,
-                         int minimumNumberOfSpareInstances, String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination,
-                         boolean useEphemeralDevices, String launchTimeoutStr, boolean associatePublicIp,
-                         String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring,
-                         boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses,
-                         List> nodeProperties, HostKeyVerificationStrategyEnum hostKeyVerificationStrategy, Tenancy tenancy) {
-        this(ami, zone, spotConfig, securityGroups, remoteFS,
-                type, ebsOptimized, labelString, mode, description, initScript,
-                tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts,
-                stopOnTerminate, subnetId, tags, idleTerminationMinutes, minimumNumberOfInstances,
-                minimumNumberOfSpareInstances, instanceCapStr, iamInstanceProfile, deleteRootOnTermination,
-                useEphemeralDevices, launchTimeoutStr, associatePublicIp,
-                customDeviceMapping, connectBySSHProcess, monitoring,
-                t2Unlimited, connectionStrategy, maxTotalUses,
-                nodeProperties, hostKeyVerificationStrategy, tenancy, null);
+    public SlaveTemplate(
+            String ami,
+            String zone,
+            SpotConfiguration spotConfig,
+            String securityGroups,
+            String remoteFS,
+            com.amazonaws.services.ec2.model.InstanceType type,
+            boolean ebsOptimized,
+            String labelString,
+            Node.Mode mode,
+            String description,
+            String initScript,
+            String tmpDir,
+            String userData,
+            String numExecutors,
+            String remoteAdmin,
+            AMITypeData amiType,
+            String jvmopts,
+            boolean stopOnTerminate,
+            String subnetId,
+            List tags,
+            String idleTerminationMinutes,
+            int minimumNumberOfInstances,
+            int minimumNumberOfSpareInstances,
+            String instanceCapStr,
+            String iamInstanceProfile,
+            boolean deleteRootOnTermination,
+            boolean useEphemeralDevices,
+            String launchTimeoutStr,
+            boolean associatePublicIp,
+            String customDeviceMapping,
+            boolean connectBySSHProcess,
+            boolean monitoring,
+            boolean t2Unlimited,
+            ConnectionStrategy connectionStrategy,
+            int maxTotalUses,
+            List> nodeProperties,
+            HostKeyVerificationStrategyEnum hostKeyVerificationStrategy,
+            Tenancy tenancy,
+            EbsEncryptRootVolume ebsEncryptRootVolume) {
+        this(
+                ami,
+                zone,
+                spotConfig,
+                securityGroups,
+                remoteFS,
+                InstanceType.fromValue(type.toString()).toString(),
+                ebsOptimized,
+                labelString,
+                mode,
+                description,
+                initScript,
+                tmpDir,
+                userData,
+                numExecutors,
+                remoteAdmin,
+                amiType,
+                EC2AbstractSlave.DEFAULT_JAVA_PATH,
+                jvmopts,
+                stopOnTerminate,
+                subnetId,
+                tags,
+                idleTerminationMinutes,
+                minimumNumberOfInstances,
+                minimumNumberOfSpareInstances,
+                instanceCapStr,
+                iamInstanceProfile,
+                deleteRootOnTermination,
+                useEphemeralDevices,
+                launchTimeoutStr,
+                associatePublicIp,
+                customDeviceMapping,
+                connectBySSHProcess,
+                monitoring,
+                t2Unlimited,
+                connectionStrategy,
+                maxTotalUses,
+                nodeProperties,
+                hostKeyVerificationStrategy,
+                tenancy,
+                null,
+                EC2AbstractSlave.DEFAULT_METADATA_ENDPOINT_ENABLED,
+                EC2AbstractSlave.DEFAULT_METADATA_TOKENS_REQUIRED,
+                EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT,
+                EC2AbstractSlave.DEFAULT_METADATA_SUPPORTED,
+                EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED);
     }
 
     @Deprecated
-    public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
-                         InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
-                         String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts,
-                         boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, int minimumNumberOfInstances,
-                         int minimumNumberOfSpareInstances, String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination,
-                         boolean useEphemeralDevices, boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp,
-                         String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring,
-                         boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses,
-                         List> nodeProperties, HostKeyVerificationStrategyEnum hostKeyVerificationStrategy) {
-        this(ami, zone, spotConfig, securityGroups, remoteFS,
-                type, ebsOptimized, labelString, mode, description, initScript,
-                tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts,
-                stopOnTerminate, subnetId, tags, idleTerminationMinutes, minimumNumberOfInstances,
-                minimumNumberOfSpareInstances, instanceCapStr, iamInstanceProfile, deleteRootOnTermination,
-                useEphemeralDevices, launchTimeoutStr, associatePublicIp,
-                customDeviceMapping, connectBySSHProcess, monitoring,
-                t2Unlimited, connectionStrategy, maxTotalUses,
-                nodeProperties, hostKeyVerificationStrategy, Tenancy.backwardsCompatible(useDedicatedTenancy));
+    public SlaveTemplate(
+            String ami,
+            String zone,
+            SpotConfiguration spotConfig,
+            String securityGroups,
+            String remoteFS,
+            com.amazonaws.services.ec2.model.InstanceType type,
+            boolean ebsOptimized,
+            String labelString,
+            Node.Mode mode,
+            String description,
+            String initScript,
+            String tmpDir,
+            String userData,
+            String numExecutors,
+            String remoteAdmin,
+            AMITypeData amiType,
+            String jvmopts,
+            boolean stopOnTerminate,
+            String subnetId,
+            List tags,
+            String idleTerminationMinutes,
+            int minimumNumberOfInstances,
+            int minimumNumberOfSpareInstances,
+            String instanceCapStr,
+            String iamInstanceProfile,
+            boolean deleteRootOnTermination,
+            boolean useEphemeralDevices,
+            String launchTimeoutStr,
+            boolean associatePublicIp,
+            String customDeviceMapping,
+            boolean connectBySSHProcess,
+            boolean monitoring,
+            boolean t2Unlimited,
+            ConnectionStrategy connectionStrategy,
+            int maxTotalUses,
+            List> nodeProperties,
+            HostKeyVerificationStrategyEnum hostKeyVerificationStrategy,
+            Tenancy tenancy) {
+        this(
+                ami,
+                zone,
+                spotConfig,
+                securityGroups,
+                remoteFS,
+                type,
+                ebsOptimized,
+                labelString,
+                mode,
+                description,
+                initScript,
+                tmpDir,
+                userData,
+                numExecutors,
+                remoteAdmin,
+                amiType,
+                jvmopts,
+                stopOnTerminate,
+                subnetId,
+                tags,
+                idleTerminationMinutes,
+                minimumNumberOfInstances,
+                minimumNumberOfSpareInstances,
+                instanceCapStr,
+                iamInstanceProfile,
+                deleteRootOnTermination,
+                useEphemeralDevices,
+                launchTimeoutStr,
+                associatePublicIp,
+                customDeviceMapping,
+                connectBySSHProcess,
+                monitoring,
+                t2Unlimited,
+                connectionStrategy,
+                maxTotalUses,
+                nodeProperties,
+                hostKeyVerificationStrategy,
+                tenancy,
+                null);
     }
 
     @Deprecated
-    public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
-            InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
-            String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts,
-            boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, int minimumNumberOfInstances,
-            int minimumNumberOfSpareInstances, String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination,
-            boolean useEphemeralDevices, boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp,
-            String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring,
-            boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses,
+    public SlaveTemplate(
+            String ami,
+            String zone,
+            SpotConfiguration spotConfig,
+            String securityGroups,
+            String remoteFS,
+            com.amazonaws.services.ec2.model.InstanceType type,
+            boolean ebsOptimized,
+            String labelString,
+            Node.Mode mode,
+            String description,
+            String initScript,
+            String tmpDir,
+            String userData,
+            String numExecutors,
+            String remoteAdmin,
+            AMITypeData amiType,
+            String jvmopts,
+            boolean stopOnTerminate,
+            String subnetId,
+            List tags,
+            String idleTerminationMinutes,
+            int minimumNumberOfInstances,
+            int minimumNumberOfSpareInstances,
+            String instanceCapStr,
+            String iamInstanceProfile,
+            boolean deleteRootOnTermination,
+            boolean useEphemeralDevices,
+            boolean useDedicatedTenancy,
+            String launchTimeoutStr,
+            boolean associatePublicIp,
+            String customDeviceMapping,
+            boolean connectBySSHProcess,
+            boolean monitoring,
+            boolean t2Unlimited,
+            ConnectionStrategy connectionStrategy,
+            int maxTotalUses,
+            List> nodeProperties,
+            HostKeyVerificationStrategyEnum hostKeyVerificationStrategy) {
+        this(
+                ami,
+                zone,
+                spotConfig,
+                securityGroups,
+                remoteFS,
+                type,
+                ebsOptimized,
+                labelString,
+                mode,
+                description,
+                initScript,
+                tmpDir,
+                userData,
+                numExecutors,
+                remoteAdmin,
+                amiType,
+                jvmopts,
+                stopOnTerminate,
+                subnetId,
+                tags,
+                idleTerminationMinutes,
+                minimumNumberOfInstances,
+                minimumNumberOfSpareInstances,
+                instanceCapStr,
+                iamInstanceProfile,
+                deleteRootOnTermination,
+                useEphemeralDevices,
+                launchTimeoutStr,
+                associatePublicIp,
+                customDeviceMapping,
+                connectBySSHProcess,
+                monitoring,
+                t2Unlimited,
+                connectionStrategy,
+                maxTotalUses,
+                nodeProperties,
+                hostKeyVerificationStrategy,
+                Tenancy.backwardsCompatible(useDedicatedTenancy));
+    }
+
+    @Deprecated
+    public SlaveTemplate(
+            String ami,
+            String zone,
+            SpotConfiguration spotConfig,
+            String securityGroups,
+            String remoteFS,
+            com.amazonaws.services.ec2.model.InstanceType type,
+            boolean ebsOptimized,
+            String labelString,
+            Node.Mode mode,
+            String description,
+            String initScript,
+            String tmpDir,
+            String userData,
+            String numExecutors,
+            String remoteAdmin,
+            AMITypeData amiType,
+            String jvmopts,
+            boolean stopOnTerminate,
+            String subnetId,
+            List tags,
+            String idleTerminationMinutes,
+            int minimumNumberOfInstances,
+            int minimumNumberOfSpareInstances,
+            String instanceCapStr,
+            String iamInstanceProfile,
+            boolean deleteRootOnTermination,
+            boolean useEphemeralDevices,
+            boolean useDedicatedTenancy,
+            String launchTimeoutStr,
+            boolean associatePublicIp,
+            String customDeviceMapping,
+            boolean connectBySSHProcess,
+            boolean monitoring,
+            boolean t2Unlimited,
+            ConnectionStrategy connectionStrategy,
+            int maxTotalUses,
             List> nodeProperties) {
-        this(ami, zone, spotConfig, securityGroups, remoteFS,
-                type, ebsOptimized, labelString, mode, description, initScript,
-                tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts,
-                stopOnTerminate, subnetId, tags, idleTerminationMinutes, minimumNumberOfInstances,
-                minimumNumberOfSpareInstances, instanceCapStr, iamInstanceProfile, deleteRootOnTermination,
-                useEphemeralDevices, useDedicatedTenancy, launchTimeoutStr, associatePublicIp,
-                customDeviceMapping, connectBySSHProcess, monitoring,
-                t2Unlimited, connectionStrategy, maxTotalUses,
-                nodeProperties, null);
+        this(
+                ami,
+                zone,
+                spotConfig,
+                securityGroups,
+                remoteFS,
+                type,
+                ebsOptimized,
+                labelString,
+                mode,
+                description,
+                initScript,
+                tmpDir,
+                userData,
+                numExecutors,
+                remoteAdmin,
+                amiType,
+                jvmopts,
+                stopOnTerminate,
+                subnetId,
+                tags,
+                idleTerminationMinutes,
+                minimumNumberOfInstances,
+                minimumNumberOfSpareInstances,
+                instanceCapStr,
+                iamInstanceProfile,
+                deleteRootOnTermination,
+                useEphemeralDevices,
+                useDedicatedTenancy,
+                launchTimeoutStr,
+                associatePublicIp,
+                customDeviceMapping,
+                connectBySSHProcess,
+                monitoring,
+                t2Unlimited,
+                connectionStrategy,
+                maxTotalUses,
+                nodeProperties,
+                null);
     }
 
     @Deprecated
-    public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
-            InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
-            String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts,
-            boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, int minimumNumberOfInstances,
-            String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination,
-            boolean useEphemeralDevices, boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp,
-            String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring,
-            boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses,List> nodeProperties ) {
-        this(ami, zone, spotConfig, securityGroups, remoteFS, type, ebsOptimized, labelString, mode, description, initScript,
-                tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, stopOnTerminate, subnetId, tags,
-                idleTerminationMinutes, minimumNumberOfInstances, 0, instanceCapStr, iamInstanceProfile, deleteRootOnTermination,
-                useEphemeralDevices, useDedicatedTenancy, launchTimeoutStr, associatePublicIp, customDeviceMapping,
-                connectBySSHProcess, monitoring, t2Unlimited, connectionStrategy, maxTotalUses, nodeProperties);
+    public SlaveTemplate(
+            String ami,
+            String zone,
+            SpotConfiguration spotConfig,
+            String securityGroups,
+            String remoteFS,
+            com.amazonaws.services.ec2.model.InstanceType type,
+            boolean ebsOptimized,
+            String labelString,
+            Node.Mode mode,
+            String description,
+            String initScript,
+            String tmpDir,
+            String userData,
+            String numExecutors,
+            String remoteAdmin,
+            AMITypeData amiType,
+            String jvmopts,
+            boolean stopOnTerminate,
+            String subnetId,
+            List tags,
+            String idleTerminationMinutes,
+            int minimumNumberOfInstances,
+            String instanceCapStr,
+            String iamInstanceProfile,
+            boolean deleteRootOnTermination,
+            boolean useEphemeralDevices,
+            boolean useDedicatedTenancy,
+            String launchTimeoutStr,
+            boolean associatePublicIp,
+            String customDeviceMapping,
+            boolean connectBySSHProcess,
+            boolean monitoring,
+            boolean t2Unlimited,
+            ConnectionStrategy connectionStrategy,
+            int maxTotalUses,
+            List> nodeProperties) {
+        this(
+                ami,
+                zone,
+                spotConfig,
+                securityGroups,
+                remoteFS,
+                type,
+                ebsOptimized,
+                labelString,
+                mode,
+                description,
+                initScript,
+                tmpDir,
+                userData,
+                numExecutors,
+                remoteAdmin,
+                amiType,
+                jvmopts,
+                stopOnTerminate,
+                subnetId,
+                tags,
+                idleTerminationMinutes,
+                minimumNumberOfInstances,
+                0,
+                instanceCapStr,
+                iamInstanceProfile,
+                deleteRootOnTermination,
+                useEphemeralDevices,
+                useDedicatedTenancy,
+                launchTimeoutStr,
+                associatePublicIp,
+                customDeviceMapping,
+                connectBySSHProcess,
+                monitoring,
+                t2Unlimited,
+                connectionStrategy,
+                maxTotalUses,
+                nodeProperties);
     }
 
     @Deprecated
-    public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
-            InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
-            String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts,
-            boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes, int minimumNumberOfInstances,
-            String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination,
-            boolean useEphemeralDevices, boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp,
-            String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring,
-            boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses) {
-        this(ami, zone, spotConfig, securityGroups, remoteFS, type, ebsOptimized, labelString, mode, description, initScript,
-                tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, stopOnTerminate, subnetId, tags,
-                idleTerminationMinutes, minimumNumberOfInstances, instanceCapStr, iamInstanceProfile, deleteRootOnTermination,
-                useEphemeralDevices, useDedicatedTenancy, launchTimeoutStr, associatePublicIp, customDeviceMapping,
-                connectBySSHProcess, monitoring, t2Unlimited, connectionStrategy, maxTotalUses, Collections.emptyList());
+    public SlaveTemplate(
+            String ami,
+            String zone,
+            SpotConfiguration spotConfig,
+            String securityGroups,
+            String remoteFS,
+            com.amazonaws.services.ec2.model.InstanceType type,
+            boolean ebsOptimized,
+            String labelString,
+            Node.Mode mode,
+            String description,
+            String initScript,
+            String tmpDir,
+            String userData,
+            String numExecutors,
+            String remoteAdmin,
+            AMITypeData amiType,
+            String jvmopts,
+            boolean stopOnTerminate,
+            String subnetId,
+            List tags,
+            String idleTerminationMinutes,
+            int minimumNumberOfInstances,
+            String instanceCapStr,
+            String iamInstanceProfile,
+            boolean deleteRootOnTermination,
+            boolean useEphemeralDevices,
+            boolean useDedicatedTenancy,
+            String launchTimeoutStr,
+            boolean associatePublicIp,
+            String customDeviceMapping,
+            boolean connectBySSHProcess,
+            boolean monitoring,
+            boolean t2Unlimited,
+            ConnectionStrategy connectionStrategy,
+            int maxTotalUses) {
+        this(
+                ami,
+                zone,
+                spotConfig,
+                securityGroups,
+                remoteFS,
+                type,
+                ebsOptimized,
+                labelString,
+                mode,
+                description,
+                initScript,
+                tmpDir,
+                userData,
+                numExecutors,
+                remoteAdmin,
+                amiType,
+                jvmopts,
+                stopOnTerminate,
+                subnetId,
+                tags,
+                idleTerminationMinutes,
+                minimumNumberOfInstances,
+                instanceCapStr,
+                iamInstanceProfile,
+                deleteRootOnTermination,
+                useEphemeralDevices,
+                useDedicatedTenancy,
+                launchTimeoutStr,
+                associatePublicIp,
+                customDeviceMapping,
+                connectBySSHProcess,
+                monitoring,
+                t2Unlimited,
+                connectionStrategy,
+                maxTotalUses,
+                Collections.emptyList());
     }
 
     @Deprecated
-    public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
-                         InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
-                         String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts,
-                         boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes,
-                         String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination,
-                         boolean useEphemeralDevices, boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp,
-                         String customDeviceMapping, boolean connectBySSHProcess, boolean monitoring,
-                         boolean t2Unlimited, ConnectionStrategy connectionStrategy, int maxTotalUses) {
-        this(ami, zone, spotConfig, securityGroups, remoteFS, type, ebsOptimized, labelString, mode, description, initScript,
-          tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, stopOnTerminate, subnetId, tags,
-          idleTerminationMinutes, 0, instanceCapStr, iamInstanceProfile, deleteRootOnTermination, useEphemeralDevices,
-          useDedicatedTenancy, launchTimeoutStr, associatePublicIp, customDeviceMapping, connectBySSHProcess,
-          monitoring, t2Unlimited, connectionStrategy, maxTotalUses);
+    public SlaveTemplate(
+            String ami,
+            String zone,
+            SpotConfiguration spotConfig,
+            String securityGroups,
+            String remoteFS,
+            com.amazonaws.services.ec2.model.InstanceType type,
+            boolean ebsOptimized,
+            String labelString,
+            Node.Mode mode,
+            String description,
+            String initScript,
+            String tmpDir,
+            String userData,
+            String numExecutors,
+            String remoteAdmin,
+            AMITypeData amiType,
+            String jvmopts,
+            boolean stopOnTerminate,
+            String subnetId,
+            List tags,
+            String idleTerminationMinutes,
+            String instanceCapStr,
+            String iamInstanceProfile,
+            boolean deleteRootOnTermination,
+            boolean useEphemeralDevices,
+            boolean useDedicatedTenancy,
+            String launchTimeoutStr,
+            boolean associatePublicIp,
+            String customDeviceMapping,
+            boolean connectBySSHProcess,
+            boolean monitoring,
+            boolean t2Unlimited,
+            ConnectionStrategy connectionStrategy,
+            int maxTotalUses) {
+        this(
+                ami,
+                zone,
+                spotConfig,
+                securityGroups,
+                remoteFS,
+                type,
+                ebsOptimized,
+                labelString,
+                mode,
+                description,
+                initScript,
+                tmpDir,
+                userData,
+                numExecutors,
+                remoteAdmin,
+                amiType,
+                jvmopts,
+                stopOnTerminate,
+                subnetId,
+                tags,
+                idleTerminationMinutes,
+                0,
+                instanceCapStr,
+                iamInstanceProfile,
+                deleteRootOnTermination,
+                useEphemeralDevices,
+                useDedicatedTenancy,
+                launchTimeoutStr,
+                associatePublicIp,
+                customDeviceMapping,
+                connectBySSHProcess,
+                monitoring,
+                t2Unlimited,
+                connectionStrategy,
+                maxTotalUses);
     }
 
     @Deprecated
-    public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
-            InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
-            String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts,
-            boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes,
-            boolean usePrivateDnsName, String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination,
-            boolean useEphemeralDevices, boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp,
-            String customDeviceMapping, boolean connectBySSHProcess, boolean connectUsingPublicIp, boolean monitoring,
+    public SlaveTemplate(
+            String ami,
+            String zone,
+            SpotConfiguration spotConfig,
+            String securityGroups,
+            String remoteFS,
+            com.amazonaws.services.ec2.model.InstanceType type,
+            boolean ebsOptimized,
+            String labelString,
+            Node.Mode mode,
+            String description,
+            String initScript,
+            String tmpDir,
+            String userData,
+            String numExecutors,
+            String remoteAdmin,
+            AMITypeData amiType,
+            String jvmopts,
+            boolean stopOnTerminate,
+            String subnetId,
+            List tags,
+            String idleTerminationMinutes,
+            boolean usePrivateDnsName,
+            String instanceCapStr,
+            String iamInstanceProfile,
+            boolean deleteRootOnTermination,
+            boolean useEphemeralDevices,
+            boolean useDedicatedTenancy,
+            String launchTimeoutStr,
+            boolean associatePublicIp,
+            String customDeviceMapping,
+            boolean connectBySSHProcess,
+            boolean connectUsingPublicIp,
+            boolean monitoring,
             boolean t2Unlimited) {
-        this(ami, zone, spotConfig, securityGroups, remoteFS, type, ebsOptimized, labelString, mode, description, initScript,
-                tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, stopOnTerminate, subnetId, tags,
-                idleTerminationMinutes, instanceCapStr, iamInstanceProfile, deleteRootOnTermination, useEphemeralDevices,
-                useDedicatedTenancy, launchTimeoutStr, associatePublicIp, customDeviceMapping, connectBySSHProcess,
-                monitoring, t2Unlimited, ConnectionStrategy.backwardsCompatible(usePrivateDnsName, connectUsingPublicIp, associatePublicIp), -1);
+        this(
+                ami,
+                zone,
+                spotConfig,
+                securityGroups,
+                remoteFS,
+                type,
+                ebsOptimized,
+                labelString,
+                mode,
+                description,
+                initScript,
+                tmpDir,
+                userData,
+                numExecutors,
+                remoteAdmin,
+                amiType,
+                jvmopts,
+                stopOnTerminate,
+                subnetId,
+                tags,
+                idleTerminationMinutes,
+                instanceCapStr,
+                iamInstanceProfile,
+                deleteRootOnTermination,
+                useEphemeralDevices,
+                useDedicatedTenancy,
+                launchTimeoutStr,
+                associatePublicIp,
+                customDeviceMapping,
+                connectBySSHProcess,
+                monitoring,
+                t2Unlimited,
+                ConnectionStrategy.backwardsCompatible(usePrivateDnsName, connectUsingPublicIp, associatePublicIp),
+                -1);
     }
 
     @Deprecated
-    public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
-            InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
-            String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts,
-            boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes,
-            boolean usePrivateDnsName, String instanceCapStr, String iamInstanceProfile, boolean deleteRootOnTermination,
-            boolean useEphemeralDevices, boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp,
-            String customDeviceMapping, boolean connectBySSHProcess, boolean connectUsingPublicIp) {
-        this(ami, zone, spotConfig, securityGroups, remoteFS, type, ebsOptimized, labelString, mode, description, initScript,
-                tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, stopOnTerminate, subnetId, tags,
-                idleTerminationMinutes, usePrivateDnsName, instanceCapStr, iamInstanceProfile, deleteRootOnTermination, useEphemeralDevices,
-                useDedicatedTenancy, launchTimeoutStr, associatePublicIp, customDeviceMapping, connectBySSHProcess,
-                connectUsingPublicIp, false, false);
+    public SlaveTemplate(
+            String ami,
+            String zone,
+            SpotConfiguration spotConfig,
+            String securityGroups,
+            String remoteFS,
+            com.amazonaws.services.ec2.model.InstanceType type,
+            boolean ebsOptimized,
+            String labelString,
+            Node.Mode mode,
+            String description,
+            String initScript,
+            String tmpDir,
+            String userData,
+            String numExecutors,
+            String remoteAdmin,
+            AMITypeData amiType,
+            String jvmopts,
+            boolean stopOnTerminate,
+            String subnetId,
+            List tags,
+            String idleTerminationMinutes,
+            boolean usePrivateDnsName,
+            String instanceCapStr,
+            String iamInstanceProfile,
+            boolean deleteRootOnTermination,
+            boolean useEphemeralDevices,
+            boolean useDedicatedTenancy,
+            String launchTimeoutStr,
+            boolean associatePublicIp,
+            String customDeviceMapping,
+            boolean connectBySSHProcess,
+            boolean connectUsingPublicIp) {
+        this(
+                ami,
+                zone,
+                spotConfig,
+                securityGroups,
+                remoteFS,
+                type,
+                ebsOptimized,
+                labelString,
+                mode,
+                description,
+                initScript,
+                tmpDir,
+                userData,
+                numExecutors,
+                remoteAdmin,
+                amiType,
+                jvmopts,
+                stopOnTerminate,
+                subnetId,
+                tags,
+                idleTerminationMinutes,
+                usePrivateDnsName,
+                instanceCapStr,
+                iamInstanceProfile,
+                deleteRootOnTermination,
+                useEphemeralDevices,
+                useDedicatedTenancy,
+                launchTimeoutStr,
+                associatePublicIp,
+                customDeviceMapping,
+                connectBySSHProcess,
+                connectUsingPublicIp,
+                false,
+                false);
     }
 
     @Deprecated
-    public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
-            InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
-            String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts,
-            boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes,
-            boolean usePrivateDnsName, String instanceCapStr, String iamInstanceProfile, boolean useEphemeralDevices,
-            boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp, String customDeviceMapping,
+    public SlaveTemplate(
+            String ami,
+            String zone,
+            SpotConfiguration spotConfig,
+            String securityGroups,
+            String remoteFS,
+            com.amazonaws.services.ec2.model.InstanceType type,
+            boolean ebsOptimized,
+            String labelString,
+            Node.Mode mode,
+            String description,
+            String initScript,
+            String tmpDir,
+            String userData,
+            String numExecutors,
+            String remoteAdmin,
+            AMITypeData amiType,
+            String jvmopts,
+            boolean stopOnTerminate,
+            String subnetId,
+            List tags,
+            String idleTerminationMinutes,
+            boolean usePrivateDnsName,
+            String instanceCapStr,
+            String iamInstanceProfile,
+            boolean useEphemeralDevices,
+            boolean useDedicatedTenancy,
+            String launchTimeoutStr,
+            boolean associatePublicIp,
+            String customDeviceMapping,
             boolean connectBySSHProcess) {
-        this(ami, zone, spotConfig, securityGroups, remoteFS, type, ebsOptimized, labelString, mode, description, initScript,
-                tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, stopOnTerminate, subnetId, tags,
-                idleTerminationMinutes, usePrivateDnsName, instanceCapStr, iamInstanceProfile, false, useEphemeralDevices,
-                useDedicatedTenancy, launchTimeoutStr, associatePublicIp, customDeviceMapping, connectBySSHProcess, false);
+        this(
+                ami,
+                zone,
+                spotConfig,
+                securityGroups,
+                remoteFS,
+                type,
+                ebsOptimized,
+                labelString,
+                mode,
+                description,
+                initScript,
+                tmpDir,
+                userData,
+                numExecutors,
+                remoteAdmin,
+                amiType,
+                jvmopts,
+                stopOnTerminate,
+                subnetId,
+                tags,
+                idleTerminationMinutes,
+                usePrivateDnsName,
+                instanceCapStr,
+                iamInstanceProfile,
+                false,
+                useEphemeralDevices,
+                useDedicatedTenancy,
+                launchTimeoutStr,
+                associatePublicIp,
+                customDeviceMapping,
+                connectBySSHProcess,
+                false);
     }
 
     @Deprecated
-    public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
-            InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description, String initScript,
-            String tmpDir, String userData, String numExecutors, String remoteAdmin, AMITypeData amiType, String jvmopts,
-            boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes,
-            boolean usePrivateDnsName, String instanceCapStr, String iamInstanceProfile, boolean useEphemeralDevices,
-            boolean useDedicatedTenancy, String launchTimeoutStr, boolean associatePublicIp, String customDeviceMapping) {
-        this(ami, zone, spotConfig, securityGroups, remoteFS, type, ebsOptimized, labelString, mode, description, initScript,
-                tmpDir, userData, numExecutors, remoteAdmin, amiType, jvmopts, stopOnTerminate, subnetId, tags,
-                idleTerminationMinutes, usePrivateDnsName, instanceCapStr, iamInstanceProfile, useEphemeralDevices,
-                useDedicatedTenancy, launchTimeoutStr, associatePublicIp, customDeviceMapping, false);
+    public SlaveTemplate(
+            String ami,
+            String zone,
+            SpotConfiguration spotConfig,
+            String securityGroups,
+            String remoteFS,
+            com.amazonaws.services.ec2.model.InstanceType type,
+            boolean ebsOptimized,
+            String labelString,
+            Node.Mode mode,
+            String description,
+            String initScript,
+            String tmpDir,
+            String userData,
+            String numExecutors,
+            String remoteAdmin,
+            AMITypeData amiType,
+            String jvmopts,
+            boolean stopOnTerminate,
+            String subnetId,
+            List tags,
+            String idleTerminationMinutes,
+            boolean usePrivateDnsName,
+            String instanceCapStr,
+            String iamInstanceProfile,
+            boolean useEphemeralDevices,
+            boolean useDedicatedTenancy,
+            String launchTimeoutStr,
+            boolean associatePublicIp,
+            String customDeviceMapping) {
+        this(
+                ami,
+                zone,
+                spotConfig,
+                securityGroups,
+                remoteFS,
+                type,
+                ebsOptimized,
+                labelString,
+                mode,
+                description,
+                initScript,
+                tmpDir,
+                userData,
+                numExecutors,
+                remoteAdmin,
+                amiType,
+                jvmopts,
+                stopOnTerminate,
+                subnetId,
+                tags,
+                idleTerminationMinutes,
+                usePrivateDnsName,
+                instanceCapStr,
+                iamInstanceProfile,
+                useEphemeralDevices,
+                useDedicatedTenancy,
+                launchTimeoutStr,
+                associatePublicIp,
+                customDeviceMapping,
+                false);
     }
 
     /**
      * Backward compatible constructor for reloading previous version data
      */
-    public SlaveTemplate(String ami, String zone, SpotConfiguration spotConfig, String securityGroups, String remoteFS,
-            String sshPort, InstanceType type, boolean ebsOptimized, String labelString, Node.Mode mode, String description,
-            String initScript, String tmpDir, String userData, String numExecutors, String remoteAdmin, String rootCommandPrefix,
-            String slaveCommandPrefix, String slaveCommandSuffix, String jvmopts, boolean stopOnTerminate, String subnetId, List tags, String idleTerminationMinutes,
-            boolean usePrivateDnsName, String instanceCapStr, String iamInstanceProfile, boolean useEphemeralDevices,
+    public SlaveTemplate(
+            String ami,
+            String zone,
+            SpotConfiguration spotConfig,
+            String securityGroups,
+            String remoteFS,
+            String sshPort,
+            com.amazonaws.services.ec2.model.InstanceType type,
+            boolean ebsOptimized,
+            String labelString,
+            Node.Mode mode,
+            String description,
+            String initScript,
+            String tmpDir,
+            String userData,
+            String numExecutors,
+            String remoteAdmin,
+            String rootCommandPrefix,
+            String slaveCommandPrefix,
+            String slaveCommandSuffix,
+            String jvmopts,
+            boolean stopOnTerminate,
+            String subnetId,
+            List tags,
+            String idleTerminationMinutes,
+            boolean usePrivateDnsName,
+            String instanceCapStr,
+            String iamInstanceProfile,
+            boolean useEphemeralDevices,
             String launchTimeoutStr) {
-        this(ami, zone, spotConfig, securityGroups, remoteFS, type, ebsOptimized, labelString, mode, description, initScript,
-                tmpDir, userData, numExecutors, remoteAdmin, new UnixData(rootCommandPrefix, slaveCommandPrefix, slaveCommandSuffix, sshPort, null),
-                jvmopts, stopOnTerminate, subnetId, tags, idleTerminationMinutes, usePrivateDnsName, instanceCapStr, iamInstanceProfile,
-                useEphemeralDevices, false, launchTimeoutStr, false, null);
+        this(
+                ami,
+                zone,
+                spotConfig,
+                securityGroups,
+                remoteFS,
+                type,
+                ebsOptimized,
+                labelString,
+                mode,
+                description,
+                initScript,
+                tmpDir,
+                userData,
+                numExecutors,
+                remoteAdmin,
+                new UnixData(rootCommandPrefix, slaveCommandPrefix, slaveCommandSuffix, sshPort, null),
+                jvmopts,
+                stopOnTerminate,
+                subnetId,
+                tags,
+                idleTerminationMinutes,
+                usePrivateDnsName,
+                instanceCapStr,
+                iamInstanceProfile,
+                useEphemeralDevices,
+                false,
+                launchTimeoutStr,
+                false,
+                null);
     }
 
     public boolean isConnectBySSHProcess() {
@@ -638,7 +1653,7 @@ public String getSlaveName(String instanceId) {
         }
     }
 
-    String getZone() {
+    public String getZone() {
         return zone;
     }
 
@@ -651,7 +1666,7 @@ public Set getSecurityGroupSet() {
     }
 
     public Set parseSecurityGroups() {
-        if (securityGroups == null || "".equals(securityGroups.trim())) {
+        if (securityGroups == null || securityGroups.trim().isEmpty()) {
             return Collections.emptySet();
         } else {
             return new HashSet<>(Arrays.asList(securityGroups.split("\\s*,\\s*")));
@@ -662,18 +1677,15 @@ public int getNumExecutors() {
         try {
             return Integer.parseInt(numExecutors);
         } catch (NumberFormatException e) {
-            return EC2AbstractSlave.toNumExecutors(type);
+            return EC2AbstractSlave.toNumExecutors(InstanceType.fromValue(type));
         }
     }
 
     public int getSshPort() {
         try {
             String sshPort = "";
-            if (amiType.isUnix()) {
-                sshPort = ((UnixData) amiType).getSshPort();
-            }
-            if (amiType.isMac()) {
-                sshPort = ((MacData) amiType).getSshPort();
+            if (amiType.isSSHAgent()) {
+                sshPort = ((SSHData) amiType).getSshPort();
             }
             return Integer.parseInt(sshPort);
         } catch (NumberFormatException e) {
@@ -686,22 +1698,22 @@ public String getRemoteAdmin() {
     }
 
     public String getRootCommandPrefix() {
-        return (amiType.isUnix() ? ((UnixData) amiType).getRootCommandPrefix() : (amiType.isMac() ? ((MacData) amiType).getRootCommandPrefix():""));
+        return amiType.isSSHAgent() ? ((SSHData) amiType).getRootCommandPrefix() : "";
     }
 
     public String getSlaveCommandPrefix() {
-        return (amiType.isUnix() ? ((UnixData) amiType).getSlaveCommandPrefix() : (amiType.isMac() ? ((MacData) amiType).getSlaveCommandPrefix() : ""));
+        return amiType.isSSHAgent() ? ((SSHData) amiType).getSlaveCommandPrefix() : "";
     }
 
     public String getSlaveCommandSuffix() {
-        return (amiType.isUnix() ? ((UnixData) amiType).getSlaveCommandSuffix() : (amiType.isMac() ? ((MacData) amiType).getSlaveCommandSuffix() : ""));
+        return amiType.isSSHAgent() ? ((SSHData) amiType).getSlaveCommandSuffix() : "";
     }
 
     public String chooseSubnetId() {
         if (StringUtils.isBlank(subnetId)) {
             return null;
         } else {
-            String[] subnetIdList= getSubnetId().split(EC2_RESOURCE_ID_DELIMETERS);
+            String[] subnetIdList = getSubnetId().split(EC2_RESOURCE_ID_DELIMETERS);
 
             // Round-robin subnet selection.
             currentSubnetId = subnetIdList[nextSubnet];
@@ -727,22 +1739,36 @@ public String getCurrentSubnetId() {
         return currentSubnetId;
     }
 
+    @Deprecated
     public boolean getAssociatePublicIp() {
-        return associatePublicIp;
+        return AssociateIPStrategy.PUBLIC_IP == associateIPStrategy;
+    }
+
+    @Deprecated
+    @DataBoundSetter
+    public void setAssociatePublicIp(boolean associatePublicIp) {
+        this.associatePublicIp = associatePublicIp;
+        this.associateIPStrategy = AssociateIPStrategy.backwardsCompatible(associatePublicIp);
+    }
+
+    public AssociateIPStrategy getAssociateIPStrategy() {
+        return associateIPStrategy;
     }
 
     @Deprecated
     @DataBoundSetter
     public void setConnectUsingPublicIp(boolean connectUsingPublicIp) {
         this.connectUsingPublicIp = connectUsingPublicIp;
-        this.connectionStrategy = ConnectionStrategy.backwardsCompatible(this.usePrivateDnsName, this.connectUsingPublicIp, this.associatePublicIp);
+        this.connectionStrategy = ConnectionStrategy.backwardsCompatible(
+                this.usePrivateDnsName, this.connectUsingPublicIp, getAssociatePublicIp());
     }
 
     @Deprecated
     @DataBoundSetter
     public void setUsePrivateDnsName(boolean usePrivateDnsName) {
         this.usePrivateDnsName = usePrivateDnsName;
-        this.connectionStrategy = ConnectionStrategy.backwardsCompatible(this.usePrivateDnsName, this.connectUsingPublicIp, this.associatePublicIp);
+        this.connectionStrategy = ConnectionStrategy.backwardsCompatible(
+                this.usePrivateDnsName, this.connectUsingPublicIp, getAssociatePublicIp());
     }
 
     @Deprecated
@@ -756,8 +1782,9 @@ public boolean isConnectUsingPublicIp() {
     }
 
     public List getTags() {
-        if (null == tags)
+        if (null == tags) {
             return null;
+        }
         return Collections.unmodifiableList(tags);
     }
 
@@ -765,6 +1792,15 @@ public String getidleTerminationMinutes() {
         return idleTerminationMinutes;
     }
 
+    public boolean getTerminateIdleDuringShutdown() {
+        return terminateIdleDuringShutdown;
+    }
+
+    @DataBoundSetter
+    public void setTerminateIdleDuringShutdown(boolean terminateIdleDuringShutdown) {
+        this.terminateIdleDuringShutdown = terminateIdleDuringShutdown;
+    }
+
     public Set getLabelSet() {
         if (labelSet == null) {
             labelSet = Label.parse(labels);
@@ -801,7 +1837,8 @@ public MinimumNumberOfInstancesTimeRangeConfig getMinimumNumberOfInstancesTimeRa
     }
 
     @DataBoundSetter
-    public void setMinimumNumberOfInstancesTimeRangeConfig(MinimumNumberOfInstancesTimeRangeConfig minimumNumberOfInstancesTimeRangeConfig) {
+    public void setMinimumNumberOfInstancesTimeRangeConfig(
+            MinimumNumberOfInstancesTimeRangeConfig minimumNumberOfInstancesTimeRangeConfig) {
         this.minimumNumberOfInstancesTimeRangeConfig = minimumNumberOfInstancesTimeRangeConfig;
     }
 
@@ -810,8 +1847,9 @@ public int getInstanceCap() {
     }
 
     public int getSpotBlockReservationDuration() {
-        if (spotConfig == null)
+        if (spotConfig == null) {
             return 0;
+        }
         return spotConfig.getSpotBlockReservationDuration();
     }
 
@@ -820,8 +1858,9 @@ public String getSpotBlockReservationDurationStr() {
             return "";
         } else {
             int dur = getSpotBlockReservationDuration();
-            if (dur == 0)
+            if (dur == 0) {
                 return "";
+            }
             return String.valueOf(getSpotBlockReservationDuration());
         }
     }
@@ -835,8 +1874,9 @@ public String getInstanceCapStr() {
     }
 
     public String getSpotMaxBidPrice() {
-        if (spotConfig == null)
+        if (spotConfig == null) {
             return null;
+        }
         return SpotConfiguration.normalizeBid(spotConfig.getSpotMaxBidPrice());
     }
 
@@ -846,12 +1886,16 @@ public String getIamInstanceProfile() {
 
     @DataBoundSetter
     public void setHostKeyVerificationStrategy(HostKeyVerificationStrategyEnum hostKeyVerificationStrategy) {
-        this.hostKeyVerificationStrategy = (hostKeyVerificationStrategy != null) ? hostKeyVerificationStrategy : HostKeyVerificationStrategyEnum.CHECK_NEW_SOFT;
+        this.hostKeyVerificationStrategy = (hostKeyVerificationStrategy != null)
+                ? hostKeyVerificationStrategy
+                : HostKeyVerificationStrategyEnum.CHECK_NEW_SOFT;
     }
 
     @NonNull
     public HostKeyVerificationStrategyEnum getHostKeyVerificationStrategy() {
-        return hostKeyVerificationStrategy != null ? hostKeyVerificationStrategy : HostKeyVerificationStrategyEnum.CHECK_NEW_SOFT;
+        return hostKeyVerificationStrategy != null
+                ? hostKeyVerificationStrategy
+                : HostKeyVerificationStrategyEnum.CHECK_NEW_SOFT;
     }
 
     @CheckForNull
@@ -884,20 +1928,130 @@ public void setAmiFilters(List amiFilters) {
         this.amiFilters = amiFilters;
     }
 
+    @DataBoundSetter
+    public void setAvoidUsingOrphanedNodes(Boolean avoidUsingOrphanedNodes) {
+        this.avoidUsingOrphanedNodes = avoidUsingOrphanedNodes;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public String getRemoteFS() {
+        return remoteFS;
+    }
+
+    public SpotConfiguration getSpotConfig() {
+        return spotConfig;
+    }
+
+    public String getSecurityGroups() {
+        return securityGroups;
+    }
+
+    public String getJavaPath() {
+        return javaPath;
+    }
+
+    public String getJvmopts() {
+        return jvmopts;
+    }
+
+    public boolean getStopOnTerminate() {
+        return stopOnTerminate;
+    }
+
+    public String getIdleTerminationMinutes() {
+        return idleTerminationMinutes;
+    }
+
+    public String getInitScript() {
+        return initScript;
+    }
+
+    public String getTmpDir() {
+        return tmpDir;
+    }
+
+    public String getUserData() {
+        return userData;
+    }
+
+    public boolean getConnectBySSHProcess() {
+        return connectBySSHProcess;
+    }
+
+    public boolean getConnectUsingPublicIp() {
+        return connectUsingPublicIp;
+    }
+
+    public boolean getDeleteRootOnTermination() {
+        return deleteRootOnTermination;
+    }
+
+    public boolean getUseEphemeralDevices() {
+        return useEphemeralDevices;
+    }
+
+    public boolean getEbsOptimized() {
+        return ebsOptimized;
+    }
+
+    public boolean getMonitoring() {
+        return monitoring;
+    }
+
+    public boolean getT2Unlimited() {
+        return t2Unlimited;
+    }
+
+    public EbsEncryptRootVolume getEbsEncryptRootVolume() {
+        return ebsEncryptRootVolume;
+    }
+
+    public String getCustomDeviceMapping() {
+        return customDeviceMapping;
+    }
+
+    public Tenancy getTenancy() {
+        return tenancy;
+    }
+
+    public ConnectionStrategy getConnectionStrategy() {
+        return connectionStrategy;
+    }
+
+    public boolean getUseDedicatedTenancy() {
+        return useDedicatedTenancy;
+    }
+
+    public int getNextSubnet() {
+        return nextSubnet;
+    }
+
     @Override
     public String toString() {
-        return "SlaveTemplate{" +
-                "description='" + description + '\'' +
-                ", labels='" + labels + '\'' +
-                '}';
+        return "SlaveTemplate{" + "description='" + description + '\'' + ", labels='" + labels + '\'' + '}';
     }
 
     public int getMaxTotalUses() {
         return maxTotalUses;
     }
 
+    public boolean isAvoidUsingOrphanedNodes() {
+        return avoidUsingOrphanedNodes;
+    }
+
+    public boolean getAvoidUsingOrphanedNodes() {
+        return avoidUsingOrphanedNodes;
+    }
+
     public Boolean getMetadataSupported() {
-      return metadataSupported;
+        return metadataSupported;
     }
 
     public Boolean getMetadataEndpointEnabled() {
@@ -916,11 +2070,18 @@ public Tenancy getTenancyAttribute() {
         return tenancy;
     }
 
+    public Boolean getEnclaveEnabled() {
+        return enclaveEnabled;
+    }
+
     public DescribableList, NodePropertyDescriptor> getNodeProperties() {
         return Objects.requireNonNull(nodeProperties);
     }
 
-    public enum ProvisionOptions { ALLOW_CREATE, FORCE_CREATE }
+    public enum ProvisionOptions {
+        ALLOW_CREATE,
+        FORCE_CREATE
+    }
 
     /**
      * Provisions a new EC2 agent or starts a previously stopped on-demand instance.
@@ -928,11 +2089,14 @@ public enum ProvisionOptions { ALLOW_CREATE, FORCE_CREATE }
      * @return always non-null. This needs to be then added to {@link Hudson#addNode(Node)}.
      */
     @NonNull
-    public List provision(int number, EnumSet provisionOptions) throws AmazonClientException, IOException {
+    public List provision(int number, EnumSet provisionOptions)
+            throws SdkException, IOException {
         final Image image = getImage();
         if (this.spotConfig != null) {
-            if (provisionOptions.contains(ProvisionOptions.ALLOW_CREATE) || provisionOptions.contains(ProvisionOptions.FORCE_CREATE))
+            if (provisionOptions.contains(ProvisionOptions.ALLOW_CREATE)
+                    || provisionOptions.contains(ProvisionOptions.FORCE_CREATE)) {
                 return provisionSpot(image, number, provisionOptions);
+            }
             return Collections.emptyList();
         }
         return provisionOndemand(image, number, provisionOptions);
@@ -943,11 +2107,10 @@ public List provision(int number, EnumSet pr
      */
     private boolean checkInstance(Instance instance) {
         for (EC2AbstractSlave node : NodeIterator.nodes(EC2AbstractSlave.class)) {
-            if ( (node.getInstanceId().equals(instance.getInstanceId())) &&
-                    (! (instance.getState().getName().equalsIgnoreCase(InstanceStateName.Stopped.toString())
-                ))
-               ){
-                logInstanceCheck(instance, ". false - found existing corresponding Jenkins agent: " + node.getInstanceId());
+            if ((node.getInstanceId().equals(instance.instanceId()))
+                    && (!(instance.state().name().equals(InstanceStateName.STOPPED)))) {
+                logInstanceCheck(
+                        instance, ". false - found existing corresponding Jenkins agent: " + node.getInstanceId());
                 return false;
             }
         }
@@ -956,102 +2119,116 @@ private boolean checkInstance(Instance instance) {
     }
 
     private void logInstanceCheck(Instance instance, String message) {
-        logProvisionInfo("checkInstance: " + instance.getInstanceId() + "." + message);
+        logProvisionInfo("checkInstance: " + instance.instanceId() + "." + message);
     }
 
     private boolean isSameIamInstanceProfile(Instance instance) {
-        return StringUtils.isBlank(getIamInstanceProfile()) ||
-                (instance.getIamInstanceProfile() != null &&
-                        instance.getIamInstanceProfile().getArn().equals(getIamInstanceProfile()));
-
+        return StringUtils.isBlank(getIamInstanceProfile())
+                || (instance.iamInstanceProfile() != null
+                        && instance.iamInstanceProfile().arn().equals(getIamInstanceProfile()));
     }
 
-    private boolean isTerminatingOrShuttindDown(String instanceStateName) {
-        return instanceStateName.equalsIgnoreCase(InstanceStateName.Terminated.toString())
-                || instanceStateName.equalsIgnoreCase(InstanceStateName.ShuttingDown.toString());
+    private boolean isTerminatingOrShuttindDown(InstanceStateName instanceStateName) {
+        return instanceStateName.equals(InstanceStateName.TERMINATED)
+                || instanceStateName.equals(InstanceStateName.SHUTTING_DOWN);
     }
 
     private void logProvisionInfo(String message) {
         LOGGER.info(this + ". " + message);
     }
 
-    HashMap> makeRunInstancesRequestAndFilters(Image image, int number, AmazonEC2 ec2) throws IOException {
+    HashMap> makeRunInstancesRequestAndFilters(Image image, int number, Ec2Client ec2)
+            throws IOException {
         return makeRunInstancesRequestAndFilters(image, number, ec2, true);
     }
 
     @Deprecated
-    HashMap> makeRunInstancesRequestAndFilters(int number, AmazonEC2 ec2) throws IOException {
+    HashMap> makeRunInstancesRequestAndFilters(int number, Ec2Client ec2)
+            throws IOException {
         return makeRunInstancesRequestAndFilters(getImage(), number, ec2);
     }
 
-    HashMap> makeRunInstancesRequestAndFilters(Image image, int number, AmazonEC2 ec2, boolean rotateSubnet) throws IOException {
-        String imageId = image.getImageId();
-        RunInstancesRequest riRequest = new RunInstancesRequest(imageId, 1, number).withInstanceType(type);
-        riRequest.setEbsOptimized(ebsOptimized);
-        riRequest.setMonitoring(monitoring);
+    HashMap> makeRunInstancesRequestAndFilters(
+            Image image, int number, Ec2Client ec2, boolean rotateSubnet) throws IOException {
+        String imageId = image.imageId();
+        RunInstancesRequest.Builder riRequestBuilder = RunInstancesRequest.builder()
+                .imageId(image.imageId())
+                .minCount(1)
+                .maxCount(number)
+                .instanceType(type)
+                .ebsOptimized(ebsOptimized)
+                .monitoring(RunInstancesMonitoringEnabled.builder()
+                        .enabled(monitoring)
+                        .build());
 
-        if (t2Unlimited){
-            CreditSpecificationRequest creditRequest = new CreditSpecificationRequest();
-            creditRequest.setCpuCredits("unlimited");
-            riRequest.setCreditSpecification(creditRequest);
+        if (t2Unlimited) {
+            CreditSpecificationRequest creditRequest =
+                    CreditSpecificationRequest.builder().cpuCredits("unlimited").build();
+            riRequestBuilder.creditSpecification(creditRequest);
         }
 
-        setupBlockDeviceMappings(image, riRequest.getBlockDeviceMappings());
+        riRequestBuilder.blockDeviceMappings(getBlockDeviceMappings(image));
 
-        if(stopOnTerminate){
-            riRequest.setInstanceInitiatedShutdownBehavior(ShutdownBehavior.Stop);
+        if (stopOnTerminate) {
+            riRequestBuilder.instanceInitiatedShutdownBehavior(ShutdownBehavior.STOP);
             logProvisionInfo("Setting Instance Initiated Shutdown Behavior : ShutdownBehavior.Stop");
-        }else{
-            riRequest.setInstanceInitiatedShutdownBehavior(ShutdownBehavior.Terminate);
+        } else {
+            riRequestBuilder.instanceInitiatedShutdownBehavior(ShutdownBehavior.TERMINATE);
             logProvisionInfo("Setting Instance Initiated Shutdown Behavior : ShutdownBehavior.Terminate");
         }
 
         List diFilters = new ArrayList<>();
-        diFilters.add(new Filter("image-id").withValues(imageId));
-        diFilters.add(new Filter("instance-type").withValues(type.toString()));
+        diFilters.add(Filter.builder().name("image-id").values(imageId).build());
+        diFilters.add(Filter.builder().name("instance-type").values(type).build());
 
         KeyPair keyPair = getKeyPair(ec2);
-        if (keyPair == null){
+        if (keyPair == null) {
             logProvisionInfo("Could not retrieve a valid key pair.");
             return null;
         }
-        riRequest.setUserData(Base64.getEncoder().encodeToString(userData.getBytes(StandardCharsets.UTF_8)));
-        riRequest.setKeyName(keyPair.getKeyName());
-        diFilters.add(new Filter("key-name").withValues(keyPair.getKeyName()));
-
+        riRequestBuilder.userData(Base64.getEncoder().encodeToString(userData.getBytes(StandardCharsets.UTF_8)));
+        riRequestBuilder.keyName(keyPair.getKeyPairInfo().keyName());
+        diFilters.add(Filter.builder()
+                .name("key-name")
+                .values(keyPair.getKeyPairInfo().keyName())
+                .build());
 
+        Placement.Builder placementBuilder = Placement.builder();
         if (StringUtils.isNotBlank(getZone())) {
-            Placement placement = new Placement(getZone());
             if (getTenancyAttribute().equals(Tenancy.Dedicated)) {
-                placement.setTenancy("dedicated");
+                placementBuilder.tenancy("dedicated");
             }
-            riRequest.setPlacement(placement);
-            diFilters.add(new Filter("availability-zone").withValues(getZone()));
-        }
-
-        if(getTenancyAttribute().equals(Tenancy.Host)){
-            Placement placement = new Placement();
-            placement.setTenancy("host");
-            riRequest.setPlacement(placement);
-            diFilters.add(new Filter("tenancy").withValues(placement.getTenancy()));
-        }else if(getTenancyAttribute().equals(Tenancy.Default)){
-            Placement placement = new Placement();
-            placement.setTenancy("default");
-            riRequest.setPlacement(placement);
-            diFilters.add(new Filter("tenancy").withValues(placement.getTenancy()));
+            riRequestBuilder.placement(placementBuilder.build());
+            diFilters.add(
+                    Filter.builder().name("availability-zone").values(getZone()).build());
+        }
+
+        if (getTenancyAttribute().equals(Tenancy.Host)) {
+            placementBuilder.tenancy("host");
+            Placement placement = placementBuilder.build();
+            riRequestBuilder.placement(placement);
+            diFilters.add(Filter.builder()
+                    .name("tenancy")
+                    .values(placement.tenancyAsString())
+                    .build());
+        } else if (getTenancyAttribute().equals(Tenancy.Default)) {
+            placementBuilder.tenancy("default");
+            Placement placement = placementBuilder.build();
+            riRequestBuilder.placement(placement);
+            diFilters.add(Filter.builder()
+                    .name("tenancy")
+                    .values(placement.tenancyAsString())
+                    .build());
         }
 
         String subnetId = chooseSubnetId(rotateSubnet);
+        LOGGER.log(Level.FINE, () -> String.format("Chose subnetId %s", subnetId));
 
-        InstanceNetworkInterfaceSpecification net = new InstanceNetworkInterfaceSpecification();
+        InstanceNetworkInterfaceSpecification.Builder netBuilder = InstanceNetworkInterfaceSpecification.builder();
         if (StringUtils.isNotBlank(subnetId)) {
-            if (getAssociatePublicIp()) {
-                net.setSubnetId(subnetId);
-            } else {
-                riRequest.setSubnetId(subnetId);
-            }
+            netBuilder.subnetId(subnetId);
 
-            diFilters.add(new Filter("subnet-id").withValues(subnetId));
+            diFilters.add(Filter.builder().name("subnet-id").values(subnetId).build());
 
             /*
              * If we have a subnet ID then we can only use VPC security groups
@@ -1060,136 +2237,195 @@ HashMap> makeRunInstancesRequestAndFilters(Ima
                 List groupIds = getEc2SecurityGroups(ec2);
 
                 if (!groupIds.isEmpty()) {
-                    if (getAssociatePublicIp()) {
-                        net.setGroups(groupIds);
-                    } else {
-                        riRequest.setSecurityGroupIds(groupIds);
-                    }
+                    netBuilder.groups(groupIds);
 
-                    diFilters.add(new Filter("instance.group-id").withValues(groupIds));
+                    diFilters.add(Filter.builder()
+                            .name("instance.group-id")
+                            .values(groupIds)
+                            .build());
                 }
             }
         } else {
-            List groupIds = getSecurityGroupsBy("group-name", securityGroupSet, ec2)
-                                            .getSecurityGroups()
-                                            .stream().map(SecurityGroup::getGroupId)
-                                            .collect(Collectors.toList());
-            if (getAssociatePublicIp()) {
-                net.setGroups(groupIds);
-            } else {
-                riRequest.setSecurityGroups(securityGroupSet);
-            }
+            List groupIds = getSecurityGroupsBy("group-name", securityGroupSet, ec2).securityGroups().stream()
+                    .map(SecurityGroup::groupId)
+                    .collect(Collectors.toList());
+            netBuilder.groups(groupIds);
+
             if (!groupIds.isEmpty()) {
-                diFilters.add(new Filter("instance.group-id").withValues(groupIds));
+                diFilters.add(Filter.builder()
+                        .name("instance.group-id")
+                        .values(groupIds)
+                        .build());
             }
         }
 
-        net.setAssociatePublicIpAddress(getAssociatePublicIp());
-        net.setDeviceIndex(0);
-
-        if (getAssociatePublicIp()) {
-            riRequest.withNetworkInterfaces(net);
+        switch (getAssociateIPStrategy()) {
+            case PUBLIC_IP:
+                netBuilder.associatePublicIpAddress(true);
+                break;
+            case PRIVATE_IP:
+                netBuilder.associatePublicIpAddress(false);
+                break;
+            case SUBNET:
+            case DEFAULT:
+                break;
         }
 
+        netBuilder.deviceIndex(0);
+        riRequestBuilder.networkInterfaces(netBuilder.build());
+
         HashSet instTags = buildTags(EC2Cloud.EC2_SLAVE_TYPE_DEMAND);
         for (Tag tag : instTags) {
-            diFilters.add(new Filter("tag:" + tag.getKey()).withValues(tag.getValue()));
+            diFilters.add(Filter.builder()
+                    .name("tag:" + tag.key())
+                    .values(tag.value())
+                    .build());
         }
 
         if (StringUtils.isNotBlank(getIamInstanceProfile())) {
-            riRequest.setIamInstanceProfile(new IamInstanceProfileSpecification().withArn(getIamInstanceProfile()));
+            riRequestBuilder.iamInstanceProfile(IamInstanceProfileSpecification.builder()
+                    .arn(getIamInstanceProfile())
+                    .build());
         }
 
         List tagList = new ArrayList<>();
-        TagSpecification tagSpecification = new TagSpecification();
-        tagSpecification.setTags(instTags);
-        tagList.add(tagSpecification.clone().withResourceType(ResourceType.Instance));
-        tagList.add(tagSpecification.clone().withResourceType(ResourceType.Volume));
-        riRequest.setTagSpecifications(tagList);
+        tagList.add(TagSpecification.builder()
+                .tags(instTags)
+                .resourceType(ResourceType.INSTANCE)
+                .build());
+        tagList.add(TagSpecification.builder()
+                .tags(instTags)
+                .resourceType(ResourceType.VOLUME)
+                .build());
+        tagList.add(TagSpecification.builder()
+                .tags(instTags)
+                .resourceType(ResourceType.NETWORK_INTERFACE)
+                .build());
+        riRequestBuilder.tagSpecifications(tagList);
 
         if (metadataSupported) {
-            InstanceMetadataOptionsRequest instanceMetadataOptionsRequest = new InstanceMetadataOptionsRequest();
-            instanceMetadataOptionsRequest.setHttpEndpoint(metadataEndpointEnabled ? InstanceMetadataEndpointState.Enabled.toString() : InstanceMetadataEndpointState.Disabled.toString());
-            instanceMetadataOptionsRequest.setHttpPutResponseHopLimit(metadataHopsLimit == null ? EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT : metadataHopsLimit);
-            instanceMetadataOptionsRequest.setHttpTokens(
-                metadataTokensRequired ? HttpTokensState.Required.toString() : HttpTokensState.Optional.toString());
-            riRequest.setMetadataOptions(instanceMetadataOptionsRequest);
+            InstanceMetadataOptionsRequest.Builder instanceMetadataOptionsRequestBuilder =
+                    InstanceMetadataOptionsRequest.builder();
+            instanceMetadataOptionsRequestBuilder.httpEndpoint(
+                    metadataEndpointEnabled
+                            ? InstanceMetadataEndpointState.ENABLED.toString()
+                            : InstanceMetadataEndpointState.DISABLED.toString());
+            instanceMetadataOptionsRequestBuilder.httpPutResponseHopLimit(
+                    metadataHopsLimit == null ? EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT : metadataHopsLimit);
+            instanceMetadataOptionsRequestBuilder.httpTokens(
+                    metadataTokensRequired ? HttpTokensState.REQUIRED.toString() : HttpTokensState.OPTIONAL.toString());
+            riRequestBuilder.metadataOptions(instanceMetadataOptionsRequestBuilder.build());
+        }
+
+        if (enclaveEnabled) {
+            EnclaveOptionsRequest.Builder enclaveOptionsRequestBuilder =
+                    EnclaveOptionsRequest.builder().enabled(true);
+            riRequestBuilder.enclaveOptions(enclaveOptionsRequestBuilder.build());
         }
 
         HashMap> ret = new HashMap<>();
-        ret.put(riRequest, diFilters);
+        ret.put(riRequestBuilder.build(), diFilters);
         return ret;
     }
 
     @Deprecated
-    HashMap> makeRunInstancesRequestAndFilters(int number, AmazonEC2 ec2, boolean rotateSubnet) throws IOException {
+    HashMap> makeRunInstancesRequestAndFilters(
+            int number, Ec2Client ec2, boolean rotateSubnet) throws IOException {
         return makeRunInstancesRequestAndFilters(getImage(), number, ec2, rotateSubnet);
     }
 
     /**
      * Provisions an On-demand EC2 agent by launching a new instance or starting a previously-stopped instance.
      */
-    private List provisionOndemand(Image image, int number, EnumSet provisionOptions)
-            throws IOException {
+    private List provisionOndemand(
+            Image image, int number, EnumSet provisionOptions) throws IOException {
         return provisionOndemand(image, number, provisionOptions, false, false);
     }
 
     /**
      * Provisions an On-demand EC2 agent by launching a new instance or starting a previously-stopped instance.
      */
-    private List provisionOndemand(Image image, int number, EnumSet provisionOptions, boolean spotWithoutBidPrice, boolean fallbackSpotToOndemand)
+    private List provisionOndemand(
+            Image image,
+            int number,
+            EnumSet provisionOptions,
+            boolean spotWithoutBidPrice,
+            boolean fallbackSpotToOndemand)
             throws IOException {
-        AmazonEC2 ec2 = getParent().connect();
+        Ec2Client ec2 = getParent().connect();
 
         logProvisionInfo("Considering launching");
-
-        HashMap> runInstancesRequestFilterMap = makeRunInstancesRequestAndFilters(image, number, ec2);
-        Map.Entry> entry = runInstancesRequestFilterMap.entrySet().iterator().next();
+        HashMap> runInstancesRequestFilterMap =
+                makeRunInstancesRequestAndFilters(image, number, ec2);
+        Map.Entry> entry =
+                runInstancesRequestFilterMap.entrySet().iterator().next();
         RunInstancesRequest riRequest = entry.getKey();
         List diFilters = entry.getValue();
 
-        DescribeInstancesRequest diRequest = new DescribeInstancesRequest().withFilters(diFilters);
+        DescribeInstancesRequest diRequest =
+                DescribeInstancesRequest.builder().filters(diFilters).build();
 
         logProvisionInfo("Looking for existing instances with describe-instance: " + diRequest);
 
-        DescribeInstancesResult diResult = ec2.describeInstances(diRequest);
-        List orphansOrStopped = findOrphansOrStopped(diResult, number);
-
-        if (orphansOrStopped.isEmpty() && !provisionOptions.contains(ProvisionOptions.FORCE_CREATE) &&
-                !provisionOptions.contains(ProvisionOptions.ALLOW_CREATE)) {
-            logProvisionInfo("No existing instance found - but cannot create new instance");
-            return null;
-        }
+        DescribeInstancesResponse diResult = ec2.describeInstances(diRequest);
+        List orphansOrStopped = new ArrayList<>();
+        if (!avoidUsingOrphanedNodes) {
+            orphansOrStopped = findOrphansOrStopped(diResult, number);
+
+            if (orphansOrStopped.isEmpty()
+                    && !provisionOptions.contains(ProvisionOptions.FORCE_CREATE)
+                    && !provisionOptions.contains(ProvisionOptions.ALLOW_CREATE)) {
+                logProvisionInfo("No existing instance found - but cannot create new instance");
+                return null;
+            }
 
-        wakeOrphansOrStoppedUp(ec2, orphansOrStopped);
+            wakeOrphansOrStoppedUp(ec2, orphansOrStopped);
 
-        if (orphansOrStopped.size() == number) {
-            return toSlaves(orphansOrStopped);
+            if (orphansOrStopped.size() == number) {
+                return toSlaves(orphansOrStopped);
+            }
         }
 
-        riRequest.setMaxCount(number - orphansOrStopped.size());
+        RunInstancesRequest.Builder riRequestBuilder = riRequest.toBuilder();
+        riRequestBuilder.maxCount(number - orphansOrStopped.size());
 
         List newInstances;
         if (spotWithoutBidPrice) {
-            InstanceMarketOptionsRequest instanceMarketOptionsRequest = new InstanceMarketOptionsRequest().withMarketType(MarketType.Spot);
+            InstanceMarketOptionsRequest.Builder instanceMarketOptionsRequestBuilder =
+                    InstanceMarketOptionsRequest.builder().marketType(MarketType.SPOT);
             if (getSpotBlockReservationDuration() != 0) {
-                SpotMarketOptions spotOptions = new SpotMarketOptions().withBlockDurationMinutes(getSpotBlockReservationDuration() * 60);
-                instanceMarketOptionsRequest.setSpotOptions(spotOptions);
+                SpotMarketOptions spotOptions = SpotMarketOptions.builder()
+                        .blockDurationMinutes(getSpotBlockReservationDuration() * 60)
+                        .build();
+                instanceMarketOptionsRequestBuilder.spotOptions(spotOptions);
             }
-            riRequest.setInstanceMarketOptions(instanceMarketOptionsRequest);
+            riRequestBuilder.instanceMarketOptions(instanceMarketOptionsRequestBuilder.build());
             try {
-                newInstances = ec2.runInstances(riRequest).getReservation().getInstances();
-            } catch (AmazonEC2Exception e) {
-                if (fallbackSpotToOndemand && e.getErrorCode().equals("InsufficientInstanceCapacity")) {
-                    logProvisionInfo("There is no spot capacity available matching your request, falling back to on-demand instance.");
-                    riRequest.setInstanceMarketOptions(new InstanceMarketOptionsRequest());
-                    newInstances = ec2.runInstances(riRequest).getReservation().getInstances();
+                newInstances = new ArrayList<>(
+                        ec2.runInstances(riRequestBuilder.build()).instances());
+            } catch (Ec2Exception e) {
+                if (fallbackSpotToOndemand
+                        && "InsufficientInstanceCapacity"
+                                .equals(e.awsErrorDetails().errorCode())) {
+                    logProvisionInfo(
+                            "There is no spot capacity available matching your request, falling back to on-demand instance.");
+                    riRequestBuilder.instanceMarketOptions(instanceMarketOptionsRequestBuilder.build());
+                    newInstances = new ArrayList<>(
+                            ec2.runInstances(riRequestBuilder.build()).instances());
                 } else {
                     throw e;
                 }
             }
         } else {
-            newInstances = ec2.runInstances(riRequest).getReservation().getInstances();
+            try {
+                newInstances = new ArrayList<>(
+                        ec2.runInstances(riRequestBuilder.build()).instances());
+            } catch (Ec2Exception e) {
+                logProvisionInfo("Jenkins attempted to reserve "
+                        + riRequest.maxCount()
+                        + " instances and received this EC2 exception: " + e.getMessage());
+                throw e;
+            }
         }
         // Have to create a new instance
 
@@ -1202,25 +2438,26 @@ private List provisionOndemand(Image image, int number, EnumSe
         return toSlaves(newInstances);
     }
 
-    void wakeOrphansOrStoppedUp(AmazonEC2 ec2, List orphansOrStopped) {
+    void wakeOrphansOrStoppedUp(Ec2Client ec2, List orphansOrStopped) {
         List instances = new ArrayList<>();
-        for(Instance instance : orphansOrStopped) {
-            if (instance.getState().getName().equalsIgnoreCase(InstanceStateName.Stopping.toString())
-                    || instance.getState().getName().equalsIgnoreCase(InstanceStateName.Stopped.toString())) {
+        for (Instance instance : orphansOrStopped) {
+            if (instance.state().name().equals(InstanceStateName.STOPPING)
+                    || instance.state().name().equals(InstanceStateName.STOPPED)) {
                 logProvisionInfo("Found stopped instances - will start it: " + instance);
-                instances.add(instance.getInstanceId());
+                instances.add(instance.instanceId());
             } else {
                 // Should be pending or running at this point, just let it come up
-                logProvisionInfo("Found existing pending or running: " + instance.getState().getName() + " instance: " + instance);
+                logProvisionInfo(
+                        "Found existing pending or running: " + instance.state().name() + " instance: " + instance);
             }
         }
 
         if (!instances.isEmpty()) {
-            StartInstancesRequest siRequest = new StartInstancesRequest(instances);
-            StartInstancesResult siResult = ec2.startInstances(siRequest);
+            StartInstancesRequest siRequest =
+                    StartInstancesRequest.builder().instanceIds(instances).build();
+            StartInstancesResponse siResult = ec2.startInstances(siRequest);
             logProvisionInfo("Result of starting stopped instances:" + siResult);
         }
-
     }
 
     List toSlaves(List newInstances) throws IOException {
@@ -1237,17 +2474,19 @@ List toSlaves(List newInstances) throws IOException
         }
     }
 
-    List findOrphansOrStopped(DescribeInstancesResult diResult, int number) {
+    List findOrphansOrStopped(DescribeInstancesResponse diResult, int number) {
         List orphansOrStopped = new ArrayList<>();
         int count = 0;
-        for (Reservation reservation : diResult.getReservations()) {
-            for (Instance instance : reservation.getInstances()) {
+        for (Reservation reservation : diResult.reservations()) {
+            for (Instance instance : reservation.instances()) {
                 if (!isSameIamInstanceProfile(instance)) {
-                    logInstanceCheck(instance, ". false - IAM Instance profile does not match: " + instance.getIamInstanceProfile());
+                    logInstanceCheck(
+                            instance,
+                            ". false - IAM Instance profile does not match: " + instance.iamInstanceProfile());
                     continue;
                 }
 
-                if (isTerminatingOrShuttindDown(instance.getState().getName())) {
+                if (isTerminatingOrShuttindDown(instance.state().name())) {
                     logInstanceCheck(instance, ". false - Instance is terminated or shutting down");
                     continue;
                 }
@@ -1267,67 +2506,73 @@ List findOrphansOrStopped(DescribeInstancesResult diResult, int number
     }
 
     private void setupRootDevice(Image image, List deviceMappings) {
-        if (!"ebs".equals(image.getRootDeviceType())) {
+        if (!DeviceType.EBS.equals(image.rootDeviceType())) {
             return;
         }
 
         // get the root device (only one expected in the blockmappings)
-        final List rootDeviceMappings = image.getBlockDeviceMappings();
-        if (rootDeviceMappings.size() == 0) {
+        if (deviceMappings.isEmpty()) {
             LOGGER.warning("AMI missing block devices");
             return;
         }
-        BlockDeviceMapping rootMapping = rootDeviceMappings.get(0);
-        LOGGER.info("AMI had " + rootMapping.getDeviceName());
-        LOGGER.info(rootMapping.getEbs().toString());
+        BlockDeviceMapping rootMapping = deviceMappings.get(0);
+        LOGGER.info("AMI had " + rootMapping.deviceName());
+        LOGGER.info(rootMapping.ebs().toString());
 
-        // Create a shadow of the AMI mapping (doesn't like reusing rootMapping directly)
-        BlockDeviceMapping newMapping = rootMapping.clone();
+        // Create a new AMI mapping as a copy of the existing one
+        BlockDeviceMapping.Builder newRootMappingBuilder = rootMapping.toBuilder();
+        EbsBlockDevice.Builder newRootDeviceBuilder = rootMapping.ebs().toBuilder();
 
         if (deleteRootOnTermination) {
+            newRootDeviceBuilder.deleteOnTermination(Boolean.TRUE);
             // Check if the root device is already in the mapping and update it
-            for (final BlockDeviceMapping mapping : deviceMappings) {
-                LOGGER.info("Request had " + mapping.getDeviceName());
-                if (rootMapping.getDeviceName().equals(mapping.getDeviceName())) {
-                    mapping.getEbs().setDeleteOnTermination(Boolean.TRUE);
-                    return;
+            for (final BlockDeviceMapping mapping : image.blockDeviceMappings()) {
+                LOGGER.info("Request had " + mapping.deviceName());
+                if (rootMapping.deviceName().equals(mapping.deviceName())) {
+                    // Existing mapping found, replace with the copy
+                    newRootMappingBuilder.ebs(newRootDeviceBuilder.build());
+                    deviceMappings.remove(0);
+                    deviceMappings.add(0, newRootMappingBuilder.build());
                 }
             }
-
-            // pass deleteRootOnTermination to shadow of the AMI mapping
-            newMapping.getEbs().setDeleteOnTermination(Boolean.TRUE);
         }
 
-        newMapping.getEbs().setEncrypted(ebsEncryptRootVolume.getValue());
-        String message = String.format("EBS default encryption value set to: %s (%s)", ebsEncryptRootVolume.getDisplayText(), ebsEncryptRootVolume.getValue());
+        // New existing mapping found, add a new one as the root
+        newRootDeviceBuilder.encrypted(ebsEncryptRootVolume.getValue());
+        String message = String.format(
+                "EBS default encryption value set to: %s (%s)",
+                ebsEncryptRootVolume.getDisplayText(), ebsEncryptRootVolume.getValue());
         logProvisionInfo(message);
-        deviceMappings.add(0, newMapping);
-
+        newRootMappingBuilder.ebs(newRootDeviceBuilder.build());
+        deviceMappings.add(0, newRootMappingBuilder.build());
     }
 
     private List getNewEphemeralDeviceMapping(Image image) {
 
-        final List oldDeviceMapping = image.getBlockDeviceMappings();
+        final List oldDeviceMapping = image.blockDeviceMappings();
 
         final Set occupiedDevices = new HashSet<>();
         for (final BlockDeviceMapping mapping : oldDeviceMapping) {
 
-            occupiedDevices.add(mapping.getDeviceName());
+            occupiedDevices.add(mapping.deviceName());
         }
 
-        final List available = new ArrayList<>(
-                Arrays.asList("ephemeral0", "ephemeral1", "ephemeral2", "ephemeral3"));
+        final List available =
+                new ArrayList<>(Arrays.asList("ephemeral0", "ephemeral1", "ephemeral2", "ephemeral3"));
 
         final List newDeviceMapping = new ArrayList<>(4);
         for (char suffix = 'b'; suffix <= 'z' && !available.isEmpty(); suffix++) {
 
             final String deviceName = String.format("/dev/xvd%s", suffix);
 
-            if (occupiedDevices.contains(deviceName))
+            if (occupiedDevices.contains(deviceName)) {
                 continue;
+            }
 
-            final BlockDeviceMapping newMapping = new BlockDeviceMapping().withDeviceName(deviceName).withVirtualName(
-                    available.get(0));
+            final BlockDeviceMapping newMapping = BlockDeviceMapping.builder()
+                    .deviceName(deviceName)
+                    .virtualName(available.get(0))
+                    .build();
 
             newDeviceMapping.add(newMapping);
             available.remove(0);
@@ -1343,15 +2588,13 @@ private void setupEphemeralDeviceMapping(Image image, List d
 
     @NonNull
     private static List makeImageAttributeList(@CheckForNull String attr) {
-        return Stream.of(Util.tokenize(Util.fixNull(attr)))
-            .collect(Collectors.toList());
+        return Stream.of(Util.tokenize(Util.fixNull(attr))).collect(Collectors.toList());
     }
 
     @NonNull
-    private DescribeImagesRequest makeDescribeImagesRequest() throws AmazonClientException {
-        List imageIds = Util.fixEmptyAndTrim(ami) == null ?
-            Collections.emptyList() :
-            Collections.singletonList(ami);
+    private DescribeImagesRequest makeDescribeImagesRequest() throws SdkException {
+        List imageIds =
+                Util.fixEmptyAndTrim(ami) == null ? Collections.emptyList() : Collections.singletonList(ami);
         List owners = makeImageAttributeList(amiOwners);
         List users = makeImageAttributeList(amiUsers);
         List filters = EC2Filter.toFilterList(amiFilters);
@@ -1359,35 +2602,40 @@ private DescribeImagesRequest makeDescribeImagesRequest() throws AmazonClientExc
         // Raise an exception if there were no search attributes.
         // This is legal but not what anyone wants - it will
         // launch random recently created public AMIs.
-        int numAttrs = Stream.of(imageIds, owners, users, filters)
-            .collect(Collectors.summingInt(List::size));
+        int numAttrs =
+                Stream.of(imageIds, owners, users, filters).mapToInt(List::size).sum();
         if (numAttrs == 0) {
-            throw new AmazonClientException("Neither AMI ID nor AMI search attributes provided");
+            throw SdkException.builder()
+                    .message("Neither AMI ID nor AMI search attributes provided")
+                    .build();
         }
 
-        return new DescribeImagesRequest()
-            .withImageIds(imageIds)
-            .withOwners(owners)
-            .withExecutableUsers(users)
-            .withFilters(filters);
+        return DescribeImagesRequest.builder()
+                .imageIds(imageIds)
+                .owners(owners)
+                .executableUsers(users)
+                .filters(filters)
+                .build();
     }
 
     @NonNull
-    private Image getImage() throws AmazonClientException {
+    private Image getImage() throws SdkException {
         DescribeImagesRequest request = makeDescribeImagesRequest();
 
         LOGGER.info("Getting image for request " + request);
-        List images = getParent().connect().describeImages(request).getImages();
+        List images =
+                new ArrayList<>(getParent().connect().describeImages(request).images());
         if (images.isEmpty()) {
-            throw new AmazonClientException("Unable to find image for request " + request);
+            throw SdkException.builder()
+                    .message("Unable to find image for request " + request)
+                    .build();
         }
 
         // Sort in reverse by creation date to get latest image
-        images.sort(Comparator.comparing(Image::getCreationDate).reversed());
+        images.sort(Comparator.comparing(Image::creationDate).reversed());
         return images.get(0);
     }
 
-
     private void setupCustomDeviceMapping(List deviceMappings) {
         if (StringUtils.isNotBlank(customDeviceMapping)) {
             deviceMappings.addAll(DeviceMappingParser.parse(customDeviceMapping));
@@ -1403,40 +2651,46 @@ private List provisionSpot(Image image, int number, EnumSet String.format("Chose subnetId %s", subnetId));
             if (StringUtils.isNotBlank(subnetId)) {
-                net.setSubnetId(subnetId);
+                netBuilder.subnetId(subnetId);
 
                 /*
                  * If we have a subnet ID then we can only use VPC security groups
@@ -1444,116 +2698,153 @@ private List provisionSpot(Image image, int number, EnumSet groupIds = getEc2SecurityGroups(ec2);
                     if (!groupIds.isEmpty()) {
-                        net.setGroups(groupIds);
+                        netBuilder.groups(groupIds);
                     }
                 }
             } else {
                 if (!securityGroupSet.isEmpty()) {
-                    List groupIds = getSecurityGroupsBy("group-name", securityGroupSet, ec2)
-                                                    .getSecurityGroups()
-                                                    .stream().map(SecurityGroup::getGroupId)
-                                                    .collect(Collectors.toList());
-                    net.setGroups(groupIds);
+                    List groupIds =
+                            getSecurityGroupsBy("group-name", securityGroupSet, ec2).securityGroups().stream()
+                                    .map(SecurityGroup::groupId)
+                                    .collect(Collectors.toList());
+                    netBuilder.groups(groupIds);
                 }
             }
 
             String userDataString = Base64.getEncoder().encodeToString(userData.getBytes(StandardCharsets.UTF_8));
 
-            launchSpecification.setUserData(userDataString);
-            launchSpecification.setKeyName(keyPair.getKeyName());
-            launchSpecification.setInstanceType(type.toString());
+            launchSpecificationBuilder.userData(userDataString);
+            launchSpecificationBuilder.keyName(keyPair.getKeyPairInfo().keyName());
+            launchSpecificationBuilder.instanceType(type);
+
+            switch (getAssociateIPStrategy()) {
+                case PUBLIC_IP:
+                    netBuilder.associatePublicIpAddress(true);
+                    break;
+                case PRIVATE_IP:
+                case DEFAULT:
+                    netBuilder.associatePublicIpAddress(false);
+                    break;
+                case SUBNET:
+                    break;
+            }
 
-            net.setAssociatePublicIpAddress(getAssociatePublicIp());
-            net.setDeviceIndex(0);
-            launchSpecification.withNetworkInterfaces(net);
+            netBuilder.deviceIndex(0);
+            launchSpecificationBuilder.networkInterfaces(netBuilder.build());
 
             HashSet instTags = buildTags(EC2Cloud.EC2_SLAVE_TYPE_SPOT);
 
             if (StringUtils.isNotBlank(getIamInstanceProfile())) {
-                launchSpecification.setIamInstanceProfile(new IamInstanceProfileSpecification().withArn(getIamInstanceProfile()));
+                launchSpecificationBuilder.iamInstanceProfile(IamInstanceProfileSpecification.builder()
+                        .arn(getIamInstanceProfile())
+                        .build());
             }
 
-            setupBlockDeviceMappings(image, launchSpecification.getBlockDeviceMappings());
+            launchSpecificationBuilder.blockDeviceMappings(getBlockDeviceMappings(image));
 
-            spotRequest.setLaunchSpecification(launchSpecification);
+            spotRequestBuilder.launchSpecification(launchSpecificationBuilder.build());
 
             if (getSpotBlockReservationDuration() != 0) {
-                spotRequest.setBlockDurationMinutes(getSpotBlockReservationDuration() * 60);
+                spotRequestBuilder.blockDurationMinutes(getSpotBlockReservationDuration() * 60);
             }
 
-            RequestSpotInstancesResult reqResult;
+            RequestSpotInstancesResponse reqResult;
             try {
                 // Make the request for a new Spot instance
-                reqResult = ec2.requestSpotInstances(spotRequest);
-            } catch (AmazonEC2Exception e) {
-                if (spotConfig.getFallbackToOndemand() && e.getErrorCode().equals("MaxSpotInstanceCountExceeded")) {
-                    logProvisionInfo("There is no spot capacity available matching your request, falling back to on-demand instance.");
+                reqResult = ec2.requestSpotInstances(spotRequestBuilder.build());
+            } catch (Ec2Exception e) {
+                if (spotConfig.getFallbackToOndemand()
+                        && "MaxSpotInstanceCountExceeded"
+                                .equals(e.awsErrorDetails().errorCode())) {
+                    logProvisionInfo(
+                            "There is no spot capacity available matching your request, falling back to on-demand instance.");
                     return provisionOndemand(image, number, provisionOptions);
                 } else {
                     throw e;
                 }
             }
 
-            List reqInstances = reqResult.getSpotInstanceRequests();
+            List reqInstances = reqResult.spotInstanceRequests();
             if (reqInstances.isEmpty()) {
-                throw new AmazonClientException("No spot instances found");
+                throw SdkException.builder().message("No spot instances found").build();
             }
 
             List slaves = new ArrayList<>(reqInstances.size());
-            for(SpotInstanceRequest spotInstReq : reqInstances) {
+            for (SpotInstanceRequest spotInstReq : reqInstances) {
                 if (spotInstReq == null) {
-                    throw new AmazonClientException("Spot instance request is null");
+                    throw SdkException.builder()
+                            .message("Spot instance request is null")
+                            .build();
                 }
-                String slaveName = spotInstReq.getSpotInstanceRequestId();
+                String slaveName = spotInstReq.spotInstanceRequestId();
 
                 if (spotConfig.getFallbackToOndemand()) {
-                    for (int i = 0; i < 2 && spotInstReq.getStatus().getCode().equals("pending-evaluation"); i++) {
+                    for (int i = 0; i < 2 && spotInstReq.status().code().equals("pending-evaluation"); i++) {
                         LOGGER.info("Spot request " + slaveName + " is still pending evaluation");
                         Thread.sleep(5000);
                         LOGGER.info("Fetching info about spot request " + slaveName);
-                        DescribeSpotInstanceRequestsRequest describeRequest = new DescribeSpotInstanceRequestsRequest().withSpotInstanceRequestIds(slaveName);
-                        spotInstReq = ec2.describeSpotInstanceRequests(describeRequest).getSpotInstanceRequests().get(0);
+                        DescribeSpotInstanceRequestsRequest describeRequest =
+                                DescribeSpotInstanceRequestsRequest.builder()
+                                        .spotInstanceRequestIds(slaveName)
+                                        .build();
+                        spotInstReq = ec2.describeSpotInstanceRequests(describeRequest)
+                                .spotInstanceRequests()
+                                .get(0);
                     }
 
-                    List spotRequestBadCodes = Arrays.asList("capacity-not-available", "capacity-oversubscribed", "price-too-low");
-                    if (spotRequestBadCodes.contains(spotInstReq.getStatus().getCode())) {
-                        LOGGER.info("There is no spot capacity available matching your request, falling back to on-demand instance.");
-                        List requestsToCancel = reqInstances.stream().map(SpotInstanceRequest::getSpotInstanceRequestId).collect(Collectors.toList());
-                        CancelSpotInstanceRequestsRequest cancelRequest = new CancelSpotInstanceRequestsRequest(requestsToCancel);
+                    List spotRequestBadCodes =
+                            Arrays.asList("capacity-not-available", "capacity-oversubscribed", "price-too-low");
+                    if (spotRequestBadCodes.contains(spotInstReq.status().code())) {
+                        LOGGER.info(
+                                "There is no spot capacity available matching your request, falling back to on-demand instance.");
+                        List requestsToCancel = reqInstances.stream()
+                                .map(SpotInstanceRequest::spotInstanceRequestId)
+                                .collect(Collectors.toList());
+                        CancelSpotInstanceRequestsRequest cancelRequest = CancelSpotInstanceRequestsRequest.builder()
+                                .spotInstanceRequestIds(requestsToCancel)
+                                .build();
                         ec2.cancelSpotInstanceRequests(cancelRequest);
                         return provisionOndemand(image, number, provisionOptions);
                     }
                 }
 
                 // Now that we have our Spot request, we can set tags on it
-                updateRemoteTags(ec2, instTags, "InvalidSpotInstanceRequestID.NotFound", spotInstReq.getSpotInstanceRequestId());
+                updateRemoteTags(
+                        ec2, instTags, "InvalidSpotInstanceRequestID.NotFound", spotInstReq.spotInstanceRequestId());
 
                 // That was a remote request - we should also update our local instance data
-                spotInstReq.setTags(instTags);
+                SpotInstanceRequest.Builder spotInstReqBuilder = spotInstReq.toBuilder();
+                spotInstReqBuilder.tags(instTags);
 
-                LOGGER.info("Spot instance id in provision: " + spotInstReq.getSpotInstanceRequestId());
+                LOGGER.info("Spot instance id in provision: " + spotInstReq.spotInstanceRequestId());
 
-                slaves.add(newSpotSlave(spotInstReq));
+                slaves.add(newSpotSlave(spotInstReqBuilder.build()));
             }
 
             return slaves;
 
         } catch (FormException e) {
             throw new AssertionError(); // we should have discovered all
-                                        // configuration issues upfront
+            // configuration issues upfront
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
             throw new RuntimeException(e);
         }
     }
 
-    private void setupBlockDeviceMappings(Image image, List blockDeviceMappings) {
-        setupRootDevice(image, blockDeviceMappings);
+    private List getBlockDeviceMappings(Image image) {
+        List newMappings = new ArrayList<>(image.blockDeviceMappings());
+
+        setupRootDevice(image, newMappings);
+
         if (useEphemeralDevices) {
-            setupEphemeralDeviceMapping(image, blockDeviceMappings);
+            newMappings.addAll(getNewEphemeralDeviceMapping(image));
         } else {
-            setupCustomDeviceMapping(blockDeviceMappings);
+            if (StringUtils.isNotBlank(customDeviceMapping)) {
+                newMappings.addAll(DeviceMappingParser.parse(customDeviceMapping));
+            }
         }
+        return newMappings;
     }
 
     private HashSet buildTags(String slaveType) {
@@ -1562,7 +2853,7 @@ private HashSet buildTags(String slaveType) {
         HashSet instTags = new HashSet<>();
         if (tags != null && !tags.isEmpty()) {
             for (EC2Tag t : tags) {
-                instTags.add(new Tag(t.getName(), t.getValue()));
+                instTags.add(Tag.builder().key(t.getName()).value(t.getValue()).build());
                 if (StringUtils.equals(t.getName(), EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE)) {
                     hasCustomTypeTag = true;
                 }
@@ -1572,73 +2863,86 @@ private HashSet buildTags(String slaveType) {
             }
         }
         if (!hasCustomTypeTag) {
-            instTags.add(new Tag(EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE, EC2Cloud.getSlaveTypeTagValue(
-                    slaveType, description)));
+            instTags.add(Tag.builder()
+                    .key(EC2Tag.TAG_NAME_JENKINS_SLAVE_TYPE)
+                    .value(EC2Cloud.getSlaveTypeTagValue(slaveType, description))
+                    .build());
         }
         JenkinsLocationConfiguration jenkinsLocation = JenkinsLocationConfiguration.get();
         if (!hasJenkinsServerUrlTag && jenkinsLocation.getUrl() != null) {
-            instTags.add(new Tag(EC2Tag.TAG_NAME_JENKINS_SERVER_URL, jenkinsLocation.getUrl()));
+            instTags.add(Tag.builder()
+                    .key(EC2Tag.TAG_NAME_JENKINS_SERVER_URL)
+                    .value(jenkinsLocation.getUrl())
+                    .build());
+        }
+
+        if (parent != null && StringUtils.isNotBlank(parent.name)) {
+            instTags.add(Tag.builder()
+                    .key(EC2Tag.TAG_NAME_JENKINS_CLOUD_NAME)
+                    .value(parent.name)
+                    .build());
         }
+
         return instTags;
     }
 
     protected EC2OndemandSlave newOndemandSlave(Instance inst) throws FormException, IOException {
         EC2AgentConfig.OnDemand config = new EC2AgentConfig.OnDemandBuilder()
-            .withName(getSlaveName(inst.getInstanceId()))
-            .withInstanceId(inst.getInstanceId())
-            .withDescription(description)
-            .withRemoteFS(remoteFS)
-            .withNumExecutors(getNumExecutors())
-            .withLabelString(labels)
-            .withMode(mode)
-            .withInitScript(initScript)
-            .withTmpDir(tmpDir)
-            .withNodeProperties(nodeProperties.toList())
-            .withRemoteAdmin(remoteAdmin)
-            .withJavaPath(javaPath)
-            .withJvmopts(jvmopts)
-            .withStopOnTerminate(stopOnTerminate)
-            .withIdleTerminationMinutes(idleTerminationMinutes)
-            .withPublicDNS(inst.getPublicDnsName())
-            .withPrivateDNS(inst.getPrivateDnsName())
-            .withTags(EC2Tag.fromAmazonTags(inst.getTags()))
-            .withCloudName(parent.name)
-            .withLaunchTimeout(getLaunchTimeout())
-            .withAmiType(amiType)
-            .withConnectionStrategy(connectionStrategy)
-            .withMaxTotalUses(maxTotalUses)
-            .withTenancyAttribute(tenancy)
-            .withMetadataSupported(metadataSupported)
-            .withMetadataEndpointEnabled(metadataEndpointEnabled)
-            .withMetadataTokensRequired(metadataTokensRequired)
-            .withMetadataHopsLimit(metadataHopsLimit)
-            .build();
+                .withName(getSlaveName(inst.instanceId()))
+                .withInstanceId(inst.instanceId())
+                .withDescription(description)
+                .withRemoteFS(remoteFS)
+                .withNumExecutors(getNumExecutors())
+                .withLabelString(labels)
+                .withMode(mode)
+                .withInitScript(initScript)
+                .withTmpDir(tmpDir)
+                .withNodeProperties(nodeProperties.toList())
+                .withRemoteAdmin(remoteAdmin)
+                .withJavaPath(javaPath)
+                .withJvmopts(jvmopts)
+                .withStopOnTerminate(stopOnTerminate)
+                .withIdleTerminationMinutes(idleTerminationMinutes)
+                .withPublicDNS(inst.publicDnsName())
+                .withPrivateDNS(inst.privateDnsName())
+                .withTags(EC2Tag.fromAmazonTags(inst.tags()))
+                .withCloudName(parent.name)
+                .withLaunchTimeout(getLaunchTimeout())
+                .withAmiType(amiType)
+                .withConnectionStrategy(connectionStrategy)
+                .withMaxTotalUses(maxTotalUses)
+                .withTenancyAttribute(tenancy)
+                .withMetadataSupported(metadataSupported)
+                .withMetadataEndpointEnabled(metadataEndpointEnabled)
+                .withMetadataTokensRequired(metadataTokensRequired)
+                .withMetadataHopsLimit(metadataHopsLimit)
+                .build();
         return EC2AgentFactory.getInstance().createOnDemandAgent(config);
     }
 
     protected EC2SpotSlave newSpotSlave(SpotInstanceRequest sir) throws FormException, IOException {
         EC2AgentConfig.Spot config = new EC2AgentConfig.SpotBuilder()
-            .withName(getSlaveName(sir.getSpotInstanceRequestId()))
-            .withSpotInstanceRequestId(sir.getSpotInstanceRequestId())
-            .withDescription(description)
-            .withRemoteFS(remoteFS)
-            .withNumExecutors(getNumExecutors())
-            .withMode(mode)
-            .withInitScript(initScript)
-            .withTmpDir(tmpDir)
-            .withLabelString(labels)
-            .withNodeProperties(nodeProperties.toList())
-            .withRemoteAdmin(remoteAdmin)
-            .withJavaPath(javaPath)
-            .withJvmopts(jvmopts)
-            .withIdleTerminationMinutes(idleTerminationMinutes)
-            .withTags(EC2Tag.fromAmazonTags(sir.getTags()))
-            .withCloudName(parent.name)
-            .withLaunchTimeout(getLaunchTimeout())
-            .withAmiType(amiType)
-            .withConnectionStrategy(connectionStrategy)
-            .withMaxTotalUses(maxTotalUses)
-            .build();
+                .withName(getSlaveName(sir.spotInstanceRequestId()))
+                .withSpotInstanceRequestId(sir.spotInstanceRequestId())
+                .withDescription(description)
+                .withRemoteFS(remoteFS)
+                .withNumExecutors(getNumExecutors())
+                .withMode(mode)
+                .withInitScript(initScript)
+                .withTmpDir(tmpDir)
+                .withLabelString(labels)
+                .withNodeProperties(nodeProperties.toList())
+                .withRemoteAdmin(remoteAdmin)
+                .withJavaPath(javaPath)
+                .withJvmopts(jvmopts)
+                .withIdleTerminationMinutes(idleTerminationMinutes)
+                .withTags(EC2Tag.fromAmazonTags(sir.tags()))
+                .withCloudName(parent.name)
+                .withLaunchTimeout(getLaunchTimeout())
+                .withAmiType(amiType)
+                .withConnectionStrategy(connectionStrategy)
+                .withMaxTotalUses(maxTotalUses)
+                .build();
         return EC2AgentFactory.getInstance().createSpotAgent(config);
     }
 
@@ -1646,15 +2950,20 @@ protected EC2SpotSlave newSpotSlave(SpotInstanceRequest sir) throws FormExceptio
      * Get a KeyPair from the configured information for the agent template
      */
     @CheckForNull
-    private KeyPair getKeyPair(AmazonEC2 ec2) throws IOException, AmazonClientException {
+    private KeyPair getKeyPair(Ec2Client ec2) throws IOException, SdkException {
         EC2PrivateKey ec2PrivateKey = getParent().resolvePrivateKey();
         if (ec2PrivateKey == null) {
-            throw new AmazonClientException("No keypair credential found. Please configure a credential in the Jenkins configuration.");
+            throw SdkException.builder()
+                    .message("No keypair credential found. Please configure a credential in the Jenkins configuration.")
+                    .build();
         }
         KeyPair keyPair = ec2PrivateKey.find(ec2);
         if (keyPair == null) {
-            throw new AmazonClientException("No matching keypair found on EC2. Is the EC2 private key a valid one?");
+            throw SdkException.builder()
+                    .message("No matching keypair found on EC2. Is the EC2 private key a valid one?")
+                    .build();
         }
+        LOGGER.fine("found matching keypair");
         return keyPair;
     }
 
@@ -1668,20 +2977,22 @@ private KeyPair getKeyPair(AmazonEC2 ec2) throws IOException, AmazonClientExcept
      * @param params
      * @throws InterruptedException
      */
-    private void updateRemoteTags(AmazonEC2 ec2, Collection instTags, String catchErrorCode, String... params)
+    private void updateRemoteTags(
+            Ec2Client ec2, Collection instTags, @NonNull String catchErrorCode, String... params)
             throws InterruptedException {
         for (int i = 0; i < 5; i++) {
             try {
-                CreateTagsRequest tagRequest = new CreateTagsRequest();
-                tagRequest.withResources(params).setTags(instTags);
-                ec2.createTags(tagRequest);
+                ec2.createTags(CreateTagsRequest.builder()
+                        .resources(params)
+                        .tags(instTags)
+                        .build());
                 break;
-            } catch (AmazonServiceException e) {
-                if (e.getErrorCode().equals(catchErrorCode)) {
+            } catch (AwsServiceException e) {
+                if (catchErrorCode.equals(e.awsErrorDetails().errorCode())) {
                     Thread.sleep(5000);
                     continue;
                 }
-                LOGGER.log(Level.SEVERE, e.getErrorMessage(), e);
+                LOGGER.log(Level.SEVERE, e.awsErrorDetails().errorMessage(), e);
             }
         }
     }
@@ -1689,62 +3000,84 @@ private void updateRemoteTags(AmazonEC2 ec2, Collection instTags, String ca
     /**
      * Get a list of security group ids for the agent
      */
-    private List getEc2SecurityGroups(AmazonEC2 ec2) throws AmazonClientException {
+    private List getEc2SecurityGroups(Ec2Client ec2) throws SdkException {
+        LOGGER.log(
+                Level.FINE,
+                () -> String.format(
+                        "Get security group %s for EC2Cloud %s with currentSubnetId %s",
+                        securityGroupSet, this.getParent().name, getCurrentSubnetId()));
         List groupIds = new ArrayList<>();
-
-        DescribeSecurityGroupsResult groupResult = getSecurityGroupsBy("group-name", securityGroupSet, ec2);
-        if (groupResult.getSecurityGroups().size() == 0) {
+        DescribeSecurityGroupsResponse groupResult = getSecurityGroupsBy("group-name", securityGroupSet, ec2);
+        if (groupResult.securityGroups().isEmpty()) {
             groupResult = getSecurityGroupsBy("group-id", securityGroupSet, ec2);
         }
 
-        for (SecurityGroup group : groupResult.getSecurityGroups()) {
-            if (group.getVpcId() != null && !group.getVpcId().isEmpty()) {
+        for (SecurityGroup group : groupResult.securityGroups()) {
+            LOGGER.log(
+                    Level.FINE,
+                    () -> String.format(
+                            "Checking security group %s (vpc-id = %s, subnet-id = %s)",
+                            group.groupId(), group.vpcId(), getCurrentSubnetId()));
+            if (group.vpcId() != null && !group.vpcId().isEmpty()) {
                 List filters = new ArrayList<>();
-                filters.add(new Filter("vpc-id").withValues(group.getVpcId()));
-                filters.add(new Filter("state").withValues("available"));
-                filters.add(new Filter("subnet-id").withValues(getCurrentSubnetId()));
-
-                DescribeSubnetsRequest subnetReq = new DescribeSubnetsRequest();
-                subnetReq.withFilters(filters);
-                DescribeSubnetsResult subnetResult = ec2.describeSubnets(subnetReq);
-
-                List subnets = subnetResult.getSubnets();
+                filters.add(
+                        Filter.builder().name("vpc-id").values(group.vpcId()).build());
+                filters.add(Filter.builder().name("state").values("available").build());
+                filters.add(Filter.builder()
+                        .name("subnet-id")
+                        .values(getCurrentSubnetId())
+                        .build());
+
+                DescribeSubnetsResponse subnetResult = ec2.describeSubnets(
+                        DescribeSubnetsRequest.builder().filters(filters).build());
+
+                List subnets = subnetResult.subnets();
                 if (subnets != null && !subnets.isEmpty()) {
-                    groupIds.add(group.getGroupId());
+                    LOGGER.log(Level.FINE, () -> "Adding security group");
+                    groupIds.add(group.groupId());
                 }
             }
         }
 
         if (securityGroupSet.size() != groupIds.size()) {
-            throw new AmazonClientException("Security groups must all be VPC security groups to work in a VPC context");
+            throw SdkException.builder()
+                    .message("Security groups must all be VPC security groups to work in a VPC context")
+                    .build();
         }
 
         return groupIds;
     }
 
-    private DescribeSecurityGroupsResult getSecurityGroupsBy(String filterName, Set filterValues, AmazonEC2 ec2) {
-        DescribeSecurityGroupsRequest groupReq = new DescribeSecurityGroupsRequest();
-        groupReq.withFilters(new Filter(filterName).withValues(filterValues));
+    private DescribeSecurityGroupsResponse getSecurityGroupsBy(
+            String filterName, Set filterValues, Ec2Client ec2) {
+        DescribeSecurityGroupsRequest groupReq = DescribeSecurityGroupsRequest.builder()
+                .filters(Filter.builder().name(filterName).values(filterValues).build())
+                .build();
         return ec2.describeSecurityGroups(groupReq);
     }
 
     /**
      * Provisions a new EC2 agent based on the currently running instance on EC2, instead of starting a new one.
      */
-    public EC2AbstractSlave attach(String instanceId, TaskListener listener) throws AmazonClientException, IOException {
+    public EC2AbstractSlave attach(String instanceId, TaskListener listener) throws SdkException, IOException {
         PrintStream logger = listener.getLogger();
-        AmazonEC2 ec2 = getParent().connect();
+        Ec2Client ec2 = getParent().connect();
 
         try {
             logger.println("Attaching to " + instanceId);
             LOGGER.info("Attaching to " + instanceId);
-            DescribeInstancesRequest request = new DescribeInstancesRequest();
-            request.setInstanceIds(Collections.singletonList(instanceId));
-            Instance inst = ec2.describeInstances(request).getReservations().get(0).getInstances().get(0);
+            DescribeInstancesRequest request = DescribeInstancesRequest.builder()
+                    .instanceIds(Collections.singletonList(instanceId))
+                    .build();
+            Instance inst = ec2.describeInstances(request)
+                    .reservations()
+                    .get(0)
+                    .instances()
+                    .get(0);
             return newOndemandSlave(inst);
         } catch (FormException e) {
             throw new AssertionError(); // we should have discovered all
-                                        // configuration issues upfront
+            // configuration issues upfront
         }
     }
 
@@ -1753,9 +3086,9 @@ public EC2AbstractSlave attach(String instanceId, TaskListener listener) throws
      */
     protected Object readResolve() {
         Jenkins j = Jenkins.getInstanceOrNull();
-         if (j != null) {
-             j.checkPermission(Jenkins.ADMINISTER);
-         }
+        if (j != null) {
+            j.checkPermission(Jenkins.ADMINISTER);
+        }
 
         securityGroupSet = parseSecurityGroups();
 
@@ -1775,9 +3108,18 @@ protected Object readResolve() {
             amiType = new UnixData(rootCommandPrefix, slaveCommandPrefix, slaveCommandSuffix, sshPort, null);
         }
 
-         // 1.43 new parameters
-        if (connectionStrategy == null )  {
-            connectionStrategy = ConnectionStrategy.backwardsCompatible(usePrivateDnsName, connectUsingPublicIp, associatePublicIp);
+        if (type != null && !type.isEmpty()) {
+            type = InstanceTypeCompat.of(type).toString();
+        }
+
+        if (associateIPStrategy == null) {
+            associateIPStrategy = AssociateIPStrategy.backwardsCompatible(associatePublicIp);
+        }
+
+        // 1.43 new parameters
+        if (connectionStrategy == null) {
+            connectionStrategy = ConnectionStrategy.backwardsCompatible(
+                    usePrivateDnsName, connectUsingPublicIp, AssociateIPStrategy.PUBLIC_IP == associateIPStrategy);
         }
 
         if (maxTotalUses == 0) {
@@ -1814,12 +3156,16 @@ protected Object readResolve() {
             metadataHopsLimit = EC2AbstractSlave.DEFAULT_METADATA_HOPS_LIMIT;
         }
         if (StringUtils.isBlank(javaPath)) {
-            javaPath = DEFAULT_JAVA_PATH;
+            javaPath = EC2AbstractSlave.DEFAULT_JAVA_PATH;
+        }
+        if (enclaveEnabled == null) {
+            enclaveEnabled = EC2AbstractSlave.DEFAULT_ENCLAVE_ENABLED;
         }
 
         return this;
     }
 
+    @Override
     public Descriptor getDescriptor() {
         return Jenkins.get().getDescriptor(getClass());
     }
@@ -1848,12 +3194,20 @@ public boolean isMacAgent() {
         return amiType.isMac();
     }
 
+    public boolean isSSHAgent() {
+        return amiType.isSSHAgent();
+    }
+
+    public boolean isWinRMAgent() {
+        return amiType.isWinRMAgent();
+    }
+
     public Secret getAdminPassword() {
-        return amiType.isWindows() ? ((WindowsData) amiType).getPassword() : Secret.fromString("");
+        return amiType.isWinRMAgent() ? ((WindowsData) amiType).getPassword() : Secret.fromString("");
     }
 
     public boolean isUseHTTPS() {
-        return amiType.isWindows() && ((WindowsData) amiType).isUseHTTPS();
+        return amiType.isWinRMAgent() && ((WindowsData) amiType).isUseHTTPS();
     }
 
     /**
@@ -1862,16 +3216,18 @@ public boolean isUseHTTPS() {
      * @param allSubnets if true, uses all subnets defined for this SlaveTemplate as the filter, else will only use the current subnet
      * @return DescribeInstancesResult of DescribeInstanceRequst constructed from this SlaveTemplate's configs
      */
-    DescribeInstancesResult getDescribeInstanceResult(AmazonEC2 ec2, boolean allSubnets) throws IOException {
-        HashMap> runInstancesRequestFilterMap = makeRunInstancesRequestAndFilters(getImage(), 1, ec2, false);
-        Map.Entry> entry = runInstancesRequestFilterMap.entrySet().iterator().next();
+    DescribeInstancesResponse getDescribeInstanceResult(Ec2Client ec2, boolean allSubnets) throws IOException {
+        HashMap> runInstancesRequestFilterMap =
+                makeRunInstancesRequestAndFilters(getImage(), 1, ec2, false);
+        Map.Entry> entry =
+                runInstancesRequestFilterMap.entrySet().iterator().next();
         List diFilters = entry.getValue();
 
         if (allSubnets) {
             /* remove any existing subnet-id filters */
             List rmvFilters = new ArrayList<>();
             for (Filter f : diFilters) {
-                if (f.getName().equals("subnet-id")) {
+                if (f.name().equals("subnet-id")) {
                     rmvFilters.add(f);
                 }
             }
@@ -1880,17 +3236,20 @@ DescribeInstancesResult getDescribeInstanceResult(AmazonEC2 ec2, boolean allSubn
             }
 
             /* Add filter using all subnets defined for this SlaveTemplate */
-            Filter subnetFilter = new Filter("subnet-id");
-            subnetFilter.setValues(Arrays.asList(getSubnetId().split(EC2_RESOURCE_ID_DELIMETERS)));
+            Filter subnetFilter = Filter.builder()
+                    .name("subnet-id")
+                    .values(Arrays.asList(getSubnetId().split(EC2_RESOURCE_ID_DELIMETERS)))
+                    .build();
             diFilters.add(subnetFilter);
         }
 
-        DescribeInstancesRequest diRequest = new DescribeInstancesRequest().withFilters(diFilters);
+        DescribeInstancesRequest diRequest =
+                DescribeInstancesRequest.builder().filters(diFilters).build();
         return ec2.describeInstances(diRequest);
     }
 
     public boolean isAllowSelfSignedCertificate() {
-        return amiType.isWindows() && ((WindowsData) amiType).isAllowSelfSignedCertificate();
+        return amiType.isWinRMAgent() && ((WindowsData) amiType).isAllowSelfSignedCertificate();
     }
 
     @Extension
@@ -1921,17 +3280,20 @@ public List> getAMITypeDescriptors() {
         @Override
         public String getHelpFile(String fieldName) {
             String p = super.getHelpFile(fieldName);
-            if (p != null)
+            if (p != null) {
                 return p;
+            }
             Descriptor slaveDescriptor = Jenkins.get().getDescriptor(EC2OndemandSlave.class);
             if (slaveDescriptor != null) {
                 p = slaveDescriptor.getHelpFile(fieldName);
-                if (p != null)
+                if (p != null) {
                     return p;
+                }
             }
             slaveDescriptor = Jenkins.get().getDescriptor(EC2SpotSlave.class);
-            if (slaveDescriptor != null)
+            if (slaveDescriptor != null) {
                 return slaveDescriptor.getHelpFile(fieldName);
+            }
             return null;
         }
 
@@ -1946,36 +3308,50 @@ public FormValidation doCheckDescription(@QueryParameter String value) {
             }
         }
 
+        @RequirePOST
+        @SuppressWarnings("lgtm[jenkins/no-permission-check]")
+        public FormValidation doValidateType(@QueryParameter String value) {
+            InstanceType instanceType = InstanceType.fromValue(value);
+
+            if (instanceType == InstanceType.UNKNOWN_TO_SDK_VERSION) {
+                return FormValidation.error("Instance type unknown to SDK version");
+            }
+
+            return FormValidation.ok();
+        }
+
         /***
          * Check that the AMI requested is available in the cloud and can be used.
          */
         @RequirePOST
-        public FormValidation doValidateAmi(@QueryParameter boolean useInstanceProfileForCredentials,
-                @QueryParameter String credentialsId, @QueryParameter String ec2endpoint,
-                @QueryParameter String region, final @QueryParameter String ami, @QueryParameter String roleArn,
-                @QueryParameter String roleSessionName) throws IOException {
+        public FormValidation doValidateAmi(
+                @QueryParameter boolean useInstanceProfileForCredentials,
+                @QueryParameter String credentialsId,
+                @QueryParameter String region,
+                @QueryParameter String altEC2Endpoint,
+                final @QueryParameter String ami,
+                @QueryParameter String roleArn,
+                @QueryParameter String roleSessionName)
+                throws IOException {
             checkPermission(EC2Cloud.PROVISION);
-            AWSCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider(useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region);
-            AmazonEC2 ec2;
-            if (region != null) {
-                ec2 = AmazonEC2Factory.getInstance().connect(credentialsProvider, AmazonEC2Cloud.getEc2EndpointUrl(region));
-            } else {
-                ec2 = AmazonEC2Factory.getInstance().connect(credentialsProvider, new URL(ec2endpoint));
-            }
+            AwsCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider(
+                    useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region);
+            Ec2Client ec2 = AmazonEC2Factory.getInstance()
+                    .connect(credentialsProvider, EC2Cloud.parseRegion(region), EC2Cloud.parseEndpoint(altEC2Endpoint));
             try {
                 Image img = CloudHelper.getAmiImage(ec2, ami);
                 if (img == null) {
                     return FormValidation.error("No such AMI, or not usable with this accessId: " + ami);
                 }
-                String ownerAlias = img.getImageOwnerAlias();
-                return FormValidation.ok(img.getImageLocation() + (ownerAlias != null ? " by " + ownerAlias : ""));
-            } catch (AmazonClientException e) {
+                String ownerAlias = img.imageOwnerAlias();
+                return FormValidation.ok(img.imageLocation() + (ownerAlias != null ? " by " + ownerAlias : ""));
+            } catch (SdkException e) {
                 return FormValidation.error(e.getMessage());
             }
         }
 
         private void checkPermission(Permission p) {
-            final EC2Cloud ancestorObject = Stapler.getCurrentRequest().findAncestorObject(EC2Cloud.class);
+            final EC2Cloud ancestorObject = Stapler.getCurrentRequest2().findAncestorObject(EC2Cloud.class);
             if (ancestorObject != null) {
                 ancestorObject.checkPermission(p);
             } else {
@@ -1995,12 +3371,14 @@ public FormValidation doCheckLabelString(@QueryParameter String value, @QueryPar
 
         @POST
         public FormValidation doCheckIdleTerminationMinutes(@QueryParameter String value) {
-            if (value == null || value.trim().isEmpty())
+            if (value == null || value.trim().isEmpty()) {
                 return FormValidation.ok();
+            }
             try {
                 int val = Integer.parseInt(value);
-                if (val >= -59)
+                if (val >= -59) {
                     return FormValidation.ok();
+                }
             } catch (NumberFormatException nfe) {
             }
             return FormValidation.error("Idle Termination time must be a greater than -59 (or null)");
@@ -2010,17 +3388,20 @@ public FormValidation doCheckIdleTerminationMinutes(@QueryParameter String value
         public FormValidation doCheckMaxTotalUses(@QueryParameter String value) {
             try {
                 int val = Integer.parseInt(value);
-                if (val >= -1)
+                if (val >= -1) {
                     return FormValidation.ok();
+                }
             } catch (NumberFormatException nfe) {
             }
             return FormValidation.error("Maximum Total Uses must be greater or equal to -1");
         }
 
         @POST
-        public FormValidation doCheckMinimumNumberOfInstances(@QueryParameter String value, @QueryParameter String instanceCapStr) {
-            if (value == null || value.trim().isEmpty())
+        public FormValidation doCheckMinimumNumberOfInstances(
+                @QueryParameter String value, @QueryParameter String instanceCapStr) {
+            if (value == null || value.trim().isEmpty()) {
                 return FormValidation.ok();
+            }
             try {
                 int val = Integer.parseInt(value);
                 if (val >= 0) {
@@ -2031,9 +3412,8 @@ public FormValidation doCheckMinimumNumberOfInstances(@QueryParameter String val
                         instanceCap = Integer.MAX_VALUE;
                     }
                     if (val > instanceCap) {
-                        return FormValidation
-                          .error("Minimum number of instances must not be larger than AMI Instance Cap %d",
-                            instanceCap);
+                        return FormValidation.error(
+                                "Minimum number of instances must not be larger than AMI Instance Cap %d", instanceCap);
                     }
                     return FormValidation.ok();
                 }
@@ -2064,23 +3444,27 @@ public FormValidation doCheckMinimumNoInstancesActiveTimeRangeTo(@QueryParameter
 
         // For some reason, all days will validate against this method so no need to repeat for each day.
         @POST
-        public FormValidation doCheckMonday(@QueryParameter boolean monday,
-                                            @QueryParameter boolean tuesday,
-                                            @QueryParameter boolean wednesday,
-                                            @QueryParameter boolean thursday,
-                                            @QueryParameter boolean friday,
-                                            @QueryParameter boolean saturday,
-                                            @QueryParameter boolean sunday) {
+        public FormValidation doCheckMonday(
+                @QueryParameter boolean monday,
+                @QueryParameter boolean tuesday,
+                @QueryParameter boolean wednesday,
+                @QueryParameter boolean thursday,
+                @QueryParameter boolean friday,
+                @QueryParameter boolean saturday,
+                @QueryParameter boolean sunday) {
             if (!(monday || tuesday || wednesday || thursday || friday || saturday || sunday)) {
-                return FormValidation.warning("At least one day should be checked or minimum number of instances won't be active");
+                return FormValidation.warning(
+                        "At least one day should be checked or minimum number of instances won't be active");
             }
             return FormValidation.ok();
         }
 
         @POST
-        public FormValidation doCheckMinimumNumberOfSpareInstances(@QueryParameter String value, @QueryParameter String instanceCapStr) {
-            if (value == null || value.trim().isEmpty())
+        public FormValidation doCheckMinimumNumberOfSpareInstances(
+                @QueryParameter String value, @QueryParameter String instanceCapStr) {
+            if (value == null || value.trim().isEmpty()) {
                 return FormValidation.ok();
+            }
             try {
                 int val = Integer.parseInt(value);
                 if (val >= 0) {
@@ -2091,9 +3475,9 @@ public FormValidation doCheckMinimumNumberOfSpareInstances(@QueryParameter Strin
                         instanceCap = Integer.MAX_VALUE;
                     }
                     if (val > instanceCap) {
-                        return FormValidation
-                          .error("Minimum number of spare instances must not be larger than AMI Instance Cap %d",
-                            instanceCap);
+                        return FormValidation.error(
+                                "Minimum number of spare instances must not be larger than AMI Instance Cap %d",
+                                instanceCap);
                     }
                     return FormValidation.ok();
                 }
@@ -2104,12 +3488,14 @@ public FormValidation doCheckMinimumNumberOfSpareInstances(@QueryParameter Strin
 
         @POST
         public FormValidation doCheckInstanceCapStr(@QueryParameter String value) {
-            if (value == null || value.trim().isEmpty())
+            if (value == null || value.trim().isEmpty()) {
                 return FormValidation.ok();
+            }
             try {
                 int val = Integer.parseInt(value);
-                if (val > 0)
+                if (val > 0) {
                     return FormValidation.ok();
+                }
             } catch (NumberFormatException nfe) {
             }
             return FormValidation.error("InstanceCap must be a non-negative integer (or null)");
@@ -2120,12 +3506,14 @@ public FormValidation doCheckInstanceCapStr(@QueryParameter String value) {
          */
         @POST
         public FormValidation doCheckSpotBlockReservationDurationStr(@QueryParameter String value) {
-            if (value == null || value.trim().isEmpty())
+            if (value == null || value.trim().isEmpty()) {
                 return FormValidation.ok();
+            }
             try {
                 int val = Integer.parseInt(value);
-                if (val >= 0 && val <= 6)
+                if (val >= 0 && val <= 6) {
                     return FormValidation.ok();
+                }
             } catch (NumberFormatException nfe) {
             }
             return FormValidation.error("Spot Block Reservation Duration must be an integer between 0 & 6");
@@ -2133,24 +3521,30 @@ public FormValidation doCheckSpotBlockReservationDurationStr(@QueryParameter Str
 
         @POST
         public FormValidation doCheckLaunchTimeoutStr(@QueryParameter String value) {
-            if (value == null || value.trim().isEmpty())
+            if (value == null || value.trim().isEmpty()) {
                 return FormValidation.ok();
+            }
             try {
                 int val = Integer.parseInt(value);
-                if (val >= 0)
+                if (val >= 0) {
                     return FormValidation.ok();
+                }
             } catch (NumberFormatException nfe) {
             }
             return FormValidation.error("Launch Timeout must be a non-negative integer (or null)");
         }
 
         @RequirePOST
-        public ListBoxModel doFillZoneItems(@QueryParameter boolean useInstanceProfileForCredentials,
-                @QueryParameter String credentialsId, @QueryParameter String region, @QueryParameter String roleArn,
+        public ListBoxModel doFillZoneItems(
+                @QueryParameter boolean useInstanceProfileForCredentials,
+                @QueryParameter String credentialsId,
+                @QueryParameter String region,
+                @QueryParameter String roleArn,
                 @QueryParameter String roleSessionName)
                 throws IOException, ServletException {
             checkPermission(EC2Cloud.PROVISION);
-            AWSCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider(useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region);
+            AwsCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider(
+                    useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region);
             return EC2AbstractSlave.fillZoneItems(credentialsProvider, region);
         }
 
@@ -2178,6 +3572,23 @@ public List getNodePropertyDescriptors() {
             return NodePropertyDescriptor.for_(NodeProperty.all(), EC2AbstractSlave.class);
         }
 
+        @RequirePOST
+        @SuppressWarnings("lgtm[jenkins/no-permission-check]")
+        public ListBoxModel doFillTypeItems(@QueryParameter String type) {
+            ListBoxModel items = new ListBoxModel();
+
+            List knownValues = InstanceType.knownValues().stream()
+                    .map(InstanceType::toString)
+                    .sorted()
+                    .collect(Collectors.toList());
+
+            for (String value : knownValues) {
+                items.add(new ListBoxModel.Option(value, value, Objects.equals(value, type)));
+            }
+
+            return items;
+        }
+
         @POST
         public ListBoxModel doFillConnectionStrategyItems(@QueryParameter String connectionStrategy) {
             return Stream.of(ConnectionStrategy.values())
@@ -2200,6 +3611,34 @@ public FormValidation doCheckConnectionStrategy(@QueryParameter String connectio
                     .orElse(FormValidation.error("Could not find selected connection strategy"));
         }
 
+        @POST
+        public ListBoxModel doFillAssociateIPStrategyItems(@QueryParameter String associateIPStrategy) {
+            checkPermission(EC2Cloud.PROVISION);
+            return Stream.of(AssociateIPStrategy.values())
+                    .map(v -> {
+                        if (v.name().equals(associateIPStrategy)) {
+                            return new ListBoxModel.Option(v.getDisplayText(), v.name(), true);
+                        } else {
+                            return new ListBoxModel.Option(v.getDisplayText(), v.name(), false);
+                        }
+                    })
+                    .collect(Collectors.toCollection(ListBoxModel::new));
+        }
+
+        @POST
+        public FormValidation doCheckAssociateIPStrategy(@QueryParameter String associateIPStrategy) {
+            checkPermission(EC2Cloud.PROVISION);
+            return Stream.of(AssociateIPStrategy.values())
+                    .filter(v -> v.name().equals(associateIPStrategy))
+                    .findFirst()
+                    .map(s -> FormValidation.ok())
+                    .orElse(FormValidation.error("Could not find selected associate IP strategy"));
+        }
+
+        public String getDefaultAssociateIPStrategy() {
+            return AssociateIPStrategy.DEFAULT.name();
+        }
+
         public String getDefaultHostKeyVerificationStrategy() {
             // new templates default to the most secure strategy
             return HostKeyVerificationStrategyEnum.CHECK_NEW_HARD.name();
@@ -2221,10 +3660,12 @@ public ListBoxModel doFillHostKeyVerificationStrategyItems(@QueryParameter Strin
         @POST
         public FormValidation doCheckHostKeyVerificationStrategy(@QueryParameter String hostKeyVerificationStrategy) {
             Stream stream = Stream.of(HostKeyVerificationStrategyEnum.values());
-            Stream filteredStream = stream.filter(v -> v.name().equals(hostKeyVerificationStrategy));
+            Stream filteredStream =
+                    stream.filter(v -> v.name().equals(hostKeyVerificationStrategy));
             Optional matched = filteredStream.findFirst();
             Optional okResult = matched.map(s -> FormValidation.ok());
-            return okResult.orElse(FormValidation.error(String.format("Could not find selected host key verification (%s)", hostKeyVerificationStrategy)));
+            return okResult.orElse(FormValidation.error(
+                    String.format("Could not find selected host key verification (%s)", hostKeyVerificationStrategy)));
         }
 
         @POST
@@ -2239,12 +3680,13 @@ public ListBoxModel doFillTenancyItems(@QueryParameter String tenancy) {
                     })
                     .collect(Collectors.toCollection(ListBoxModel::new));
         }
+
         public String getDefaultEbsEncryptRootVolume() {
             return EbsEncryptRootVolume.DEFAULT.getDisplayText();
         }
 
         @POST
-        public ListBoxModel doFillEbsEncryptRootVolumeItems(@QueryParameter String ebsEncryptRootVolume ) {
+        public ListBoxModel doFillEbsEncryptRootVolumeItems(@QueryParameter String ebsEncryptRootVolume) {
             return Stream.of(EbsEncryptRootVolume.values())
                     .map(v -> {
                         if (v.name().equals(ebsEncryptRootVolume)) {
@@ -2259,10 +3701,48 @@ public ListBoxModel doFillEbsEncryptRootVolumeItems(@QueryParameter String ebsEn
         @POST
         public FormValidation doEbsEncryptRootVolume(@QueryParameter String ebsEncryptRootVolume) {
             Stream stream = Stream.of(EbsEncryptRootVolume.values());
-            Stream filteredStream = stream.filter(v -> v.name().equals(ebsEncryptRootVolume));
+            Stream filteredStream =
+                    stream.filter(v -> v.name().equals(ebsEncryptRootVolume));
             Optional matched = filteredStream.findFirst();
             Optional okResult = matched.map(s -> FormValidation.ok());
-            return okResult.orElse(FormValidation.error(String.format("Could not find selected option (%s)", ebsEncryptRootVolume)));
+            return okResult.orElse(
+                    FormValidation.error(String.format("Could not find selected option (%s)", ebsEncryptRootVolume)));
+        }
+
+        @RequirePOST
+        public FormValidation doCheckEnclaveEnabled(
+                @QueryParameter boolean enclaveEnabled,
+                @QueryParameter String type,
+                @QueryParameter boolean useInstanceProfileForCredentials,
+                @QueryParameter String credentialsId,
+                @QueryParameter String region,
+                @QueryParameter String altEC2Endpoint,
+                @QueryParameter String roleArn,
+                @QueryParameter String roleSessionName) {
+            checkPermission(EC2Cloud.PROVISION);
+            if (enclaveEnabled && type != null && !type.isEmpty()) {
+                AwsCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider(
+                        useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region);
+                Ec2Client ec2 = AmazonEC2Factory.getInstance()
+                        .connect(
+                                credentialsProvider,
+                                EC2Cloud.parseRegion(region),
+                                EC2Cloud.parseEndpoint(altEC2Endpoint));
+                DescribeInstanceTypesRequest request = DescribeInstanceTypesRequest.builder()
+                        .instanceTypes(InstanceType.fromValue(type))
+                        .build();
+                DescribeInstanceTypesResponse response = ec2.describeInstanceTypes(request);
+                for (InstanceTypeInfo instanceTypeInfo : response.instanceTypes()) {
+                    if (!InstanceTypeHypervisor.UNKNOWN_TO_SDK_VERSION.equals(instanceTypeInfo.hypervisor())
+                            && !InstanceTypeHypervisor.NITRO.equals(instanceTypeInfo.hypervisor())) {
+                        return FormValidation.error("The selected instance type does not use the AWS Nitro System.");
+                    }
+                    if (NitroEnclavesSupport.UNSUPPORTED.equals(instanceTypeInfo.nitroEnclavesSupport())) {
+                        return FormValidation.error("The selected instance type does not support AWS Nitro Enclaves.");
+                    }
+                }
+            }
+            return FormValidation.ok();
         }
     }
 }
diff --git a/src/main/java/hudson/plugins/ec2/SpotConfiguration.java b/src/main/java/hudson/plugins/ec2/SpotConfiguration.java
index 5b278d45f..3ed9256ac 100644
--- a/src/main/java/hudson/plugins/ec2/SpotConfiguration.java
+++ b/src/main/java/hudson/plugins/ec2/SpotConfiguration.java
@@ -1,39 +1,43 @@
 package hudson.plugins.ec2;
 
-import com.amazonaws.AmazonServiceException;
-import com.amazonaws.auth.AWSCredentialsProvider;
-import com.amazonaws.services.ec2.AmazonEC2;
-import com.amazonaws.services.ec2.model.DescribeSpotPriceHistoryRequest;
-import com.amazonaws.services.ec2.model.DescribeSpotPriceHistoryResult;
-import com.amazonaws.services.ec2.model.Image;
-import com.amazonaws.services.ec2.model.InstanceType;
-import com.amazonaws.services.ec2.model.SpotPrice;
 import hudson.Extension;
+import hudson.Functions;
 import hudson.model.AbstractDescribableImpl;
 import hudson.model.Descriptor;
 import hudson.plugins.ec2.util.AmazonEC2Factory;
 import hudson.util.FormValidation;
+import jakarta.servlet.ServletException;
 import java.io.IOException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Date;
 import java.util.Objects;
-import javax.servlet.ServletException;
 import org.kohsuke.stapler.DataBoundConstructor;
 import org.kohsuke.stapler.DataBoundSetter;
 import org.kohsuke.stapler.QueryParameter;
 import org.kohsuke.stapler.interceptor.RequirePOST;
-
-import static hudson.Functions.checkPermission;
-
-public final class SpotConfiguration extends AbstractDescribableImpl  {
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.services.ec2.Ec2Client;
+import software.amazon.awssdk.services.ec2.model.DescribeSpotPriceHistoryRequest;
+import software.amazon.awssdk.services.ec2.model.DescribeSpotPriceHistoryResponse;
+import software.amazon.awssdk.services.ec2.model.Image;
+import software.amazon.awssdk.services.ec2.model.InstanceType;
+import software.amazon.awssdk.services.ec2.model.PlatformValues;
+import software.amazon.awssdk.services.ec2.model.SpotPrice;
+
+public final class SpotConfiguration extends AbstractDescribableImpl {
     public final boolean useBidPrice;
     private String spotMaxBidPrice;
     private boolean fallbackToOndemand;
     private int spotBlockReservationDuration;
 
     @Deprecated
-    public SpotConfiguration(boolean useBidPrice, String spotMaxBidPrice, boolean fallbackToOndemand, String spotBlockReservationDurationStr) {
+    public SpotConfiguration(
+            boolean useBidPrice,
+            String spotMaxBidPrice,
+            boolean fallbackToOndemand,
+            String spotBlockReservationDurationStr) {
         this.useBidPrice = useBidPrice;
         this.spotMaxBidPrice = spotMaxBidPrice;
         this.fallbackToOndemand = fallbackToOndemand;
@@ -77,7 +81,8 @@ public void setSpotBlockReservationDuration(int spotBlockReservationDuration) {
         this.spotBlockReservationDuration = spotBlockReservationDuration;
     }
 
-    @Override public boolean equals(Object obj) {
+    @Override
+    public boolean equals(Object obj) {
         if (obj == null || (this.getClass() != obj.getClass())) {
             return false;
         }
@@ -85,18 +90,20 @@ public void setSpotBlockReservationDuration(int spotBlockReservationDuration) {
 
         String normalizedBid = normalizeBid(this.spotMaxBidPrice);
         String otherNormalizedBid = normalizeBid(config.spotMaxBidPrice);
-        boolean normalizedBidsAreEqual =
-                normalizedBid == null ? (otherNormalizedBid == null) : normalizedBid.equals(otherNormalizedBid);
+        boolean normalizedBidsAreEqual = Objects.equals(normalizedBid, otherNormalizedBid);
         boolean blockReservationIsEqual = true;
         if (this.spotBlockReservationDuration != config.spotBlockReservationDuration) {
             blockReservationIsEqual = false;
         }
 
-        return this.useBidPrice == config.useBidPrice && this.fallbackToOndemand == config.fallbackToOndemand
-                && normalizedBidsAreEqual && blockReservationIsEqual;
+        return this.useBidPrice == config.useBidPrice
+                && this.fallbackToOndemand == config.fallbackToOndemand
+                && normalizedBidsAreEqual
+                && blockReservationIsEqual;
     }
 
-    @Override public int hashCode() {
+    @Override
+    public int hashCode() {
         return Objects.hash(useBidPrice, spotMaxBidPrice, fallbackToOndemand);
     }
 
@@ -109,17 +116,16 @@ public void setSpotBlockReservationDuration(int spotBlockReservationDuration) {
      */
     public static String normalizeBid(String bid) {
         try {
-            Float spotPrice = Float.parseFloat(bid);
+            float spotPrice = Float.parseFloat(bid);
 
             /* The specified bid price cannot be less than 0.001 */
             if (spotPrice < 0.001) {
                 return null;
             }
-            return spotPrice.toString();
+            return Float.toString(spotPrice);
         } catch (NumberFormatException ex) {
             return null;
         }
-
     }
 
     @Extension
@@ -133,57 +139,52 @@ public String getDisplayName() {
          * Check the current Spot price of the selected instance type for the selected region
          */
         @RequirePOST
-        public FormValidation doCurrentSpotPrice(@QueryParameter boolean useInstanceProfileForCredentials,
-                @QueryParameter String credentialsId, @QueryParameter String region,
-                @QueryParameter String type, @QueryParameter String zone, @QueryParameter String roleArn,
-                @QueryParameter String roleSessionName, @QueryParameter String ami) throws IOException, ServletException {
-
-            checkPermission(EC2Cloud.PROVISION);
+        public FormValidation doCurrentSpotPrice(
+                @QueryParameter boolean useInstanceProfileForCredentials,
+                @QueryParameter String credentialsId,
+                @QueryParameter String region,
+                @QueryParameter String altEC2Endpoint,
+                @QueryParameter String type,
+                @QueryParameter String zone,
+                @QueryParameter String roleArn,
+                @QueryParameter String roleSessionName,
+                @QueryParameter String ami)
+                throws IOException, ServletException {
+
+            Functions.checkPermission(EC2Cloud.PROVISION);
 
             String cp = "";
             String zoneStr = "";
 
             // Connect to the EC2 cloud with the access id, secret key, and
             // region queried from the created cloud
-            AWSCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider(useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region);
-            AmazonEC2 ec2 = AmazonEC2Factory.getInstance().connect(credentialsProvider, AmazonEC2Cloud.getEc2EndpointUrl(region));
+            AwsCredentialsProvider credentialsProvider = EC2Cloud.createCredentialsProvider(
+                    useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region);
+            Ec2Client ec2 =
+                    AmazonEC2Factory.getInstance().connect(credentialsProvider, EC2Cloud.parseRegion(region), null);
 
             if (ec2 != null) {
 
                 try {
                     // Build a new price history request with the currently
                     // selected type
-                    DescribeSpotPriceHistoryRequest request = new DescribeSpotPriceHistoryRequest();
+                    DescribeSpotPriceHistoryRequest.Builder requestBuilder = DescribeSpotPriceHistoryRequest.builder();
                     // If a zone is specified, set the availability zone in the
                     // request
                     // Else, proceed with no availability zone which will result
                     // with the cheapest Spot price
                     if (CloudHelper.getAvailabilityZones(ec2).contains(zone)) {
-                        request.setAvailabilityZone(zone);
+                        requestBuilder.availabilityZone(zone);
                         zoneStr = zone + " availability zone";
                     } else {
                         zoneStr = region + " region";
                     }
 
-                    /*
-                     * Iterate through the AWS instance types to see if can find a match for the databound String type.
-                     * This is necessary because the AWS API needs the instance type string formatted a particular way
-                     * to retrieve prices and the form gives us the strings in a different format. For example "T1Micro"
-                     * vs "t1.micro".
-                     */
-                    InstanceType ec2Type = null;
-
-                    for (InstanceType it : InstanceType.values()) {
-                        if (it.name().equals(type)) {
-                            ec2Type = it;
-                            break;
-                        }
-                    }
-
                     /*
                      * If the type string cannot be matched with an instance type, throw a Form error
                      */
-                    if (ec2Type == null) {
+                    InstanceType ec2Type = InstanceType.fromValue(type);
+                    if (ec2Type == null || ec2Type == InstanceType.UNKNOWN_TO_SDK_VERSION) {
                         return FormValidation.error("Could not resolve instance type: " + type);
                     }
 
@@ -191,27 +192,28 @@ public FormValidation doCurrentSpotPrice(@QueryParameter boolean useInstanceProf
                         Image img = CloudHelper.getAmiImage(ec2, ami);
                         if (img != null) {
                             Collection productDescriptions = new ArrayList<>();
-                            productDescriptions.add(img.getPlatform() == "Windows" ? "Windows" : "Linux/UNIX");
-                            request.setProductDescriptions(productDescriptions);
+                            productDescriptions.add(
+                                    img.platform() == PlatformValues.WINDOWS ? "Windows" : "Linux/UNIX");
+                            requestBuilder.productDescriptions(productDescriptions);
                         }
                     }
 
-                    Collection instanceType = new ArrayList<>();
-                    instanceType.add(ec2Type.toString());
-                    request.setInstanceTypes(instanceType);
-                    request.setStartTime(new Date());
+                    Collection instanceType = new ArrayList<>();
+                    instanceType.add(ec2Type);
+                    requestBuilder.instanceTypes(instanceType);
+                    requestBuilder.startTime(Instant.now());
 
                     // Retrieve the price history request result and store the
                     // current price
-                    DescribeSpotPriceHistoryResult result = ec2.describeSpotPriceHistory(request);
+                    DescribeSpotPriceHistoryResponse result = ec2.describeSpotPriceHistory(requestBuilder.build());
 
-                    if (!result.getSpotPriceHistory().isEmpty()) {
-                        SpotPrice currentPrice = result.getSpotPriceHistory().get(0);
+                    if (!result.spotPriceHistory().isEmpty()) {
+                        SpotPrice currentPrice = result.spotPriceHistory().get(0);
 
-                        cp = currentPrice.getSpotPrice();
+                        cp = currentPrice.spotPrice();
                     }
 
-                } catch (AmazonServiceException e) {
+                } catch (AwsServiceException e) {
                     return FormValidation.error(e.getMessage());
                 }
             }
diff --git a/src/main/java/hudson/plugins/ec2/Tenancy.java b/src/main/java/hudson/plugins/ec2/Tenancy.java
index 030d2b7dc..9912ab933 100644
--- a/src/main/java/hudson/plugins/ec2/Tenancy.java
+++ b/src/main/java/hudson/plugins/ec2/Tenancy.java
@@ -5,23 +5,20 @@ public enum Tenancy {
     Dedicated("dedicated"),
     Host("host");
 
-    private String value;
+    private final String value;
 
-    private Tenancy(String value) {
+    Tenancy(String value) {
         this.value = value;
     }
 
+    @Override
     public String toString() {
         return this.value;
     }
 
     public static Tenancy fromValue(String value) {
-        if (value != null && !"".equals(value)) {
-            Tenancy[] var1 = values();
-            int var2 = var1.length;
-
-            for(int var3 = 0; var3 < var2; ++var3) {
-                Tenancy enumEntry = var1[var3];
+        if (value != null && !value.isEmpty()) {
+            for (Tenancy enumEntry : values()) {
                 if (enumEntry.toString().equals(value)) {
                     return enumEntry;
                 }
@@ -46,4 +43,3 @@ public static Tenancy backwardsCompatible(boolean useDedicatedTenancy) {
         }
     }
 }
-
diff --git a/src/main/java/hudson/plugins/ec2/UnixData.java b/src/main/java/hudson/plugins/ec2/UnixData.java
index dba888018..6ccfd7f6b 100644
--- a/src/main/java/hudson/plugins/ec2/UnixData.java
+++ b/src/main/java/hudson/plugins/ec2/UnixData.java
@@ -2,39 +2,17 @@
 
 import hudson.Extension;
 import hudson.model.Descriptor;
-import jenkins.model.Jenkins;
-import org.apache.commons.lang.StringUtils;
 import org.kohsuke.stapler.DataBoundConstructor;
 
-public class UnixData extends AMITypeData {
-    private final String rootCommandPrefix;
-    private final String slaveCommandPrefix;
-    private final String slaveCommandSuffix;
-    private final String sshPort;
-    private final String bootDelay;
-
+public class UnixData extends SSHData {
     @DataBoundConstructor
-    public UnixData(String rootCommandPrefix, String slaveCommandPrefix, String slaveCommandSuffix, String sshPort, String bootDelay) {
-        this.rootCommandPrefix = rootCommandPrefix;
-        this.slaveCommandPrefix = slaveCommandPrefix;
-        this.slaveCommandSuffix = slaveCommandSuffix;
-        this.sshPort = sshPort;
-        this.bootDelay = bootDelay;
-
-        this.readResolve();
-    }
-
-    protected Object readResolve() {
-        Jenkins j = Jenkins.getInstanceOrNull();
-        if (j != null) {
-            j.checkPermission(Jenkins.ADMINISTER);
-        }
-        return this;
-    }
-
-    @Override
-    public boolean isWindows() {
-        return false;
+    public UnixData(
+            String rootCommandPrefix,
+            String slaveCommandPrefix,
+            String slaveCommandSuffix,
+            String sshPort,
+            String bootDelay) {
+        super(rootCommandPrefix, slaveCommandPrefix, slaveCommandSuffix, sshPort, bootDelay);
     }
 
     @Override
@@ -42,11 +20,6 @@ public boolean isUnix() {
         return true;
     }
 
-    @Override
-    public boolean isMac() {
-        return false;
-    }
-
     @Extension
     public static class DescriptorImpl extends Descriptor {
         @Override
@@ -54,73 +27,4 @@ public String getDisplayName() {
             return "unix";
         }
     }
-
-    public String getRootCommandPrefix() {
-        return rootCommandPrefix;
-    }
-
-    public String getSlaveCommandPrefix() {
-        return slaveCommandPrefix;
-    }
-
-    public String getSlaveCommandSuffix() {
-        return slaveCommandSuffix;
-    }
-
-    public String getSshPort() {
-        return sshPort == null || sshPort.isEmpty() ? "22" : sshPort;
-    }
-
-    public String getBootDelay() {
-        return bootDelay;
-    }
-
-    @Override
-    public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + ((rootCommandPrefix == null) ? 0 : rootCommandPrefix.hashCode());
-        result = prime * result + ((slaveCommandPrefix == null) ? 0 : slaveCommandPrefix.hashCode());
-        result = prime * result + ((slaveCommandSuffix == null) ? 0 : slaveCommandSuffix.hashCode());
-        result = prime * result + ((sshPort == null) ? 0 : sshPort.hashCode());
-        result = prime * result + ((bootDelay == null) ? 0 : bootDelay.hashCode());
-        return result;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj)
-            return true;
-        if (obj == null)
-            return false;
-        if (this.getClass() != obj.getClass())
-            return false;
-        final UnixData other = (UnixData) obj;
-        if (StringUtils.isEmpty(rootCommandPrefix)) {
-            if (!StringUtils.isEmpty(other.rootCommandPrefix))
-                return false;
-        } else if (!rootCommandPrefix.equals(other.rootCommandPrefix))
-            return false;
-        if (StringUtils.isEmpty(slaveCommandPrefix)) {
-            if (!StringUtils.isEmpty(other.slaveCommandPrefix))
-                return false;
-        } else if (!slaveCommandPrefix.equals(other.slaveCommandPrefix))
-            return false;
-        if (StringUtils.isEmpty(slaveCommandSuffix)) {
-            if (!StringUtils.isEmpty(other.slaveCommandSuffix))
-                return false;
-        } else if (!slaveCommandSuffix.equals(other.slaveCommandSuffix))
-            return false;
-        if (StringUtils.isEmpty(sshPort)) {
-            if (!StringUtils.isEmpty(other.sshPort))
-                return false;
-        } else if (!sshPort.equals(other.sshPort))
-            return false;
-        if (bootDelay == null) {
-            if (other.bootDelay != null)
-                return false;
-        } else if (!bootDelay.equals(other.bootDelay))
-            return false;
-        return true;
-    }
 }
diff --git a/src/main/java/hudson/plugins/ec2/WindowsData.java b/src/main/java/hudson/plugins/ec2/WindowsData.java
index 4eba6b25f..8a6657bfd 100644
--- a/src/main/java/hudson/plugins/ec2/WindowsData.java
+++ b/src/main/java/hudson/plugins/ec2/WindowsData.java
@@ -1,13 +1,16 @@
 package hudson.plugins.ec2;
 
-import java.util.concurrent.TimeUnit;
-import java.util.Objects;
-
 import hudson.Extension;
 import hudson.model.Descriptor;
-
+import hudson.plugins.ec2.util.FIPS140Utils;
+import hudson.util.FormValidation;
 import hudson.util.Secret;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import jenkins.model.Jenkins;
 import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.verb.POST;
 
 public class WindowsData extends AMITypeData {
 
@@ -15,28 +18,50 @@ public class WindowsData extends AMITypeData {
     private final boolean useHTTPS;
     private final String bootDelay;
     private final boolean specifyPassword;
-    private final Boolean allowSelfSignedCertificate; //Boolean to allow nulls when the saved template doesn't have the field
+    private final Boolean
+            allowSelfSignedCertificate; // Boolean to allow nulls when the saved template doesn't have the field
 
     @DataBoundConstructor
-    public WindowsData(String password, boolean useHTTPS, String bootDelay, boolean  specifyPassword, boolean allowSelfSignedCertificate) {
+    public WindowsData(
+            String password,
+            boolean useHTTPS,
+            String bootDelay,
+            boolean specifyPassword,
+            boolean allowSelfSignedCertificate)
+            throws Descriptor.FormException {
+        try {
+            FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate);
+        } catch (IllegalArgumentException e) {
+            throw new Descriptor.FormException(e, "allowSelfSignedCertificate");
+        }
         this.password = Secret.fromString(password);
         this.useHTTPS = useHTTPS;
         this.bootDelay = bootDelay;
-        //Backwards compatibility
+        // Backwards compatibility
         if (!specifyPassword && !this.password.getPlainText().isEmpty()) {
             specifyPassword = true;
         }
         this.specifyPassword = specifyPassword;
 
+        try {
+            if (specifyPassword) {
+                FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password);
+                FIPS140Utils.ensurePasswordLength(password);
+            }
+        } catch (IllegalArgumentException e) {
+            throw new Descriptor.FormException(e, "password");
+        }
+
         this.allowSelfSignedCertificate = allowSelfSignedCertificate;
     }
-    
+
     @Deprecated
-    public WindowsData(String password, boolean useHTTPS, String bootDelay, boolean  specifyPassword) {
+    public WindowsData(String password, boolean useHTTPS, String bootDelay, boolean specifyPassword)
+            throws Descriptor.FormException {
         this(password, useHTTPS, bootDelay, specifyPassword, true);
     }
 
-    public WindowsData(String password, boolean useHTTPS, String bootDelay) {
+    public WindowsData(String password, boolean useHTTPS, String bootDelay) throws Descriptor.FormException {
         this(password, useHTTPS, bootDelay, false);
     }
 
@@ -55,6 +80,16 @@ public boolean isMac() {
         return false;
     }
 
+    @Override
+    public boolean isSSHAgent() {
+        return false;
+    }
+
+    @Override
+    public boolean isWinRMAgent() {
+        return true;
+    }
+
     public Secret getPassword() {
         return password;
     }
@@ -63,6 +98,7 @@ public boolean isUseHTTPS() {
         return useHTTPS;
     }
 
+    @Override
     public String getBootDelay() {
         return bootDelay;
     }
@@ -71,6 +107,7 @@ public boolean isSpecifyPassword() {
         return specifyPassword;
     }
 
+    @Override
     public int getBootDelayInMillis() {
         try {
             return (int) TimeUnit.SECONDS.toMillis(Integer.parseInt(bootDelay));
@@ -79,47 +116,101 @@ public int getBootDelayInMillis() {
         }
     }
 
-    public boolean isAllowSelfSignedCertificate(){
+    public boolean isAllowSelfSignedCertificate() {
         return allowSelfSignedCertificate == null || allowSelfSignedCertificate;
     }
-    
+
     @Extension
     public static class DescriptorImpl extends Descriptor {
         @Override
         public String getDisplayName() {
             return "windows";
         }
+
+        @POST
+        @SuppressWarnings("unused")
+        public FormValidation doCheckPassword(@QueryParameter String password) {
+            if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
+                // for security reasons, do not perform any check if the user is not an admin
+                return FormValidation.ok();
+            }
+            try {
+                FIPS140Utils.ensurePasswordLength(password);
+            } catch (IllegalArgumentException ex) {
+                return FormValidation.error(ex, ex.getLocalizedMessage());
+            }
+            return FormValidation.ok();
+        }
+
+        @POST
+        @SuppressWarnings("unused")
+        public FormValidation doCheckUseHTTPS(@QueryParameter boolean useHTTPS, @QueryParameter String password) {
+            if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
+                // for security reasons, do not perform any check if the user is not an admin
+                return FormValidation.ok();
+            }
+            try {
+                FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password);
+            } catch (IllegalArgumentException ex) {
+                return FormValidation.error(ex, ex.getLocalizedMessage());
+            }
+            return FormValidation.ok();
+        }
+
+        @POST
+        @SuppressWarnings("unused")
+        public FormValidation doCheckAllowSelfSignedCertificate(@QueryParameter boolean allowSelfSignedCertificate) {
+            if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
+                // for security reasons, do not perform any check if the user is not an admin
+                return FormValidation.ok();
+            }
+            try {
+                FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate);
+            } catch (IllegalArgumentException ex) {
+                return FormValidation.error(ex, ex.getLocalizedMessage());
+            }
+            return FormValidation.ok();
+        }
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(password,useHTTPS, bootDelay, specifyPassword);
+        return Objects.hash(password, useHTTPS, bootDelay, specifyPassword);
     }
 
     @Override
     public boolean equals(Object obj) {
-        if (this == obj)
+        if (this == obj) {
             return true;
-        if (obj == null)
+        }
+        if (obj == null) {
             return false;
-        if (this.getClass() != obj.getClass())
+        }
+        if (this.getClass() != obj.getClass()) {
             return false;
+        }
         final WindowsData other = (WindowsData) obj;
         if (bootDelay == null) {
-            if (other.bootDelay != null)
+            if (other.bootDelay != null) {
                 return false;
-        } else if (!bootDelay.equals(other.bootDelay))
+            }
+        } else if (!bootDelay.equals(other.bootDelay)) {
             return false;
+        }
         if (password == null) {
-            if (other.password != null)
+            if (other.password != null) {
                 return false;
-        } else if (!password.equals(other.password))
+            }
+        } else if (!password.equals(other.password)) {
             return false;
+        }
         if (allowSelfSignedCertificate == null) {
-            if (other.allowSelfSignedCertificate != null)
+            if (other.allowSelfSignedCertificate != null) {
                 return false;
-        } else if (!allowSelfSignedCertificate.equals(other.allowSelfSignedCertificate))
+            }
+        } else if (!allowSelfSignedCertificate.equals(other.allowSelfSignedCertificate)) {
             return false;
+        }
         return useHTTPS == other.useHTTPS && specifyPassword == other.specifyPassword;
     }
 }
diff --git a/src/main/java/hudson/plugins/ec2/WindowsSSHData.java b/src/main/java/hudson/plugins/ec2/WindowsSSHData.java
new file mode 100644
index 000000000..6fbdcaf8d
--- /dev/null
+++ b/src/main/java/hudson/plugins/ec2/WindowsSSHData.java
@@ -0,0 +1,30 @@
+package hudson.plugins.ec2;
+
+import hudson.Extension;
+import hudson.model.Descriptor;
+import org.kohsuke.stapler.DataBoundConstructor;
+
+public class WindowsSSHData extends SSHData {
+    @DataBoundConstructor
+    public WindowsSSHData(
+            String rootCommandPrefix,
+            String slaveCommandPrefix,
+            String slaveCommandSuffix,
+            String sshPort,
+            String bootDelay) {
+        super(rootCommandPrefix, slaveCommandPrefix, slaveCommandSuffix, sshPort, bootDelay);
+    }
+
+    @Override
+    public boolean isWindows() {
+        return true;
+    }
+
+    @Extension
+    public static class DescriptorImpl extends Descriptor {
+        @Override
+        public String getDisplayName() {
+            return "windows-ssh";
+        }
+    }
+}
diff --git a/src/main/java/hudson/plugins/ec2/ebs/ZPoolExpandNotice.java b/src/main/java/hudson/plugins/ec2/ebs/ZPoolExpandNotice.java
deleted file mode 100644
index e5ab4a08a..000000000
--- a/src/main/java/hudson/plugins/ec2/ebs/ZPoolExpandNotice.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * The MIT License
- *
- * Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-package hudson.plugins.ec2.ebs;
-
-import hudson.model.AdministrativeMonitor;
-import hudson.Extension;
-
-/**
- * {@link AdministrativeMonitor} that tells the user that ZFS pool is filling up and they need to add more storage.
- *
- * @author Kohsuke Kawaguchi
- */
-@Extension
-public class ZPoolExpandNotice extends AdministrativeMonitor {
-    /**
-     * Set by {@link ZPoolMonitor}.
-     */
-    /* package */boolean activated = false;
-
-    public ZPoolExpandNotice() {
-        super("zpool.ebs");
-    }
-
-    @Override
-    public boolean isActivated() {
-        return activated;
-    }
-
-    @Override
-    public String getDisplayName() {
-        return Messages.ZPoolExpandNotice_DisplayName();
-    }
-}
diff --git a/src/main/java/hudson/plugins/ec2/ebs/ZPoolMonitor.java b/src/main/java/hudson/plugins/ec2/ebs/ZPoolMonitor.java
deleted file mode 100644
index 61e6abd68..000000000
--- a/src/main/java/hudson/plugins/ec2/ebs/ZPoolMonitor.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * The MIT License
- *
- * Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-package hudson.plugins.ec2.ebs;
-
-import hudson.model.PeriodicWork;
-import hudson.model.AdministrativeMonitor;
-import hudson.Extension;
-import jenkins.model.Jenkins;
-import org.jvnet.solaris.libzfs.LibZFS;
-import org.jvnet.solaris.libzfs.ZFSFileSystem;
-import org.jvnet.solaris.libzfs.ZFSPool;
-
-import java.net.URL;
-import java.io.IOException;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Once an hour, check if the main zpool is that hosts $HUDSON_HOME has still enough free space.
- *
- * @author Kohsuke Kawaguchi
- */
-@Extension
-public class ZPoolMonitor extends PeriodicWork {
-    private static Boolean isInsideEC2;
-
-    @Override
-    public long getRecurrencePeriod() {
-        return TimeUnit.HOURS.toMillis(1);
-    }
-
-    @Override
-    protected void doRun() {
-        ZPoolExpandNotice zen = AdministrativeMonitor.all().get(ZPoolExpandNotice.class);
-        Jenkins jenkinsInstance = Jenkins.getInstanceOrNull();
-        ZFSFileSystem fs = null;
-        try {
-            if (isInsideEC2() && jenkinsInstance != null)
-                fs = new LibZFS().getFileSystemByMountPoint(jenkinsInstance.getRootDir());
-        } catch (LinkageError e) {
-            // probably not running on OpenSolaris
-        }
-        if (fs == null || zen == null) {
-            cancel();
-            return;
-        }
-        ZFSPool pool = fs.getPool();
-        long a = pool.getAvailableSize();
-        long t = pool.getSize();
-
-        // if the disk is 90% filled up and the available space is less than
-        // 1GB,
-        // notify the user
-        zen.activated = t / a > 10 && a < 1000L * 1000 * 1000;
-    }
-
-    /**
-     * Returns true if this JVM runs inside EC2.
-     */
-    public static synchronized boolean isInsideEC2() {
-        if (isInsideEC2 == null) {
-            try {
-                new URL("http://169.254.169.254/latest").openStream().close();
-                isInsideEC2 = true;
-            } catch (IOException e) {
-                isInsideEC2 = false;
-            }
-        }
-        return isInsideEC2;
-    }
-}
diff --git a/src/main/java/hudson/plugins/ec2/ebs/package-info.java b/src/main/java/hudson/plugins/ec2/ebs/package-info.java
deleted file mode 100644
index ccd1f7a4a..000000000
--- a/src/main/java/hudson/plugins/ec2/ebs/package-info.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * The MIT License
- *
- * Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-/**
- * Code that deals with HUDSON_HOME on EBS, which is used in
- * conjunction with our EC2 launch wizard.
- *
- * 

- * This should eventually move to its own plugin, but for - * now I'm putting this here. - */ -package hudson.plugins.ec2.ebs; \ No newline at end of file diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index f45b54309..45557c1a3 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -23,114 +23,81 @@ */ package hudson.plugins.ec2.ssh; -import com.amazonaws.AmazonClientException; -import com.amazonaws.services.ec2.model.Instance; -import com.amazonaws.services.ec2.model.KeyPair; -import com.trilead.ssh2.*; -import hudson.FilePath; -import hudson.ProxyConfiguration; import hudson.Util; -import hudson.model.Descriptor; import hudson.model.TaskListener; -import hudson.plugins.ec2.*; -import hudson.plugins.ec2.ssh.verifiers.HostKey; -import hudson.plugins.ec2.ssh.verifiers.Messages; -import hudson.remoting.Channel; -import hudson.remoting.Channel.Listener; +import hudson.plugins.ec2.EC2AbstractSlave; +import hudson.plugins.ec2.EC2Computer; +import hudson.plugins.ec2.EC2Readiness; +import hudson.plugins.ec2.SlaveTemplate; +import hudson.plugins.ec2.util.KeyHelper; +import hudson.plugins.ec2.util.KeyPair; +import hudson.plugins.ec2.util.SSHClientHelper; import hudson.slaves.CommandLauncher; import hudson.slaves.ComputerLauncher; -import jenkins.model.Jenkins; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.StringUtils; - -import java.io.*; -import java.net.InetSocketAddress; -import java.net.Proxy; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermission; +import java.time.Duration; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.model.Jenkins; +import org.apache.commons.lang.StringUtils; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.scp.client.CloseableScpClient; +import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.ec2.model.Instance; +import software.amazon.awssdk.services.ec2.model.InstanceType; /** * {@link ComputerLauncher} that connects to a Unix agent on EC2 by using SSH. * * @author Kohsuke Kawaguchi */ -public class EC2MacLauncher extends EC2ComputerLauncher { +public class EC2MacLauncher extends EC2SSHLauncher { private static final Logger LOGGER = Logger.getLogger(EC2MacLauncher.class.getName()); - private static final String BOOTSTRAP_AUTH_SLEEP_MS = "jenkins.ec2.bootstrapAuthSleepMs"; - private static final String BOOTSTRAP_AUTH_TRIES= "jenkins.ec2.bootstrapAuthTries"; private static final String READINESS_SLEEP_MS = "jenkins.ec2.readinessSleepMs"; - private static final String READINESS_TRIES= "jenkins.ec2.readinessTries"; - - private static int bootstrapAuthSleepMs = 30000; - private static int bootstrapAuthTries = 30; + private static final String READINESS_TRIES = "jenkins.ec2.readinessTries"; + private static final String CORRETTO_LATEST_URL = "https://corretto.aws/downloads/latest"; private static int readinessSleepMs = 1000; private static int readinessTries = 120; - static { - String prop = System.getProperty(BOOTSTRAP_AUTH_SLEEP_MS); - if (prop != null) - bootstrapAuthSleepMs = Integer.parseInt(prop); - prop = System.getProperty(BOOTSTRAP_AUTH_TRIES); - if (prop != null) - bootstrapAuthTries = Integer.parseInt(prop); - prop = System.getProperty(READINESS_TRIES); - if (prop != null) + static { + String prop = System.getProperty(READINESS_TRIES); + if (prop != null) { readinessTries = Integer.parseInt(prop); + } prop = System.getProperty(READINESS_SLEEP_MS); - if (prop != null) + if (prop != null) { readinessSleepMs = Integer.parseInt(prop); - } - - protected void log(Level level, EC2Computer computer, TaskListener listener, String message) { - EC2Cloud.log(LOGGER, level, listener, message); - } - - protected void logException(EC2Computer computer, TaskListener listener, String message, Throwable exception) { - EC2Cloud.log(LOGGER, Level.WARNING, listener, message, exception); - } - - protected void logInfo(EC2Computer computer, TaskListener listener, String message) { - log(Level.INFO, computer, listener, message); - } - - protected void logWarning(EC2Computer computer, TaskListener listener, String message) { - log(Level.WARNING, computer, listener, message); - } - - protected String buildUpCommand(EC2Computer computer, String command) { - String remoteAdmin = computer.getRemoteAdmin(); - if (remoteAdmin != null && !remoteAdmin.equals("root")) { - command = computer.getRootCommandPrefix() + " " + command; } - return command; } @Override - protected void launchScript(EC2Computer computer, TaskListener listener) throws IOException, - AmazonClientException, InterruptedException { - final Connection conn; - Connection cleanupConn = null; // java's code path analysis for final - // doesn't work that well. - boolean successful = false; + protected void launchScript(EC2Computer computer, TaskListener listener) + throws IOException, SdkException, InterruptedException { PrintStream logger = listener.getLogger(); EC2AbstractSlave node = computer.getNode(); SlaveTemplate template = computer.getSlaveTemplate(); - if(node == null) { + if (node == null) { throw new IllegalStateException(); } + final long timeout = node.getLaunchTimeoutInMillis(); + if (template == null) { throw new IOException("Could not find corresponding agent template for " + computer.getDisplayName()); } - if (node instanceof EC2Readiness) { - EC2Readiness readinessNode = (EC2Readiness) node; + if (node instanceof EC2Readiness readinessNode) { int tries = readinessTries; while (tries-- > 0) { @@ -138,349 +105,179 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws break; } - logInfo(computer, listener, "Node still not ready. Current status: " + readinessNode.getEc2ReadinessStatus()); + logInfo( + computer, + listener, + "Node still not ready. Current status: " + readinessNode.getEc2ReadinessStatus()); Thread.sleep(readinessSleepMs); } if (!readinessNode.isReady()) { - throw new AmazonClientException("Node still not ready, timed out after " + (readinessTries * readinessSleepMs / 1000) + "s with status " + readinessNode.getEc2ReadinessStatus()); + throw SdkException.builder() + .message("Node still not ready, timed out after " + (readinessTries * readinessSleepMs / 1000) + + "s with status " + readinessNode.getEc2ReadinessStatus()) + .build(); } } logInfo(computer, listener, "Launching instance: " + node.getInstanceId()); - try { + // TODO: parse the version number. maven-enforcer-plugin might help + final String javaPath = node.javaPath; + String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); + + try (SshClient client = SSHClientHelper.getInstance().setupSshClient(computer)) { boolean isBootstrapped = bootstrap(computer, listener, template); - if (isBootstrapped) { - int bootDelay = node.getBootDelay(); - if (bootDelay > 0) { - logInfo(computer, listener, "SSH service responded. Waiting " + bootDelay + "ms for service to stabilize"); - Thread.sleep(bootDelay); - logInfo(computer, listener, "SSH service should have stabilized"); - } - // connect fresh as ROOT - logInfo(computer, listener, "connect fresh as root"); - cleanupConn = connectToSsh(computer, listener, template); - KeyPair key = computer.getCloud().getKeyPair(); - if (key == null || !cleanupConn.authenticateWithPublicKey(computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), "")) { - logWarning(computer, listener, "Authentication failed"); - return; // failed to connect as root. - } - } else { + if (!isBootstrapped) { logWarning(computer, listener, "bootstrapresult failed"); return; // bootstrap closed for us. } - conn = cleanupConn; - - SCPClient scp = conn.createSCPClient(); - String initScript = node.initScript; - String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); - - logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); - conn.exec("mkdir -p " + tmpDir, logger); - - if (initScript != null && initScript.trim().length() > 0 - && conn.exec("test -e ~/.hudson-run-init", logger) != 0) { - logInfo(computer, listener, "Executing init script"); - scp.put(initScript.getBytes("UTF-8"), "init.sh", tmpDir, "0700"); - Session sess = conn.openSession(); - sess.requestDumbPTY(); // so that the remote side bundles stdout - // and stderr - sess.execCommand(buildUpCommand(computer, tmpDir + "/init.sh")); - - sess.getStdin().close(); // nothing to write here - sess.getStderr().close(); // we are not supposed to get anything - // from stderr - IOUtils.copy(sess.getStdout(), logger); - - int exitStatus = waitCompletion(sess); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } - sess.close(); - - logInfo(computer, listener, "Creating ~/.hudson-run-init"); - - // Needs a tty to run sudo. - sess = conn.openSession(); - sess.requestDumbPTY(); // so that the remote side bundles stdout - // and stderr - sess.execCommand(buildUpCommand(computer, "touch ~/.hudson-run-init")); - - sess.getStdin().close(); // nothing to write here - sess.getStderr().close(); // we are not supposed to get anything - // from stderr - IOUtils.copy(sess.getStdout(), logger); - - exitStatus = waitCompletion(sess); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } - sess.close(); + int bootDelay = node.getBootDelay(); + if (bootDelay > 0) { + logInfo( + computer, + listener, + "SSH service responded. Waiting " + bootDelay + "ms for service to stabilize"); + Thread.sleep(bootDelay); + logInfo(computer, listener, "SSH service should have stabilized"); } - // TODO: parse the version number. maven-enforcer-plugin might help - final String javaPath = node.javaPath; - try { - Instance nodeInstance = computer.describeInstance(); - if (nodeInstance.getInstanceType().equals("mac2.metal")) { - LOGGER.info("Running Command for mac2.metal"); - executeRemote(computer, conn, javaPath + " -fullversion", "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-aarch64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-aarch64-macos-jdk.pkg -target /", logger, listener); + // connect fresh as ROOT + logInfo(computer, listener, "connect fresh as root"); + try (ClientSession clientSession = connectToSsh(client, computer, listener, template)) { + KeyPair key = computer.getCloud().getKeyPair(); + + final boolean isAuthenticated; + if (key == null) { + isAuthenticated = false; + } else { + clientSession.addPublicKeyIdentity(KeyHelper.decodeKeyPair(key.getMaterial(), "")); + clientSession.auth().await(timeout); + isAuthenticated = clientSession.isAuthenticated(); } - else{ - executeRemote(computer, conn, javaPath + " -fullversion", "curl -L -O https://corretto.aws/downloads/latest/amazon-corretto-11-x64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-x64-macos-jdk.pkg -target /", logger, listener); + if (!isAuthenticated) { + logWarning(computer, listener, "Authentication failed"); + return; // failed to connect as root. } - } catch (InterruptedException ex) { - LOGGER.warning(ex.getMessage()); - } - - // Always copy so we get the most recent remoting.jar - logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); - scp.put(Jenkins.get().getJnlpJars("remoting.jar").readFully(), "remoting.jar", tmpDir); - - final String jvmopts = node.jvmopts; - final String prefix = computer.getSlaveCommandPrefix(); - final String suffix = computer.getSlaveCommandSuffix(); - final String remoteFS = node.getRemoteFS(); - final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; - String launchString = prefix + " " + javaPath + " " + (jvmopts != null ? jvmopts : "") + " -jar " + tmpDir + "/remoting.jar -workDir " + workDir + suffix; - // launchString = launchString.trim(); - - SlaveTemplate slaveTemplate = computer.getSlaveTemplate(); - if (slaveTemplate != null && slaveTemplate.isConnectBySSHProcess()) { - File identityKeyFile = createIdentityKeyFile(computer); - - try { - // Obviously the controller must have an installed ssh client. - // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag - String sshClientLaunchString = String.format("ssh -o StrictHostKeyChecking=%s -i %s %s@%s -p %d %s", slaveTemplate.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), identityKeyFile.getAbsolutePath(), node.remoteAdmin, getEC2HostAddress(computer, template), node.getSshPort(), launchString); - - logInfo(computer, listener, "Launching remoting agent (via SSH client process): " + sshClientLaunchString); - CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); - commandLauncher.launch(computer, listener); - } finally { - if(!identityKeyFile.delete()) { - LOGGER.log(Level.WARNING, "Failed to delete identity key file"); - } - } - } else { - logInfo(computer, listener, "Launching remoting agent (via Trilead SSH2 Connection): " + launchString); - final Session sess = conn.openSession(); - sess.execCommand(launchString); - computer.setChannel(sess.getStdout(), sess.getStdin(), logger, new Listener() { - @Override - public void onClosed(Channel channel, IOException cause) { - sess.close(); - conn.close(); + try (CloseableScpClient scp = createScpClient(clientSession)) { + String timestamp = + Duration.ofMillis(System.currentTimeMillis()).toSeconds() + " 0"; + ScpTimestampCommandDetails scpTimestamp = + ScpTimestampCommandDetails.parse("T" + timestamp + " " + timestamp); + String initScript = node.initScript; + + logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); + executeRemote(clientSession, "mkdir -p " + tmpDir, logger); + + if (StringUtils.isNotBlank(initScript) + && !executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { + logInfo(computer, listener, "Upload init script"); + scp.upload( + initScript.getBytes(StandardCharsets.UTF_8), + tmpDir + "/init.sh", + List.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE), + scpTimestamp); + + logInfo(computer, listener, "Executing init script"); + String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); + executeRemote(clientSession, initCommand, logger); + + logInfo(computer, listener, "Creating ~/.hudson-run-init"); + String createHudsonRunInitCommand = buildUpCommand(computer, "touch ~/.hudson-run-init"); + executeRemote(clientSession, createHudsonRunInitCommand, logger); } - }); - } - successful = true; - } finally { - if (cleanupConn != null && !successful) - cleanupConn.close(); - } - } + try { + Instance nodeInstance = computer.describeInstance(); + if (nodeInstance.instanceType().equals(InstanceType.MAC2_METAL)) { + LOGGER.info("Running Command for mac2.metal"); + executeRemote( + computer, + clientSession, + javaPath + " -fullversion", + "curl -L -O " + + CORRETTO_LATEST_URL + + "/amazon-corretto-11-aarch64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-aarch64-macos-jdk.pkg -target /", + logger, + listener); + } else { + executeRemote( + computer, + clientSession, + javaPath + " -fullversion", + "curl -L -O " + + CORRETTO_LATEST_URL + + "/amazon-corretto-11-x64-macos-jdk.pkg; sudo installer -pkg amazon-corretto-11-x64-macos-jdk.pkg -target /", + logger, + listener); + } + } catch (InterruptedException ex) { + LOGGER.warning(ex.getMessage()); + } - private boolean executeRemote(EC2Computer computer, Connection conn, String checkCommand, String command, PrintStream logger, TaskListener listener) - throws IOException, InterruptedException { - logInfo(computer, listener,"Verifying: " + checkCommand); - if (conn.exec(checkCommand, logger) != 0) { - logInfo(computer, listener, "Installing: " + command); - if (conn.exec(command, logger) != 0) { - logWarning(computer, listener, "Failed to install: " + command); - return false; + // Always copy so we get the most recent remoting.jar + logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); + scp.upload( + Jenkins.get().getJnlpJars("remoting.jar").readFully(), + tmpDir + "/remoting.jar", + List.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), + scpTimestamp); + } } + client.stop(); } - return true; - } - private File createIdentityKeyFile(EC2Computer computer) throws IOException { - EC2PrivateKey ec2PrivateKey = computer.getCloud().resolvePrivateKey(); - String privateKey = ""; - if (ec2PrivateKey != null){ - privateKey = ec2PrivateKey.getPrivateKey(); - } - - File tempFile = Files.createTempFile("ec2_", ".pem").toFile(); + final String jvmopts = node.jvmopts; + final String prefix = computer.getSlaveCommandPrefix(); + final String suffix = computer.getSlaveCommandSuffix(); + final String remoteFS = node.getRemoteFS(); + final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; + String launchString = prefix + + " " + + javaPath + + " " + + (jvmopts != null ? jvmopts : "") + + " -jar " + + tmpDir + + "/remoting.jar -workDir " + + workDir + + suffix; + // launchString = launchString.trim(); + + SlaveTemplate slaveTemplate = computer.getSlaveTemplate(); + + if (slaveTemplate != null && slaveTemplate.isConnectBySSHProcess()) { + File identityKeyFile = createIdentityKeyFile(computer); - try { - FileOutputStream fileOutputStream = new FileOutputStream(tempFile); - OutputStreamWriter writer = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8); try { - writer.write(privateKey); - writer.flush(); + // Obviously the controller must have an installed ssh client. + // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag + String sshClientLaunchString = String.format( + "ssh -o StrictHostKeyChecking=%s -i %s %s@%s -p %d %s", + slaveTemplate.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), + identityKeyFile.getAbsolutePath(), + node.remoteAdmin, + getEC2HostAddress(computer, template), + node.getSshPort(), + launchString); + + logInfo( + computer, + listener, + "Launching remoting agent (via SSH client process): " + sshClientLaunchString); + CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); + commandLauncher.launch(computer, listener); } finally { - writer.close(); - fileOutputStream.close(); - } - FilePath filePath = new FilePath(tempFile); - filePath.chmod(0400); // octal file mask - readonly by owner - return tempFile; - } catch (Exception e) { - if (!tempFile.delete()) { - LOGGER.log(Level.WARNING, "Failed to delete identity key file"); - } - throw new IOException("Error creating temporary identity key file for connecting to EC2 agent.", e); - } - } - - private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemplate template) throws IOException, - InterruptedException, AmazonClientException { - logInfo(computer, listener, "bootstrap()"); - Connection bootstrapConn = null; - try { - int tries = bootstrapAuthTries; - boolean isAuthenticated = false; - logInfo(computer, listener, "Getting keypair..."); - KeyPair key = computer.getCloud().getKeyPair(); - if (key == null){ - logWarning(computer, listener, "Could not retrieve a valid key pair."); - return false; - } - logInfo(computer, listener, - String.format("Using private key %s (SHA-1 fingerprint %s)", key.getKeyName(), key.getKeyFingerprint())); - while (tries-- > 0) { - logInfo(computer, listener, "Authenticating as " + computer.getRemoteAdmin()); - try { - bootstrapConn = connectToSsh(computer, listener, template); - isAuthenticated = bootstrapConn.authenticateWithPublicKey(computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), ""); - } catch(IOException e) { - logException(computer, listener, "Exception trying to authenticate", e); - bootstrapConn.close(); + if (!identityKeyFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete identity key file"); } - if (isAuthenticated) { - break; - } - logWarning(computer, listener, "Authentication failed. Trying again..."); - Thread.sleep(bootstrapAuthSleepMs); - } - if (!isAuthenticated) { - logWarning(computer, listener, "Authentication failed"); - return false; - } - } finally { - if (bootstrapConn != null) { - bootstrapConn.close(); } + } else { + launchRemotingAgent(computer, listener, launchString, template, timeout, logger); } - return true; - } - - private Connection connectToSsh(EC2Computer computer, TaskListener listener, SlaveTemplate template) throws AmazonClientException, - InterruptedException { - final EC2AbstractSlave node = computer.getNode(); - final long timeout = node == null ? 0L : node.getLaunchTimeoutInMillis(); - final long startTime = System.currentTimeMillis(); - while (true) { - try { - long waitTime = System.currentTimeMillis() - startTime; - if (timeout > 0 && waitTime > timeout) { - throw new AmazonClientException("Timed out after " + (waitTime / 1000) - + " seconds of waiting for ssh to become available. (maximum timeout configured is " - + (timeout / 1000) + ")"); - } - String host = getEC2HostAddress(computer, template); - - if ((node instanceof EC2SpotSlave) && computer.getInstanceId() == null) { - // getInstanceId() on EC2SpotSlave can return null if the spot request doesn't yet know - // the instance id that it is starting. Continue to wait until the instanceId is set. - logInfo(computer, listener, "empty instanceId for Spot Slave."); - throw new IOException("goto sleep"); - } - - if (StringUtils.isBlank(host)) { - logWarning(computer, listener, "Empty host, your host is most likely waiting for an ip address."); - throw new IOException("goto sleep"); - } - - if ("0.0.0.0".equals(host)) { - logWarning(computer, listener, "Invalid host 0.0.0.0, your host is most likely waiting for an ip address."); - throw new IOException("goto sleep"); - } - - int port = computer.getSshPort(); - Integer slaveConnectTimeout = Integer.getInteger("jenkins.ec2.slaveConnectTimeout", 10000); - logInfo(computer, listener, "Connecting to " + host + " on port " + port + ", with timeout " + slaveConnectTimeout - + "."); - Connection conn = new Connection(host, port); - ProxyConfiguration proxyConfig = Jenkins.get().proxy; - Proxy proxy = proxyConfig == null ? Proxy.NO_PROXY : proxyConfig.createProxy(host); - if (!proxy.equals(Proxy.NO_PROXY) && proxy.address() instanceof InetSocketAddress) { - InetSocketAddress address = (InetSocketAddress) proxy.address(); - HTTPProxyData proxyData = null; - if (null != proxyConfig.getUserName()) { - proxyData = new HTTPProxyData(address.getHostName(), address.getPort(), proxyConfig.getUserName(), proxyConfig.getPassword()); - } else { - proxyData = new HTTPProxyData(address.getHostName(), address.getPort()); - } - conn.setProxyData(proxyData); - logInfo(computer, listener, "Using HTTP Proxy Configuration"); - } - - conn.connect(new ServerHostKeyVerifierImpl(computer, listener), slaveConnectTimeout, slaveConnectTimeout); - logInfo(computer, listener, "Connected via SSH."); - return conn; // successfully connected - } catch (IOException e) { - // keep retrying until SSH comes up - logInfo(computer, listener, "Failed to connect via ssh: " + e.getMessage()); - - // If the computer was set offline because it's not trusted, we avoid persisting in connecting to it. - // The computer is offline for a long period - if (computer.isOffline() && StringUtils.isNotBlank(computer.getOfflineCauseReason()) && computer.getOfflineCauseReason().equals(Messages.OfflineCause_SSHKeyCheckFailed())) { - throw new AmazonClientException("The connection couldn't be established and the computer is now offline", e); - } else { - logInfo(computer, listener, "Waiting for SSH to come up. Sleeping 5."); - Thread.sleep(5000); - } - } - } - } - - /** - * Our host key verifier just pick up the right strategy and call its verify method. - */ - private static class ServerHostKeyVerifierImpl implements ServerHostKeyVerifier { - - private final EC2Computer computer; - private final TaskListener listener; - - public ServerHostKeyVerifierImpl(final EC2Computer computer, final TaskListener listener) { - this.computer = computer; - this.listener = listener; - } - - @Override - public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception { - SlaveTemplate template = computer.getSlaveTemplate(); - return template != null && template.getHostKeyVerificationStrategy().getStrategy().verify(computer, new HostKey(serverHostKeyAlgorithm, serverHostKey), listener); - } - } - - private static String getEC2HostAddress(EC2Computer computer, SlaveTemplate template) throws InterruptedException { - Instance instance = computer.updateInstanceDescription(); - ConnectionStrategy strategy = template.connectionStrategy; - return EC2HostAddressProvider.unix(instance, strategy); - } - - private int waitCompletion(Session session) throws InterruptedException { - // I noticed that the exit status delivery often gets delayed. Wait up - // to 1 sec. - for (int i = 0; i < 10; i++) { - Integer r = session.getExitStatus(); - if (r != null) - return r; - Thread.sleep(100); - } - return -1; - } - - @Override - public Descriptor getDescriptor() { - throw new UnsupportedOperationException(); } } diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2SSHLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2SSHLauncher.java new file mode 100644 index 000000000..74598a74f --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2SSHLauncher.java @@ -0,0 +1,442 @@ +package hudson.plugins.ec2.ssh; + +import hudson.FilePath; +import hudson.ProxyConfiguration; +import hudson.model.Descriptor; +import hudson.model.TaskListener; +import hudson.plugins.ec2.ConnectionStrategy; +import hudson.plugins.ec2.EC2AbstractSlave; +import hudson.plugins.ec2.EC2Cloud; +import hudson.plugins.ec2.EC2Computer; +import hudson.plugins.ec2.EC2ComputerLauncher; +import hudson.plugins.ec2.EC2HostAddressProvider; +import hudson.plugins.ec2.EC2PrivateKey; +import hudson.plugins.ec2.EC2SpotSlave; +import hudson.plugins.ec2.SlaveTemplate; +import hudson.plugins.ec2.ssh.proxy.ProxyCONNECTListener; +import hudson.plugins.ec2.ssh.verifiers.HostKey; +import hudson.plugins.ec2.ssh.verifiers.HostKeyHelper; +import hudson.plugins.ec2.ssh.verifiers.Messages; +import hudson.plugins.ec2.util.KeyHelper; +import hudson.plugins.ec2.util.KeyPair; +import hudson.plugins.ec2.util.SSHClientHelper; +import hudson.remoting.Channel; +import hudson.remoting.Channel.Listener; +import hudson.slaves.ComputerLauncher; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.SocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.PublicKey; +import java.util.Base64; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.model.Jenkins; +import org.apache.commons.lang.StringUtils; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.channel.ChannelExec; +import org.apache.sshd.client.future.ConnectFuture; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.config.keys.OpenSshCertificate; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.ec2.model.Instance; +import software.amazon.awssdk.services.ec2.model.InstanceStateName; + +public abstract class EC2SSHLauncher extends EC2ComputerLauncher { + + private static final Logger LOGGER = Logger.getLogger(EC2SSHLauncher.class.getName()); + + private static final String BOOTSTRAP_AUTH_SLEEP_MS = "jenkins.ec2.bootstrapAuthSleepMs"; + private static final String BOOTSTRAP_AUTH_TRIES = "jenkins.ec2.bootstrapAuthTries"; + + private static int bootstrapAuthSleepMs = 30000; + private static int bootstrapAuthTries = 30; + + static { + String prop = System.getProperty(BOOTSTRAP_AUTH_SLEEP_MS); + if (prop != null) { + bootstrapAuthSleepMs = Integer.parseInt(prop); + } + prop = System.getProperty(BOOTSTRAP_AUTH_TRIES); + if (prop != null) { + bootstrapAuthTries = Integer.parseInt(prop); + } + } + + protected void log(Level level, EC2Computer computer, TaskListener listener, String message) { + EC2Cloud.log(LOGGER, level, listener, message); + } + + protected void logException(EC2Computer computer, TaskListener listener, String message, Throwable exception) { + EC2Cloud.log(LOGGER, Level.WARNING, listener, message, exception); + } + + protected void logInfo(EC2Computer computer, TaskListener listener, String message) { + log(Level.INFO, computer, listener, message); + } + + protected void logWarning(EC2Computer computer, TaskListener listener, String message) { + log(Level.WARNING, computer, listener, message); + } + + protected String buildUpCommand(EC2Computer computer, String command) { + String defaultAdmin = "root"; + SlaveTemplate template = computer.getSlaveTemplate(); + if (template != null && template.isWindowsSlave()) { + defaultAdmin = "Administrator"; + } + String remoteAdmin = computer.getRemoteAdmin(); + if (remoteAdmin != null && !remoteAdmin.equals(defaultAdmin)) { + command = computer.getRootCommandPrefix() + " " + command; + } + return command; + } + + protected void launchRemotingAgent( + EC2Computer computer, + TaskListener listener, + String launchString, + SlaveTemplate template, + long timeout, + PrintStream logger) + throws InterruptedException, IOException { + logInfo(computer, listener, "Launching remoting agent (via SSH2 Connection): " + launchString); + + final SshClient remotingClient = SSHClientHelper.getInstance().setupSshClient(computer); + final ClientSession remotingSession = connectToSsh(remotingClient, computer, listener, template); + KeyPair key = computer.getCloud().getKeyPair(); + if (key != null) { + remotingSession.addPublicKeyIdentity(KeyHelper.decodeKeyPair(key.getMaterial(), "")); + } + remotingSession.auth().await(timeout); + ChannelExec agentExecChannel = remotingSession.createExecChannel( + launchString, StandardCharsets.US_ASCII, null, Collections.emptyMap()); + agentExecChannel.open().verify(timeout); + + InputStream invertedOut = agentExecChannel.getInvertedOut(); + OutputStream invertedIn = agentExecChannel.getInvertedIn(); + + Listener channelListener = new Listener() { + + @Override + public void onClosed(Channel channel, IOException cause) { + try { + agentExecChannel.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error when closing the channel", e); + } + try { + remotingSession.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error when closing the session", e); + } + try { + remotingClient.stop(); + remotingClient.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error when closing the client", e); + } + } + }; + + computer.setChannel(invertedOut, invertedIn, logger, channelListener); + } + + protected boolean executeRemote( + EC2Computer computer, + ClientSession clientSession, + String checkCommand, + String command, + PrintStream logger, + TaskListener listener) { + logInfo(computer, listener, "Verifying: " + checkCommand); + if (!executeRemote(clientSession, checkCommand, logger)) { + logInfo(computer, listener, "Installing: " + command); + if (!executeRemote(clientSession, command, logger)) { + logWarning(computer, listener, "Failed to install: " + command); + return false; + } + } + return true; + } + + protected boolean executeRemote(ClientSession session, String command, OutputStream logger) { + try { + session.executeRemoteCommand(command, logger, logger, null); + return true; + } catch (IOException e) { + LOGGER.log(Level.FINE, "Failed to execute remote command: " + command, e); + return false; + } + } + + protected File createIdentityKeyFile(EC2Computer computer) throws IOException { + EC2PrivateKey ec2PrivateKey = computer.getCloud().resolvePrivateKey(); + String privateKey = ""; + if (ec2PrivateKey != null) { + privateKey = ec2PrivateKey.getPrivateKey(); + } + + File tempFile = Files.createTempFile("ec2_", ".pem").toFile(); + + try { + try (FileOutputStream fileOutputStream = new FileOutputStream(tempFile); + OutputStreamWriter writer = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8)) { + writer.write(privateKey); + writer.flush(); + } + FilePath filePath = new FilePath(tempFile); + filePath.chmod(0400); // octal file mask - readonly by owner + return tempFile; + } catch (Exception e) { + if (!tempFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete identity key file"); + } + throw new IOException("Error creating temporary identity key file for connecting to EC2 agent.", e); + } + } + + protected File createHostKeyFile(EC2Computer computer, String ec2HostAddress, TaskListener listener) + throws IOException { + HostKey ec2HostKey = HostKeyHelper.getInstance().getHostKey(computer); + if (ec2HostKey == null) { + return null; + } + File tempFile = Files.createTempFile("ec2_", "_known_hosts").toFile(); + String knownHost = ""; + knownHost = String.format( + "%s %s %s", + ec2HostAddress, ec2HostKey.getAlgorithm(), Base64.getEncoder().encodeToString(ec2HostKey.getKey())); + + try (FileOutputStream fileOutputStream = new FileOutputStream(tempFile); + OutputStreamWriter writer = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8)) { + writer.write(knownHost); + writer.flush(); + FilePath filePath = new FilePath(tempFile); + filePath.chmod(0400); // octal file mask - readonly by owner + return tempFile; + } catch (Exception e) { + if (!tempFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete known hosts key file"); + } + throw new IOException("Error creating temporary known hosts file for connecting to EC2 agent.", e); + } + } + + protected boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemplate template) + throws IOException, InterruptedException, SdkException { + logInfo(computer, listener, "bootstrap()"); + final EC2AbstractSlave node = computer.getNode(); + final long timeout = node == null ? 0L : node.getLaunchTimeoutInMillis(); + ClientSession bootstrapSession = null; + try (SshClient client = SSHClientHelper.getInstance().setupSshClient(computer)) { + int tries = bootstrapAuthTries; + boolean isAuthenticated = false; + logInfo(computer, listener, "Getting keypair..."); + KeyPair key = computer.getCloud().getKeyPair(); + if (key == null) { + logWarning(computer, listener, "Could not retrieve a valid key pair."); + return false; + } + logInfo( + computer, + listener, + String.format( + "Using private key %s (SHA-1 fingerprint %s)", + key.getKeyPairInfo().keyName(), key.getKeyPairInfo().keyFingerprint())); + while (tries-- > 0) { + logInfo(computer, listener, "Authenticating as " + computer.getRemoteAdmin()); + try { + bootstrapSession = connectToSsh(client, computer, listener, template); + bootstrapSession.addPublicKeyIdentity(KeyHelper.decodeKeyPair(key.getMaterial(), "")); + bootstrapSession.auth().await(timeout); + + isAuthenticated = bootstrapSession.isAuthenticated(); + } catch (IOException e) { + logException(computer, listener, "Exception trying to authenticate", e); + bootstrapSession.close(); + } + if (isAuthenticated) { + break; + } + logWarning(computer, listener, "Authentication failed. Trying again..."); + Thread.sleep(bootstrapAuthSleepMs); + } + if (!isAuthenticated) { + logWarning(computer, listener, "Authentication failed"); + return false; + } + } finally { + if (bootstrapSession != null) { + bootstrapSession.close(); + } + } + return true; + } + + protected ClientSession connectToSsh( + SshClient client, EC2Computer computer, TaskListener listener, SlaveTemplate template) + throws SdkException, InterruptedException { + final EC2AbstractSlave node = computer.getNode(); + final long timeout = node == null ? 0L : node.getLaunchTimeoutInMillis(); + final long startTime = System.currentTimeMillis(); + while (true) { + try { + long waitTime = System.currentTimeMillis() - startTime; + if (timeout > 0 && waitTime > timeout) { + throw SdkException.builder() + .message("Timed out after " + (waitTime / 1000) + + " seconds of waiting for ssh to become available. (maximum timeout configured is " + + (timeout / 1000) + ")") + .build(); + } + String host = getEC2HostAddress(computer, template); + + if ((node instanceof EC2SpotSlave) && computer.getInstanceId() == null) { + // getInstanceId() on EC2SpotSlave can return null if the spot request doesn't yet know + // the instance id that it is starting. Continue to wait until the instanceId is set. + logInfo(computer, listener, "empty instanceId for Spot Slave."); + throw new IOException("goto sleep"); + } + + if (StringUtils.isBlank(host)) { + logWarning(computer, listener, "Empty host, your host is most likely waiting for an ip address."); + throw new IOException("goto sleep"); + } + + if ("0.0.0.0".equals(host)) { + logWarning( + computer, + listener, + "Invalid host 0.0.0.0, your host is most likely waiting for an ip address."); + throw new IOException("goto sleep"); + } + + int port = computer.getSshPort(); + Integer slaveConnectTimeout = Integer.getInteger("jenkins.ec2.slaveConnectTimeout", 10000); + logInfo( + computer, + listener, + "Connecting to " + host + " on port " + port + ", with timeout " + slaveConnectTimeout + "."); + + // Configure Host key verification + client.setServerKeyVerifier(new ServerKeyVerifierImpl(computer, listener)); + client.start(); + + ConnectFuture connectFuture; + + ProxyConfiguration proxyConfig = Jenkins.get().proxy; + Proxy proxy = proxyConfig == null ? Proxy.NO_PROXY : proxyConfig.createProxy(host); + if (!proxy.equals(Proxy.NO_PROXY) && proxy.address() instanceof InetSocketAddress address) { + String username = proxyConfig.getUserName(); + String password = proxyConfig.getSecretPassword().getPlainText(); + + client.setClientProxyConnector(new ProxyCONNECTListener(host, port, username, password)); + + connectFuture = client.connect(computer.getRemoteAdmin(), address); + + logInfo(computer, listener, "Using HTTP Proxy Configuration"); + } else { + connectFuture = client.connect(computer.getRemoteAdmin(), host, port); + } + + ClientSession clientSession = connectFuture + .verify(slaveConnectTimeout, TimeUnit.SECONDS) // successfully connected + .getClientSession(); + + logInfo(computer, listener, "Connected via SSH."); + return clientSession; + } catch (IOException e) { + // keep retrying until SSH comes up + logInfo(computer, listener, "Failed to connect via ssh: " + e.getMessage()); + + // If the computer was set offline because it's not trusted, we avoid persisting in connecting to it. + // The computer is offline for a long period + if (computer.isOffline() + && StringUtils.isNotBlank(computer.getOfflineCauseReason()) + && computer.getOfflineCauseReason().equals(Messages.OfflineCause_SSHKeyCheckFailed())) { + throw SdkException.create( + "The connection couldn't be established and the computer is now offline", e); + } else { + logInfo(computer, listener, "Waiting for SSH to come up. Sleeping 5."); + Thread.sleep(5000); + } + } + } + } + + @Restricted(NoExternalUse.class) + public static class ServerKeyVerifierImpl implements ServerKeyVerifier { + private final EC2Computer computer; + private final TaskListener listener; + + public ServerKeyVerifierImpl(final EC2Computer computer, final TaskListener listener) { + this.computer = computer; + this.listener = listener; + } + + @Override + public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) { + PublicKey usableKey = serverKey; + // Unwrap OpenSSH certificate key into actual public key + if (serverKey instanceof OpenSshCertificate cert) { + // Extract actual signed public key + usableKey = cert.getCertPubKey(); + } + SlaveTemplate template = computer.getSlaveTemplate(); + try { + return template != null + && template.getHostKeyVerificationStrategy() + .getStrategy() + .verify(computer, usableKey, listener); + } catch (Exception exception) { + // false will trigger a SSHException which is a subclass of IOException. + // Therefore, it is not needed to throw a RuntimeException. + EC2Cloud.log(LOGGER, Level.WARNING, listener, "Unable to check the server key", exception); + return false; + } + } + } + + protected static String getEC2HostAddress(EC2Computer computer, SlaveTemplate template) + throws SdkException, InterruptedException { + Instance instance = computer.updateInstanceDescription(); + if (instance.state().name() == InstanceStateName.TERMINATED) { + throw SdkException.builder() + .message("Instance " + instance.instanceId() + " is already terminated") + .build(); + } + ConnectionStrategy strategy = template.connectionStrategy; + return template.isMacAgent() + ? EC2HostAddressProvider.mac(instance, strategy) + : (template.isWindowsSlave() + ? EC2HostAddressProvider.windows(instance, strategy) + : EC2HostAddressProvider.unix(instance, strategy)); + } + + protected static String getEC2HostKeyAlgorithmFlag(EC2Computer computer) throws IOException { + HostKey ec2HostKey = HostKeyHelper.getInstance().getHostKey(computer); + if (ec2HostKey != null) { + return String.format(" -o \"HostKeyAlgorithms=%s\"", ec2HostKey.getAlgorithm()); + } + return ""; + } + + @Override + public Descriptor getDescriptor() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index 663a356d6..de8bb56c4 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -23,127 +23,78 @@ */ package hudson.plugins.ec2.ssh; -import hudson.FilePath; import hudson.Util; -import hudson.ProxyConfiguration; -import hudson.model.Descriptor; import hudson.model.TaskListener; -import hudson.plugins.ec2.*; -import hudson.plugins.ec2.ssh.verifiers.HostKey; -import hudson.plugins.ec2.ssh.verifiers.HostKeyHelper; -import hudson.plugins.ec2.ssh.verifiers.Messages; -import hudson.remoting.Channel; -import hudson.remoting.Channel.Listener; +import hudson.plugins.ec2.EC2AbstractSlave; +import hudson.plugins.ec2.EC2Computer; +import hudson.plugins.ec2.EC2Readiness; +import hudson.plugins.ec2.SlaveTemplate; +import hudson.plugins.ec2.util.KeyHelper; +import hudson.plugins.ec2.util.KeyPair; +import hudson.plugins.ec2.util.SSHClientHelper; import hudson.slaves.CommandLauncher; import hudson.slaves.ComputerLauncher; - import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.OutputStreamWriter; import java.io.PrintStream; -import java.net.InetSocketAddress; -import java.net.Proxy; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.Base64; +import java.nio.file.attribute.PosixFilePermission; +import java.time.Duration; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; - import jenkins.model.Jenkins; - -import org.apache.commons.io.IOUtils; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.services.ec2.model.Instance; -import com.amazonaws.services.ec2.model.KeyPair; -import com.trilead.ssh2.Connection; -import com.trilead.ssh2.HTTPProxyData; -import com.trilead.ssh2.SCPClient; -import com.trilead.ssh2.ServerHostKeyVerifier; -import com.trilead.ssh2.Session; import org.apache.commons.lang.StringUtils; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.scp.client.CloseableScpClient; +import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; +import software.amazon.awssdk.core.exception.SdkException; /** * {@link ComputerLauncher} that connects to a Unix agent on EC2 by using SSH. * * @author Kohsuke Kawaguchi */ -public class EC2UnixLauncher extends EC2ComputerLauncher { +public class EC2UnixLauncher extends EC2SSHLauncher { private static final Logger LOGGER = Logger.getLogger(EC2UnixLauncher.class.getName()); - private static final String BOOTSTRAP_AUTH_SLEEP_MS = "jenkins.ec2.bootstrapAuthSleepMs"; - private static final String BOOTSTRAP_AUTH_TRIES= "jenkins.ec2.bootstrapAuthTries"; private static final String READINESS_SLEEP_MS = "jenkins.ec2.readinessSleepMs"; - private static final String READINESS_TRIES= "jenkins.ec2.readinessTries"; - - private static int bootstrapAuthSleepMs = 30000; - private static int bootstrapAuthTries = 30; + private static final String READINESS_TRIES = "jenkins.ec2.readinessTries"; private static int readinessSleepMs = 1000; private static int readinessTries = 120; - static { - String prop = System.getProperty(BOOTSTRAP_AUTH_SLEEP_MS); - if (prop != null) - bootstrapAuthSleepMs = Integer.parseInt(prop); - prop = System.getProperty(BOOTSTRAP_AUTH_TRIES); - if (prop != null) - bootstrapAuthTries = Integer.parseInt(prop); - prop = System.getProperty(READINESS_TRIES); - if (prop != null) + static { + String prop = System.getProperty(READINESS_TRIES); + if (prop != null) { readinessTries = Integer.parseInt(prop); + } prop = System.getProperty(READINESS_SLEEP_MS); - if (prop != null) + if (prop != null) { readinessSleepMs = Integer.parseInt(prop); - } - - protected void log(Level level, EC2Computer computer, TaskListener listener, String message) { - EC2Cloud.log(LOGGER, level, listener, message); - } - - protected void logException(EC2Computer computer, TaskListener listener, String message, Throwable exception) { - EC2Cloud.log(LOGGER, Level.WARNING, listener, message, exception); - } - - protected void logInfo(EC2Computer computer, TaskListener listener, String message) { - log(Level.INFO, computer, listener, message); - } - - protected void logWarning(EC2Computer computer, TaskListener listener, String message) { - log(Level.WARNING, computer, listener, message); - } - - protected String buildUpCommand(EC2Computer computer, String command) { - String remoteAdmin = computer.getRemoteAdmin(); - if (remoteAdmin != null && !remoteAdmin.equals("root")) { - command = computer.getRootCommandPrefix() + " " + command; } - return command; } @Override - protected void launchScript(EC2Computer computer, TaskListener listener) throws IOException, - AmazonClientException, InterruptedException { - final Connection conn; - Connection cleanupConn = null; // java's code path analysis for final - // doesn't work that well. - boolean successful = false; + protected void launchScript(EC2Computer computer, TaskListener listener) + throws IOException, SdkException, InterruptedException { PrintStream logger = listener.getLogger(); EC2AbstractSlave node = computer.getNode(); SlaveTemplate template = computer.getSlaveTemplate(); - if(node == null) { + if (node == null) { throw new IllegalStateException(); } + final long timeout = node.getLaunchTimeoutInMillis(); + if (template == null) { throw new IOException("Could not find corresponding agent template for " + computer.getDisplayName()); } - if (node instanceof EC2Readiness) { - EC2Readiness readinessNode = (EC2Readiness) node; + if (node instanceof EC2Readiness readinessNode) { int tries = readinessTries; while (tries-- > 0) { @@ -151,379 +102,192 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws break; } - logInfo(computer, listener, "Node still not ready. Current status: " + readinessNode.getEc2ReadinessStatus()); + logInfo( + computer, + listener, + "Node still not ready. Current status: " + readinessNode.getEc2ReadinessStatus()); Thread.sleep(readinessSleepMs); } if (!readinessNode.isReady()) { - throw new AmazonClientException("Node still not ready, timed out after " + (readinessTries * readinessSleepMs / 1000) + "s with status " + readinessNode.getEc2ReadinessStatus()); + throw SdkException.builder() + .message("Node still not ready, timed out after " + (readinessTries * readinessSleepMs / 1000) + + "s with status " + readinessNode.getEc2ReadinessStatus()) + .build(); } } logInfo(computer, listener, "Launching instance: " + node.getInstanceId()); - try { - boolean isBootstrapped = bootstrap(computer, listener, template); - if (isBootstrapped) { - int bootDelay = node.getBootDelay(); - if (bootDelay > 0) { - logInfo(computer, listener, "SSH service responded. Waiting " + bootDelay + "ms for service to stabilize"); - Thread.sleep(bootDelay); - logInfo(computer, listener, "SSH service should have stabilized"); - } + // TODO: parse the version number. maven-enforcer-plugin might help + final String javaPath = node.javaPath; + String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); - // connect fresh as ROOT - logInfo(computer, listener, "connect fresh as root"); - cleanupConn = connectToSsh(computer, listener, template); - KeyPair key = computer.getCloud().getKeyPair(); - if (key == null || !cleanupConn.authenticateWithPublicKey(computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), "")) { - logWarning(computer, listener, "Authentication failed"); - return; // failed to connect as root. - } - } else { + try (SshClient client = SSHClientHelper.getInstance().setupSshClient(computer)) { + boolean isBootstrapped = bootstrap(computer, listener, template); + if (!isBootstrapped) { logWarning(computer, listener, "bootstrapresult failed"); return; // bootstrap closed for us. } - conn = cleanupConn; - - SCPClient scp = conn.createSCPClient(); - String initScript = node.initScript; - String tmpDir = (Util.fixEmptyAndTrim(node.tmpDir) != null ? node.tmpDir : "/tmp"); - - logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); - conn.exec("mkdir -p " + tmpDir, logger); - - if (initScript != null && initScript.trim().length() > 0 - && conn.exec("test -e ~/.hudson-run-init", logger) != 0) { - logInfo(computer, listener, "Executing init script"); - scp.put(initScript.getBytes("UTF-8"), "init.sh", tmpDir, "0700"); - Session sess = conn.openSession(); - sess.requestDumbPTY(); // so that the remote side bundles stdout - // and stderr - sess.execCommand(buildUpCommand(computer, tmpDir + "/init.sh")); - - sess.getStdin().close(); // nothing to write here - sess.getStderr().close(); // we are not supposed to get anything - // from stderr - IOUtils.copy(sess.getStdout(), logger); - - int exitStatus = waitCompletion(sess); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } - sess.close(); - - logInfo(computer, listener, "Creating ~/.hudson-run-init"); - - // Needs a tty to run sudo. - sess = conn.openSession(); - sess.requestDumbPTY(); // so that the remote side bundles stdout - // and stderr - sess.execCommand(buildUpCommand(computer, "touch ~/.hudson-run-init")); - - sess.getStdin().close(); // nothing to write here - sess.getStderr().close(); // we are not supposed to get anything - // from stderr - IOUtils.copy(sess.getStdout(), logger); - - exitStatus = waitCompletion(sess); - if (exitStatus != 0) { - logWarning(computer, listener, "init script failed: exit code=" + exitStatus); - return; - } - sess.close(); + int bootDelay = node.getBootDelay(); + if (bootDelay > 0) { + logInfo( + computer, + listener, + "SSH service responded. Waiting " + bootDelay + "ms for service to stabilize"); + Thread.sleep(bootDelay); + logInfo(computer, listener, "SSH service should have stabilized"); } - // TODO: parse the version number. maven-enforcer-plugin might help - final String javaPath = node.javaPath; - executeRemote(computer, conn, javaPath + " -fullversion", "sudo amazon-linux-extras install java-openjdk11 -y; sudo yum install -y fontconfig java-11-openjdk", logger, listener); - executeRemote(computer, conn, "which scp", "sudo yum install -y openssh-clients", logger, listener); - - // Always copy so we get the most recent remoting.jar - logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); - scp.put(Jenkins.get().getJnlpJars("remoting.jar").readFully(), "remoting.jar", tmpDir); - - final String jvmopts = node.jvmopts; - final String prefix = computer.getSlaveCommandPrefix(); - final String suffix = computer.getSlaveCommandSuffix(); - final String remoteFS = node.getRemoteFS(); - final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; - String launchString = prefix + " " + javaPath + " " + (jvmopts != null ? jvmopts : "") + " -jar " + tmpDir + "/remoting.jar -workDir " + workDir + suffix; - // launchString = launchString.trim(); + // connect fresh as ROOT + logInfo(computer, listener, "connect fresh as root"); + try (ClientSession clientSession = connectToSsh(client, computer, listener, template)) { + KeyPair key = computer.getCloud().getKeyPair(); - if (template.isConnectBySSHProcess()) { - File identityKeyFile = createIdentityKeyFile(computer); - String ec2HostAddress = getEC2HostAddress(computer, template); - File hostKeyFile = createHostKeyFile(computer, ec2HostAddress, listener); - String userKnownHostsFileFlag = ""; - if (hostKeyFile != null) { - userKnownHostsFileFlag = String.format(" -o \"UserKnownHostsFile=%s\"", hostKeyFile.getAbsolutePath()); + final boolean isAuthenticated; + if (key == null) { + isAuthenticated = false; + } else { + clientSession.addPublicKeyIdentity(KeyHelper.decodeKeyPair(key.getMaterial(), "")); + clientSession.auth().await(timeout); + isAuthenticated = clientSession.isAuthenticated(); + } + if (!isAuthenticated) { + logWarning(computer, listener, "Authentication failed"); + return; // failed to connect as root. } - try { - // Obviously the controller must have an installed ssh client. - // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag - String sshClientLaunchString = String.format("ssh -o StrictHostKeyChecking=%s%s%s -i %s %s@%s -p %d %s", template.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), userKnownHostsFileFlag, getEC2HostKeyAlgorithmFlag(computer), identityKeyFile.getAbsolutePath(), node.remoteAdmin, ec2HostAddress, node.getSshPort(), launchString); - - logInfo(computer, listener, "Launching remoting agent (via SSH client process): " + sshClientLaunchString); - CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); - commandLauncher.launch(computer, listener); - } finally { - if(!identityKeyFile.delete()) { - LOGGER.log(Level.WARNING, "Failed to delete identity key file"); - } - if(hostKeyFile != null && !hostKeyFile.delete()) { - LOGGER.log(Level.WARNING, "Failed to delete host key file"); + try (CloseableScpClient scp = createScpClient(clientSession)) { + String timestamp = + Duration.ofMillis(System.currentTimeMillis()).toSeconds() + " 0"; + ScpTimestampCommandDetails scpTimestamp = + ScpTimestampCommandDetails.parse("T" + timestamp + " " + timestamp); + String initScript = node.initScript; + + logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); + executeRemote(clientSession, "mkdir -p " + tmpDir, logger); + + if (StringUtils.isNotBlank(initScript) + && !executeRemote(clientSession, "test -e ~/.hudson-run-init", logger)) { + logInfo(computer, listener, "Upload init script"); + scp.upload( + initScript.getBytes(StandardCharsets.UTF_8), + tmpDir + "/init.sh", + List.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE), + scpTimestamp); + + logInfo(computer, listener, "Executing init script"); + String initCommand = buildUpCommand(computer, tmpDir + "/init.sh"); + // Set the flag only when init script executed successfully. + if (executeRemote(clientSession, initCommand, logger)) { + log( + Level.FINE, + computer, + listener, + "Init script executed successfully and creating ~/.hudson-run-init"); + String createHudsonRunInitCommand = buildUpCommand(computer, "touch ~/.hudson-run-init"); + if (!executeRemote(clientSession, createHudsonRunInitCommand, logger)) { + logInfo(computer, listener, "Unable to create ~/.hudson-run-init"); + } + } else { + log( + Level.WARNING, + computer, + listener, + "Failed to execute init script on " + node.getInstanceId()); + clientSession.close(); + scp.close(); + client.stop(); + throw new IOException("Failed to execute init script on " + node.getInstanceId()); + } } + + executeRemote( + computer, + clientSession, + javaPath + " -fullversion", + "sudo amazon-linux-extras install java-openjdk11 -y; sudo yum install -y fontconfig java-11-openjdk", + logger, + listener); + executeRemote( + computer, + clientSession, + "which scp", + "sudo yum install -y openssh-clients", + logger, + listener); + + // Always copy so we get the most recent remoting.jar + logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); + scp.upload( + Jenkins.get().getJnlpJars("remoting.jar").readFully(), + tmpDir + "/remoting.jar", + List.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), + scpTimestamp); } - } else { - logInfo(computer, listener, "Launching remoting agent (via Trilead SSH2 Connection): " + launchString); - final Session sess = conn.openSession(); - sess.execCommand(launchString); - computer.setChannel(sess.getStdout(), sess.getStdin(), logger, new Listener() { - @Override - public void onClosed(Channel channel, IOException cause) { - sess.close(); - conn.close(); - } - }); } - - successful = true; - } finally { - if (cleanupConn != null && (!successful || template.isConnectBySSHProcess())) - cleanupConn.close(); + client.stop(); } - } - private boolean executeRemote(EC2Computer computer, Connection conn, String checkCommand, String command, PrintStream logger, TaskListener listener) - throws IOException, InterruptedException { - logInfo(computer, listener,"Verifying: " + checkCommand); - if (conn.exec(checkCommand, logger) != 0) { - logInfo(computer, listener, "Installing: " + command); - if (conn.exec(command, logger) != 0) { - logWarning(computer, listener, "Failed to install: " + command); - return false; + final String jvmopts = node.jvmopts; + final String prefix = computer.getSlaveCommandPrefix(); + final String suffix = computer.getSlaveCommandSuffix(); + final String remoteFS = node.getRemoteFS(); + final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; + String launchString = prefix + + " " + + javaPath + + " " + + (jvmopts != null ? jvmopts : "") + + " -jar " + + tmpDir + + "/remoting.jar -workDir " + + workDir + + suffix; + // launchString = launchString.trim(); + + if (template.isConnectBySSHProcess()) { + File identityKeyFile = createIdentityKeyFile(computer); + String ec2HostAddress = getEC2HostAddress(computer, template); + File hostKeyFile = createHostKeyFile(computer, ec2HostAddress, listener); + String userKnownHostsFileFlag = ""; + if (hostKeyFile != null) { + userKnownHostsFileFlag = String.format(" -o \"UserKnownHostsFile=%s\"", hostKeyFile.getAbsolutePath()); } - } - return true; - } - - private File createIdentityKeyFile(EC2Computer computer) throws IOException { - EC2PrivateKey ec2PrivateKey = computer.getCloud().resolvePrivateKey(); - String privateKey = ""; - if (ec2PrivateKey != null){ - privateKey = ec2PrivateKey.getPrivateKey(); - } - - File tempFile = Files.createTempFile("ec2_", ".pem").toFile(); - try { - FileOutputStream fileOutputStream = new FileOutputStream(tempFile); - OutputStreamWriter writer = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8); try { - writer.write(privateKey); - writer.flush(); + // Obviously the controller must have an installed ssh client. + // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag + String sshClientLaunchString = String.format( + "ssh -o StrictHostKeyChecking=%s%s%s -i %s %s@%s -p %d %s", + template.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), + userKnownHostsFileFlag, + getEC2HostKeyAlgorithmFlag(computer), + identityKeyFile.getAbsolutePath(), + node.remoteAdmin, + ec2HostAddress, + node.getSshPort(), + launchString); + + logInfo( + computer, + listener, + "Launching remoting agent (via SSH client process): " + sshClientLaunchString); + CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); + commandLauncher.launch(computer, listener); } finally { - writer.close(); - fileOutputStream.close(); - } - FilePath filePath = new FilePath(tempFile); - filePath.chmod(0400); // octal file mask - readonly by owner - return tempFile; - } catch (Exception e) { - if (!tempFile.delete()) { - LOGGER.log(Level.WARNING, "Failed to delete identity key file"); - } - throw new IOException("Error creating temporary identity key file for connecting to EC2 agent.", e); - } - } - - private File createHostKeyFile(EC2Computer computer, String ec2HostAddress, TaskListener listener) throws IOException { - HostKey ec2HostKey = HostKeyHelper.getInstance().getHostKey(computer); - if (ec2HostKey == null){ - return null; - } - File tempFile = Files.createTempFile("ec2_", "_known_hosts").toFile(); - String knownHost = ""; - knownHost = String.format("%s %s %s", ec2HostAddress, ec2HostKey.getAlgorithm(), Base64.getEncoder().encodeToString(ec2HostKey.getKey())); - - try (FileOutputStream fileOutputStream = new FileOutputStream(tempFile); - OutputStreamWriter writer = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8)) { - writer.write(knownHost); - writer.flush(); - FilePath filePath = new FilePath(tempFile); - filePath.chmod(0400); // octal file mask - readonly by owner - return tempFile; - } catch (Exception e) { - if (!tempFile.delete()) { - LOGGER.log(Level.WARNING, "Failed to delete known hosts key file"); - } - throw new IOException("Error creating temporary known hosts file for connecting to EC2 agent.", e); - } - } - - private boolean bootstrap(EC2Computer computer, TaskListener listener, SlaveTemplate template) throws IOException, - InterruptedException, AmazonClientException { - logInfo(computer, listener, "bootstrap()"); - Connection bootstrapConn = null; - try { - int tries = bootstrapAuthTries; - boolean isAuthenticated = false; - logInfo(computer, listener, "Getting keypair..."); - KeyPair key = computer.getCloud().getKeyPair(); - if (key == null){ - logWarning(computer, listener, "Could not retrieve a valid key pair."); - return false; - } - logInfo(computer, listener, - String.format("Using private key %s (SHA-1 fingerprint %s)", key.getKeyName(), key.getKeyFingerprint())); - while (tries-- > 0) { - logInfo(computer, listener, "Authenticating as " + computer.getRemoteAdmin()); - try { - bootstrapConn = connectToSsh(computer, listener, template); - isAuthenticated = bootstrapConn.authenticateWithPublicKey(computer.getRemoteAdmin(), key.getKeyMaterial().toCharArray(), ""); - } catch(IOException e) { - logException(computer, listener, "Exception trying to authenticate", e); - bootstrapConn.close(); + if (!identityKeyFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete identity key file"); } - if (isAuthenticated) { - break; + if (hostKeyFile != null && !hostKeyFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete host key file"); } - logWarning(computer, listener, "Authentication failed. Trying again..."); - Thread.sleep(bootstrapAuthSleepMs); - } - if (!isAuthenticated) { - logWarning(computer, listener, "Authentication failed"); - return false; - } - } finally { - if (bootstrapConn != null) { - bootstrapConn.close(); } + } else { + launchRemotingAgent(computer, listener, launchString, template, timeout, logger); } - return true; - } - - private Connection connectToSsh(EC2Computer computer, TaskListener listener, SlaveTemplate template) throws AmazonClientException, - InterruptedException { - final EC2AbstractSlave node = computer.getNode(); - final long timeout = node == null ? 0L : node.getLaunchTimeoutInMillis(); - final long startTime = System.currentTimeMillis(); - while (true) { - try { - long waitTime = System.currentTimeMillis() - startTime; - if (timeout > 0 && waitTime > timeout) { - throw new AmazonClientException("Timed out after " + (waitTime / 1000) - + " seconds of waiting for ssh to become available. (maximum timeout configured is " - + (timeout / 1000) + ")"); - } - String host = getEC2HostAddress(computer, template); - - if ((node instanceof EC2SpotSlave) && computer.getInstanceId() == null) { - // getInstanceId() on EC2SpotSlave can return null if the spot request doesn't yet know - // the instance id that it is starting. Continue to wait until the instanceId is set. - logInfo(computer, listener, "empty instanceId for Spot Slave."); - throw new IOException("goto sleep"); - } - - if (StringUtils.isBlank(host)) { - logWarning(computer, listener, "Empty host, your host is most likely waiting for an ip address."); - throw new IOException("goto sleep"); - } - - if ("0.0.0.0".equals(host)) { - logWarning(computer, listener, "Invalid host 0.0.0.0, your host is most likely waiting for an ip address."); - throw new IOException("goto sleep"); - } - - int port = computer.getSshPort(); - Integer slaveConnectTimeout = Integer.getInteger("jenkins.ec2.slaveConnectTimeout", 10000); - logInfo(computer, listener, "Connecting to " + host + " on port " + port + ", with timeout " + slaveConnectTimeout - + "."); - Connection conn = new Connection(host, port); - ProxyConfiguration proxyConfig = Jenkins.get().proxy; - Proxy proxy = proxyConfig == null ? Proxy.NO_PROXY : proxyConfig.createProxy(host); - if (!proxy.equals(Proxy.NO_PROXY) && proxy.address() instanceof InetSocketAddress) { - InetSocketAddress address = (InetSocketAddress) proxy.address(); - HTTPProxyData proxyData = null; - if (null != proxyConfig.getUserName()) { - proxyData = new HTTPProxyData(address.getHostName(), address.getPort(), proxyConfig.getUserName(), proxyConfig.getPassword()); - } else { - proxyData = new HTTPProxyData(address.getHostName(), address.getPort()); - } - conn.setProxyData(proxyData); - logInfo(computer, listener, "Using HTTP Proxy Configuration"); - } - - conn.connect(new ServerHostKeyVerifierImpl(computer, listener), slaveConnectTimeout, slaveConnectTimeout); - logInfo(computer, listener, "Connected via SSH."); - return conn; // successfully connected - } catch (IOException e) { - // keep retrying until SSH comes up - logInfo(computer, listener, "Failed to connect via ssh: " + e.getMessage()); - - // If the computer was set offline because it's not trusted, we avoid persisting in connecting to it. - // The computer is offline for a long period - if (computer.isOffline() && StringUtils.isNotBlank(computer.getOfflineCauseReason()) && computer.getOfflineCauseReason().equals(Messages.OfflineCause_SSHKeyCheckFailed())) { - throw new AmazonClientException("The connection couldn't be established and the computer is now offline", e); - } else { - logInfo(computer, listener, "Waiting for SSH to come up. Sleeping 5."); - Thread.sleep(5000); - } - } - } - } - - /** - * Our host key verifier just pick up the right strategy and call its verify method. - */ - private static class ServerHostKeyVerifierImpl implements ServerHostKeyVerifier { - - private final EC2Computer computer; - private final TaskListener listener; - - public ServerHostKeyVerifierImpl(final EC2Computer computer, final TaskListener listener) { - this.computer = computer; - this.listener = listener; - } - - @Override - public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception { - SlaveTemplate template = computer.getSlaveTemplate(); - return template != null && template.getHostKeyVerificationStrategy().getStrategy().verify(computer, new HostKey(serverHostKeyAlgorithm, serverHostKey), listener); - } - } - - private static String getEC2HostAddress(EC2Computer computer, SlaveTemplate template) throws InterruptedException { - Instance instance = computer.updateInstanceDescription(); - ConnectionStrategy strategy = template.connectionStrategy; - return EC2HostAddressProvider.unix(instance, strategy); - } - - private static String getEC2HostKeyAlgorithmFlag(EC2Computer computer) throws IOException { - HostKey ec2HostKey = HostKeyHelper.getInstance().getHostKey(computer); - if (ec2HostKey != null){ - return String.format(" -o \"HostKeyAlgorithms=%s\"", ec2HostKey.getAlgorithm()); - } - return ""; - } - - private int waitCompletion(Session session) throws InterruptedException { - // I noticed that the exit status delivery often gets delayed. Wait up - // to 1 sec. - for (int i = 0; i < 10; i++) { - Integer r = session.getExitStatus(); - if (r != null) - return r; - Thread.sleep(100); - } - return -1; - } - - @Override - public Descriptor getDescriptor() { - throw new UnsupportedOperationException(); } } diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2WindowsSSHLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2WindowsSSHLauncher.java new file mode 100644 index 000000000..3d6cef512 --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2WindowsSSHLauncher.java @@ -0,0 +1,257 @@ +package hudson.plugins.ec2.ssh; + +import hudson.Util; +import hudson.model.TaskListener; +import hudson.os.WindowsUtil; +import hudson.plugins.ec2.EC2AbstractSlave; +import hudson.plugins.ec2.EC2Computer; +import hudson.plugins.ec2.EC2Readiness; +import hudson.plugins.ec2.SlaveTemplate; +import hudson.plugins.ec2.util.KeyHelper; +import hudson.plugins.ec2.util.KeyPair; +import hudson.plugins.ec2.util.SSHClientHelper; +import hudson.slaves.CommandLauncher; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.attribute.PosixFilePermission; +import java.time.Duration; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.model.Jenkins; +import org.apache.commons.lang.StringUtils; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.scp.client.CloseableScpClient; +import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; +import software.amazon.awssdk.core.exception.SdkException; + +public class EC2WindowsSSHLauncher extends EC2SSHLauncher { + + private static final Logger LOGGER = Logger.getLogger(EC2WindowsSSHLauncher.class.getName()); + + private static final String READINESS_SLEEP_MS = "jenkins.ec2.readinessSleepMs"; + private static final String READINESS_TRIES = "jenkins.ec2.readinessTries"; + + private static int readinessSleepMs = 1000; + private static int readinessTries = 120; + + static { + String prop = System.getProperty(READINESS_TRIES); + if (prop != null) { + readinessTries = Integer.parseInt(prop); + } + prop = System.getProperty(READINESS_SLEEP_MS); + if (prop != null) { + readinessSleepMs = Integer.parseInt(prop); + } + } + + @Override + protected void launchScript(EC2Computer computer, TaskListener listener) + throws IOException, SdkException, InterruptedException { + PrintStream logger = listener.getLogger(); + EC2AbstractSlave node = computer.getNode(); + SlaveTemplate template = computer.getSlaveTemplate(); + + if (node == null) { + throw new IllegalStateException(); + } + + final long timeout = node.getLaunchTimeoutInMillis(); + + if (template == null) { + throw new IOException("Could not find corresponding agent template for " + computer.getDisplayName()); + } + + if (node instanceof EC2Readiness readinessNode) { + int tries = readinessTries; + + while (tries-- > 0) { + if (readinessNode.isReady()) { + break; + } + + logInfo( + computer, + listener, + "Node still not ready. Current status: " + readinessNode.getEc2ReadinessStatus()); + Thread.sleep(readinessSleepMs); + } + + if (!readinessNode.isReady()) { + throw SdkException.builder() + .message("Node still not ready, timed out after " + (readinessTries * readinessSleepMs / 1000) + + "s with status " + readinessNode.getEc2ReadinessStatus()) + .build(); + } + } + + logInfo(computer, listener, "Launching instance: " + node.getInstanceId()); + + // TODO: parse the version number. maven-enforcer-plugin might help + final String javaPath = node.javaPath; + String tmpDir = (node.tmpDir != null && !node.tmpDir.isEmpty() + ? WindowsUtil.quoteArgument(Util.ensureEndsWith(node.tmpDir, "\\")) + : "C:\\Windows\\Temp\\"); + + try (SshClient client = SSHClientHelper.getInstance().setupSshClient(computer)) { + boolean isBootstrapped = bootstrap(computer, listener, template); + if (!isBootstrapped) { + logWarning(computer, listener, "bootstrapresult failed"); + return; // bootstrap closed for us. + } + int bootDelay = node.getBootDelay(); + if (bootDelay > 0) { + logInfo( + computer, + listener, + "SSH service responded. Waiting " + bootDelay + "ms for service to stabilize"); + Thread.sleep(bootDelay); + logInfo(computer, listener, "SSH service should have stabilized"); + } + + // connect fresh as Administrator + logInfo(computer, listener, "connect fresh as Administrator"); + try (ClientSession clientSession = connectToSsh(client, computer, listener, template)) { + KeyPair key = computer.getCloud().getKeyPair(); + + final boolean isAuthenticated; + if (key == null) { + isAuthenticated = false; + } else { + clientSession.addPublicKeyIdentity(KeyHelper.decodeKeyPair(key.getMaterial(), "")); + clientSession.auth().await(timeout); + isAuthenticated = clientSession.isAuthenticated(); + } + if (!isAuthenticated) { + logWarning(computer, listener, "Authentication failed"); + return; // failed to connect as Administrator. + } + + try (CloseableScpClient scp = createScpClient(clientSession)) { + String timestamp = + Duration.ofMillis(System.currentTimeMillis()).toSeconds() + " 0"; + ScpTimestampCommandDetails scpTimestamp = + ScpTimestampCommandDetails.parse("T" + timestamp + " " + timestamp); + String initScript = node.initScript; + + logInfo(computer, listener, "Creating tmp directory (" + tmpDir + ") if it does not exist"); + executeRemote(clientSession, "IF NOT EXIST " + tmpDir + " MKDIR " + tmpDir, logger); + + if (StringUtils.isNotBlank(initScript) + && !executeRemote( + clientSession, + "IF NOT EXIST %USERPROFILE%\\.hudson-run-init EXIT /B 999", + logger)) { + logInfo(computer, listener, "Upload init script"); + String scriptPath = tmpDir + "init.bat"; + scp.upload( + initScript.getBytes(StandardCharsets.UTF_8), + scriptPath.replace('\\', '/'), + List.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE), + scpTimestamp); + + logInfo(computer, listener, "Executing init script"); + String initCommand = buildUpCommand(computer, scriptPath); + if (executeRemote(clientSession, initCommand, logger)) { + log( + Level.FINE, + computer, + listener, + "Init script executed successfully and creating %USERPROFILE%\\.hudson-run-init"); + String createHudsonRunInitCommand = + buildUpCommand(computer, "COPY NUL %USERPROFILE%\\.hudson-run-init"); + if (!executeRemote(clientSession, createHudsonRunInitCommand, logger)) { + logInfo(computer, listener, "Unable to create %USERPROFILE%\\.hudson-run-init"); + } + } else { + log( + Level.WARNING, + computer, + listener, + "Failed to execute init script on " + node.getInstanceId()); + clientSession.close(); + scp.close(); + client.stop(); + throw new IOException("Failed to execute init script on " + node.getInstanceId()); + } + } + + // Always copy so we get the most recent remoting.jar + logInfo(computer, listener, "Copying remoting.jar to: " + tmpDir); + String remotingPath = tmpDir + "remoting.jar"; + scp.upload( + Jenkins.get().getJnlpJars("remoting.jar").readFully(), + remotingPath.replace('\\', '/'), + List.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), + scpTimestamp); + } + } + client.stop(); + } + + final String jvmopts = node.jvmopts; + final String prefix = computer.getSlaveCommandPrefix(); + final String suffix = computer.getSlaveCommandSuffix(); + final String remoteFS = node.getRemoteFS(); + final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; + String launchString = prefix + + " " + + javaPath + + " " + + (jvmopts != null ? jvmopts : "") + + " -jar " + + tmpDir + + "remoting.jar -workDir " + + workDir + + suffix; + // launchString = launchString.trim(); + + if (template.isConnectBySSHProcess()) { + File identityKeyFile = createIdentityKeyFile(computer); + String ec2HostAddress = getEC2HostAddress(computer, template); + File hostKeyFile = createHostKeyFile(computer, ec2HostAddress, listener); + String userKnownHostsFileFlag = ""; + if (hostKeyFile != null) { + userKnownHostsFileFlag = String.format(" -o \"UserKnownHostsFile=%s\"", hostKeyFile.getAbsolutePath()); + } + + try { + // Obviously the controller must have an installed ssh client. + // Depending on the strategy selected on the UI, we set the StrictHostKeyChecking flag + String sshClientLaunchString = String.format( + "ssh -o StrictHostKeyChecking=%s%s%s -i %s %s@%s -p %d %s", + template.getHostKeyVerificationStrategy().getSshCommandEquivalentFlag(), + userKnownHostsFileFlag, + getEC2HostKeyAlgorithmFlag(computer), + identityKeyFile.getAbsolutePath(), + node.remoteAdmin, + ec2HostAddress, + node.getSshPort(), + launchString); + + logInfo( + computer, + listener, + "Launching remoting agent (via SSH client process): " + sshClientLaunchString); + CommandLauncher commandLauncher = new CommandLauncher(sshClientLaunchString, null); + commandLauncher.launch(computer, listener); + } finally { + if (!identityKeyFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete identity key file"); + } + if (hostKeyFile != null && !hostKeyFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete host key file"); + } + } + } else { + launchRemotingAgent(computer, listener, launchString, template, timeout, logger); + } + } +} diff --git a/src/main/java/hudson/plugins/ec2/ssh/HostKeyVerifierImpl.java b/src/main/java/hudson/plugins/ec2/ssh/HostKeyVerifierImpl.java index 884a2420b..664e08149 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/HostKeyVerifierImpl.java +++ b/src/main/java/hudson/plugins/ec2/ssh/HostKeyVerifierImpl.java @@ -23,12 +23,10 @@ */ package hudson.plugins.ec2.ssh; -import java.util.logging.Logger; - -import com.trilead.ssh2.ServerHostKeyVerifier; import java.security.MessageDigest; +import java.util.logging.Logger; -public class HostKeyVerifierImpl implements ServerHostKeyVerifier { +public class HostKeyVerifierImpl { private static final Logger LOGGER = Logger.getLogger(HostKeyVerifierImpl.class.getName()); private final String console; @@ -44,8 +42,9 @@ private String getFingerprint(byte[] serverHostKey) throws Exception { StringBuilder buf = new StringBuilder(); for (byte b : fingerprint) { - if (buf.length() > 0) + if (!buf.isEmpty()) { buf.append(':'); + } buf.append(String.format("%02x", b)); } return buf.toString(); @@ -59,10 +58,10 @@ public boolean verifyServerHostKey(String hostname, int port, String serverHostK boolean matches = console.contains(fingerprint); - if (!matches) + if (!matches) { LOGGER.severe("No matching fingerprint found in the console output: " + console); + } return matches; } - } diff --git a/src/main/java/hudson/plugins/ec2/ssh/proxy/ProxyCONNECTListener.java b/src/main/java/hudson/plugins/ec2/ssh/proxy/ProxyCONNECTListener.java new file mode 100644 index 000000000..5dd832a1f --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/ssh/proxy/ProxyCONNECTListener.java @@ -0,0 +1,73 @@ +package hudson.plugins.ec2.ssh.proxy; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Base64; +import org.apache.sshd.client.session.ClientProxyConnector; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.io.IoSession; +import org.apache.sshd.common.util.buffer.ByteArrayBuffer; + +/** + * {@link ClientProxyConnector} that issue an HTTP CONNECT to connect through an HTTP proxy. + */ +public class ProxyCONNECTListener implements ClientProxyConnector { + + private static final long timeout = Duration.ofSeconds(10).toMillis(); + + public final String targetHost; + public final int targetPort; + public final String proxyUser; + public final String proxyPass; + + public ProxyCONNECTListener(String targetHost, int targetPort, String proxyUser, String proxyPass) { + this.targetHost = targetHost; + this.targetPort = targetPort; + this.proxyUser = proxyUser; + this.proxyPass = proxyPass; + } + + @Override + public void sendClientProxyMetadata(ClientSession session) throws Exception { + proxyCONNECT(session.getIoSession()); + } + + public void proxyCONNECT(IoSession ioSession) { + StringBuilder connectRequest = new StringBuilder(); + + // Based on https://www.rfc-editor.org/rfc/rfc7231#section-4.3.6 + connectRequest + .append("CONNECT ") + .append(targetHost) + .append(':') + .append(targetPort) + .append(" HTTP/1.0\r\n"); + // Host should be included https://datatracker.ietf.org/doc/html/rfc2616#section-14.23 + connectRequest + .append("Host: ") + .append(targetHost) + .append(':') + .append(targetPort) + .append("\r\n"); + + if ((proxyUser != null) && (proxyPass != null)) { + String credentials = proxyUser + ":" + proxyPass; + String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.ISO_8859_1)); + connectRequest.append("Proxy-Authorization: Basic "); + connectRequest.append(encoded); + connectRequest.append("\r\n"); + } + + // End of the header + connectRequest.append("\r\n"); + + try { + ioSession + .writeBuffer(new ByteArrayBuffer(connectRequest.toString().getBytes(StandardCharsets.US_ASCII))) + .await(timeout); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/AcceptNewStrategy.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/AcceptNewStrategy.java index 76bf4a015..5251a2dfa 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/AcceptNewStrategy.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/AcceptNewStrategy.java @@ -27,7 +27,6 @@ import hudson.plugins.ec2.EC2Cloud; import hudson.plugins.ec2.EC2Computer; import hudson.slaves.OfflineCause; - import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; @@ -41,19 +40,35 @@ */ public class AcceptNewStrategy extends SshHostKeyVerificationStrategy { private static final Logger LOGGER = Logger.getLogger(AcceptNewStrategy.class.getName()); - + @Override public boolean verify(EC2Computer computer, HostKey hostKey, TaskListener listener) throws IOException { HostKey existingHostKey = HostKeyHelper.getInstance().getHostKey(computer); if (null == existingHostKey) { HostKeyHelper.getInstance().saveHostKey(computer, hostKey); - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("The SSH key %s %s has been automatically trusted for connections to %s", hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format( + "The SSH key %s %s has been automatically trusted for connections to %s", + hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); return true; } else if (existingHostKey.equals(hostKey)) { - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("Connection allowed after the host key has been verified")); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + "Connection allowed after the host key has been verified"); return true; } else { - EC2Cloud.log(LOGGER, Level.WARNING, computer.getListener(), String.format("The SSH key (%s) presented by the instance has changed since first saved (%s). The connection to %s is closed to prevent a possible man-in-the-middle attack", hostKey.getFingerprint(), existingHostKey.getFingerprint(), computer.getName())); + EC2Cloud.log( + LOGGER, + Level.WARNING, + computer.getListener(), + String.format( + "The SSH key (%s) presented by the instance has changed since first saved (%s). The connection to %s is closed to prevent a possible man-in-the-middle attack", + hostKey.getFingerprint(), existingHostKey.getFingerprint(), computer.getName())); // To avoid reconnecting continuously computer.setTemporarilyOffline(true, OfflineCause.create(Messages._OfflineCause_SSHKeyCheckFailed())); return false; diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewHardStrategy.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewHardStrategy.java index 4f637f7e5..cb67393a6 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewHardStrategy.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewHardStrategy.java @@ -27,7 +27,6 @@ import hudson.plugins.ec2.EC2Cloud; import hudson.plugins.ec2.EC2Computer; import hudson.slaves.OfflineCause; - import java.io.IOException; import java.util.concurrent.TimeUnit; import java.util.logging.Level; @@ -43,22 +42,40 @@ */ public class CheckNewHardStrategy extends SshHostKeyVerificationStrategy { private static final Logger LOGGER = Logger.getLogger(CheckNewHardStrategy.class.getName()); - + @Override public boolean verify(EC2Computer computer, HostKey hostKey, TaskListener listener) throws IOException { HostKey existingHostKey = HostKeyHelper.getInstance().getHostKey(computer); if (null == existingHostKey) { HostKey consoleHostKey = getHostKeyFromConsole(LOGGER, computer, hostKey.getAlgorithm()); - + if (hostKey.equals(consoleHostKey)) { HostKeyHelper.getInstance().saveHostKey(computer, hostKey); - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("The SSH key %s %s has been successfully checked against the instance console for connections to %s", hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format( + "The SSH key %s %s has been successfully checked against the instance console for connections to %s", + hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); return true; } else if (consoleHostKey == null) { - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("The instance console is blank. Cannot check the key. The connection to %s is not allowed", computer.getName())); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format( + "The instance console is blank. Cannot check the key. The connection to %s is not allowed", + computer.getName())); return false; // waiting for next retry to have the console filled up } else if (consoleHostKey.getKey().length == 0) { - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("The SSH key (%s %s) presented by the instance has not been found on the instance console. Cannot check the key. The connection to %s is not allowed", hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format( + "The SSH key (%s %s) presented by the instance has not been found on the instance console. Cannot check the key. The connection to %s is not allowed", + hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); // it is the difference with the soft strategy, the key is not accepted boolean stop = false; try { @@ -68,21 +85,41 @@ public boolean verify(EC2Computer computer, HostKey hostKey, TaskListener listen } if (stop) { - computer.setTemporarilyOffline(true, OfflineCause.create(Messages._OfflineCause_SSHKeyCheckFailed())); // avoid next try + computer.setTemporarilyOffline( + true, OfflineCause.create(Messages._OfflineCause_SSHKeyCheckFailed())); // avoid next try } return false; } else { - EC2Cloud.log(LOGGER, Level.WARNING, computer.getListener(), String.format("The SSH key (%s %s) presented by the instance is different from the one printed out on the instance console (%s %s). The connection to %s is closed to prevent a possible man-in-the-middle attack", - hostKey.getAlgorithm(), hostKey.getFingerprint(), consoleHostKey.getAlgorithm(), consoleHostKey.getFingerprint(), computer.getName())); + EC2Cloud.log( + LOGGER, + Level.WARNING, + computer.getListener(), + String.format( + "The SSH key (%s %s) presented by the instance is different from the one printed out on the instance console (%s %s). The connection to %s is closed to prevent a possible man-in-the-middle attack", + hostKey.getAlgorithm(), + hostKey.getFingerprint(), + consoleHostKey.getAlgorithm(), + consoleHostKey.getFingerprint(), + computer.getName())); // To avoid reconnecting continuously computer.setTemporarilyOffline(true, OfflineCause.create(Messages._OfflineCause_SSHKeyCheckFailed())); return false; } } else if (existingHostKey.equals(hostKey)) { - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("Connection allowed after the host key has been verified")); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + "Connection allowed after the host key has been verified"); return true; } else { - EC2Cloud.log(LOGGER, Level.WARNING, computer.getListener(), String.format("The SSH key (%s) presented by the instance has changed since first saved (%s). The connection to %s is closed to prevent a possible man-in-the-middle attack", hostKey.getFingerprint(), existingHostKey.getFingerprint(), computer.getName())); + EC2Cloud.log( + LOGGER, + Level.WARNING, + computer.getListener(), + String.format( + "The SSH key (%s) presented by the instance has changed since first saved (%s). The connection to %s is closed to prevent a possible man-in-the-middle attack", + hostKey.getFingerprint(), existingHostKey.getFingerprint(), computer.getName())); // To avoid reconnecting continuously computer.setTemporarilyOffline(true, OfflineCause.create(Messages._OfflineCause_SSHKeyCheckFailed())); return false; diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewSoftStrategy.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewSoftStrategy.java index c884659b1..698789b20 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewSoftStrategy.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/CheckNewSoftStrategy.java @@ -27,7 +27,6 @@ import hudson.plugins.ec2.EC2Cloud; import hudson.plugins.ec2.EC2Computer; import hudson.slaves.OfflineCause; - import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; @@ -45,38 +44,75 @@ */ public class CheckNewSoftStrategy extends SshHostKeyVerificationStrategy { private static final Logger LOGGER = Logger.getLogger(CheckNewSoftStrategy.class.getName()); - + @Override public boolean verify(EC2Computer computer, HostKey hostKey, TaskListener listener) throws IOException { HostKey existingHostKey = HostKeyHelper.getInstance().getHostKey(computer); if (null == existingHostKey) { HostKey consoleHostKey = getHostKeyFromConsole(LOGGER, computer, hostKey.getAlgorithm()); - + if (hostKey.equals(consoleHostKey)) { HostKeyHelper.getInstance().saveHostKey(computer, hostKey); - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("The SSH key %s %s has been successfully checked against the instance console for connections to %s", hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format( + "The SSH key %s %s has been successfully checked against the instance console for connections to %s", + hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); return true; } else if (consoleHostKey == null) { - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("The instance console is blank. Cannot check the key. The connection to %s is not allowed", computer.getName())); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format( + "The instance console is blank. Cannot check the key. The connection to %s is not allowed", + computer.getName())); return false; // waiting for next retry to have the console filled up } else if (consoleHostKey.getKey().length == 0) { - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("The SSH key (%s %s) presented by the instance has not been found on the instance console. Cannot check the key but the connection to %s is allowed", hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); - // it is the difference with the the hard strategy, the key is accepted + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format( + "The SSH key (%s %s) presented by the instance has not been found on the instance console. Cannot check the key but the connection to %s is allowed", + hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); + // it is the difference with the the hard strategy, the key is accepted HostKeyHelper.getInstance().saveHostKey(computer, hostKey); return true; } else { - EC2Cloud.log(LOGGER, Level.WARNING, computer.getListener(), String.format("The SSH key (%s %s) presented by the instance is different from the one printed out on the instance console (%s %s). The connection to %s is closed to prevent a possible man-in-the-middle attack", - hostKey.getAlgorithm(), hostKey.getFingerprint(), consoleHostKey.getAlgorithm(), consoleHostKey.getFingerprint(), computer.getName())); - // To avoid reconnecting continuously + EC2Cloud.log( + LOGGER, + Level.WARNING, + computer.getListener(), + String.format( + "The SSH key (%s %s) presented by the instance is different from the one printed out on the instance console (%s %s). The connection to %s is closed to prevent a possible man-in-the-middle attack", + hostKey.getAlgorithm(), + hostKey.getFingerprint(), + consoleHostKey.getAlgorithm(), + consoleHostKey.getFingerprint(), + computer.getName())); + // To avoid reconnecting continuously computer.setTemporarilyOffline(true, OfflineCause.create(Messages._OfflineCause_SSHKeyCheckFailed())); return false; } } else if (existingHostKey.equals(hostKey)) { - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("Connection allowed after the host key has been verified")); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + "Connection allowed after the host key has been verified"); return true; } else { - EC2Cloud.log(LOGGER, Level.WARNING, computer.getListener(), String.format("The SSH key (%s) presented by the instance has changed since first saved (%s). The connection to %s is closed to prevent a possible man-in-the-middle attack", hostKey.getFingerprint(), existingHostKey.getFingerprint(), computer.getName())); - // To avoid reconnecting continuously + EC2Cloud.log( + LOGGER, + Level.WARNING, + computer.getListener(), + String.format( + "The SSH key (%s) presented by the instance has changed since first saved (%s). The connection to %s is closed to prevent a possible man-in-the-middle attack", + hostKey.getFingerprint(), existingHostKey.getFingerprint(), computer.getName())); + // To avoid reconnecting continuously computer.setTemporarilyOffline(true, OfflineCause.create(Messages._OfflineCause_SSHKeyCheckFailed())); return false; } diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java index 1f01734fe..f6ce8e1a7 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java @@ -1,36 +1,39 @@ /* - * The MIT License - * - * Original work from ssh-slaves-plugin Copyright (c) 2016, Michael Clarke - * Modified work Copyright (c) 2020-, M Ramon Leon, CloudBees, Inc. - * Modified work: - * - Just the since annotation +* The MIT License +* +* Original work from ssh-slaves-plugin Copyright (c) 2016, Michael Clarke +* Modified work Copyright (c) 2020-, M Ramon Leon, CloudBees, Inc. +* Modified work: +* - Just the since annotation - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ package hudson.plugins.ec2.ssh.verifiers; -import com.trilead.ssh2.KnownHosts; import edu.umd.cs.findbugs.annotations.NonNull; - +import hudson.plugins.ec2.util.FIPS140Utils; +import java.io.Serial; import java.io.Serializable; import java.util.Arrays; +import org.apache.sshd.common.digest.BuiltinDigests; +import org.apache.sshd.common.digest.DigestUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; /** * A representation of the SSH key provided by a remote host to verify itself @@ -40,6 +43,7 @@ */ public final class HostKey implements Serializable { + @Serial private static final long serialVersionUID = -3873284593211178494L; private final String algorithm; @@ -47,6 +51,8 @@ public final class HostKey implements Serializable { public HostKey(@NonNull String algorithm, @NonNull byte[] key) { super(); + FIPS140Utils.ensurePublicKeyInFipsMode(algorithm, key); + this.algorithm = algorithm; this.key = key.clone(); } @@ -70,7 +76,12 @@ public byte[] getKey() { } public String getFingerprint() { - return KnownHosts.createHexFingerprint(getAlgorithm(), getKey()); + try { + byte[] rawFingerprint = DigestUtils.getRawFingerprint(BuiltinDigests.md5.get(), getKey()); + return BufferUtils.toHex(':', rawFingerprint).toLowerCase(); + } catch (Exception e) { + return ""; + } } @Override @@ -84,20 +95,26 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) + if (this == obj) { return true; - if (obj == null) + } + if (obj == null) { return false; - if (getClass() != obj.getClass()) + } + if (getClass() != obj.getClass()) { return false; + } HostKey other = (HostKey) obj; if (algorithm == null) { - if (other.algorithm != null) + if (other.algorithm != null) { return false; - } else if (!algorithm.equals(other.algorithm)) + } + } else if (!algorithm.equals(other.algorithm)) { return false; - if (!Arrays.equals(key, other.key)) + } + if (!Arrays.equals(key, other.key)) { return false; + } return true; } } diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKeyHelper.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKeyHelper.java index f0eed8543..96f99c48f 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKeyHelper.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKeyHelper.java @@ -1,53 +1,59 @@ /* - * The MIT License - * - * Original work from ssh-slaves-plugin Copyright (c) 2016, Michael Clarke - * Modified work Copyright (c) 2020-, M Ramon Leon, CloudBees, Inc. - * Modified work: - * - Just the since annotation +* The MIT License +* +* Original work from ssh-slaves-plugin Copyright (c) 2016, Michael Clarke +* Modified work Copyright (c) 2020-, M Ramon Leon, CloudBees, Inc. +* Modified work: +* - Just the since annotation - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ package hudson.plugins.ec2.ssh.verifiers; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.XmlFile; import hudson.model.Computer; import hudson.model.Node; -import jenkins.model.Jenkins; - +import hudson.plugins.ec2.util.KeyHelper; import java.io.File; import java.io.IOException; +import java.security.PublicKey; import java.util.Map; import java.util.WeakHashMap; +import jenkins.model.Jenkins; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.util.OpenSSHPublicKeyUtil; +import org.bouncycastle.crypto.util.PublicKeyFactory; /** * Helper methods to allow loading and saving of host keys for a computer. Verifiers * don't have a reference to the Node or Computer that they're running for at the point * they're created, so can only load the existing key to run comparisons against at the - * point the verifier is invoked during the connection attempt. + * point the verifier is invoked during the connection attempt. * @author Michael Clarke, M Ramon Leon * @since TODO */ public final class HostKeyHelper { private static final HostKeyHelper INSTANCE = new HostKeyHelper(); - + private final Map cache = new WeakHashMap<>(); private HostKeyHelper() { @@ -58,6 +64,23 @@ public static HostKeyHelper getInstance() { return INSTANCE; } + /** + * Converts a Java {@link PublicKey} to a {@link HostKey} for SSH verification. + * Uses the key's encoded form and BouncyCastle utilities to produce the SSH format. + * + * @param serverKey the public key to convert + * @return a {@link HostKey} representing the SSH-formatted key, or {@code null} if the algorithm is unsupported + * @throws IOException if the key cannot be processed + */ + @CheckForNull + public HostKey getHostKey(@NonNull PublicKey serverKey) throws IOException { + String sshAlgorithm = KeyHelper.getSshAlgorithm(serverKey); + if (sshAlgorithm == null) { + return null; + } + AsymmetricKeyParameter parameters = PublicKeyFactory.createKey(serverKey.getEncoded()); + return new HostKey(sshAlgorithm, OpenSSHPublicKeyUtil.encodePublicKey(parameters)); + } /** * Retrieve the currently trusted host key for the requested computer, or null if @@ -81,7 +104,6 @@ public HostKey getHostKey(Computer host) throws IOException { return key; } - /** * Persists an SSH key to disk for the requested host. This effectively marks * the requested key as trusted for all future connections to the host, until @@ -95,20 +117,20 @@ public void saveHostKey(Computer host, HostKey hostKey) throws IOException { xmlHostKeyFile.write(hostKey); cache.put(host, hostKey); } - + private File getSshHostKeyFile(Node node) throws IOException { return new File(getNodeDirectory(node), "ssh-host-key.xml"); } - + private File getNodeDirectory(Node node) throws IOException { if (null == node) { throw new IOException("Could not load key for the requested node"); } return new File(getNodesDirectory(), node.getNodeName()); } - + private File getNodesDirectory() throws IOException { - // jenkins.model.Nodes#getNodesDirectory() is private, so we have to duplicate it here. + // Apparent clone of jenkins.model.Nodes#getNodesDirectory(), which no longer even exists. File nodesDir = new File(Jenkins.get().getRootDir(), "nodes"); if (!nodesDir.exists() || !nodesDir.isDirectory()) { throw new IOException("Nodes directory does not exist"); diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/NonVerifyingKeyVerificationStrategy.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/NonVerifyingKeyVerificationStrategy.java index 609e33940..130213bb1 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/NonVerifyingKeyVerificationStrategy.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/NonVerifyingKeyVerificationStrategy.java @@ -2,7 +2,7 @@ * The MIT License * * Copyright (c) 2020-, M Ramon Leon, CloudBees, Inc. - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -26,8 +26,9 @@ import hudson.model.TaskListener; import hudson.plugins.ec2.EC2Cloud; import hudson.plugins.ec2.EC2Computer; - +import hudson.plugins.ec2.util.KeyHelper; import java.io.IOException; +import java.security.PublicKey; import java.util.logging.Level; import java.util.logging.Logger; @@ -38,10 +39,28 @@ */ public class NonVerifyingKeyVerificationStrategy extends SshHostKeyVerificationStrategy { private static final Logger LOGGER = Logger.getLogger(NonVerifyingKeyVerificationStrategy.class.getName()); - + + @Override + public boolean verify(EC2Computer computer, PublicKey serverKey, TaskListener listener) throws Exception { + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format( + "No SSH key verification (%s %s) for connections to %s", + KeyHelper.getSshAlgorithm(serverKey), KeyHelper.getFingerprint(serverKey), computer.getName())); + return true; + } + @Override public boolean verify(EC2Computer computer, HostKey hostKey, TaskListener listener) throws IOException { - EC2Cloud.log(LOGGER, Level.INFO, computer.getListener(), String.format("No SSH key verification (%s %s) for connections to %s", hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); + EC2Cloud.log( + LOGGER, + Level.INFO, + computer.getListener(), + String.format( + "No SSH key verification (%s %s) for connections to %s", + hostKey.getAlgorithm(), hostKey.getFingerprint(), computer.getName())); return true; } } diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationAdministrativeMonitor.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationAdministrativeMonitor.java index 57e49f11f..d142de312 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationAdministrativeMonitor.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationAdministrativeMonitor.java @@ -30,23 +30,21 @@ import hudson.plugins.ec2.PluginImpl; import hudson.plugins.ec2.SlaveTemplate; import hudson.slaves.Cloud; -import jenkins.model.Jenkins; -import org.kohsuke.stapler.HttpResponse; -import org.kohsuke.stapler.HttpResponses; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.interceptor.RequirePOST; - import java.io.IOException; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; -import java.util.stream.Collectors; +import jenkins.model.Jenkins; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.HttpResponses; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.interceptor.RequirePOST; @Extension public class SshHostKeyVerificationAdministrativeMonitor extends AdministrativeMonitor { - private final static int MAX_TEMPLATES_FOUND = 5; - + private static final int MAX_TEMPLATES_FOUND = 5; + List veryInsecureTemplates = new ArrayList<>(MAX_TEMPLATES_FOUND); List insecureTemplates = new ArrayList<>(MAX_TEMPLATES_FOUND); @@ -56,11 +54,11 @@ public String getDisplayName() { } public String getVeryInsecureTemplates() { - return veryInsecureTemplates.stream().collect(Collectors.joining(", ")); + return String.join(", ", veryInsecureTemplates); } public String getInsecureTemplates() { - return insecureTemplates.stream().collect(Collectors.joining(", ")); + return String.join(", ", insecureTemplates); } public boolean showVeryInsecureTemplates() { @@ -73,9 +71,11 @@ public boolean showInsecureTemplates() { if (plugin == null) { return true; } - - Instant whenDismissed = Instant.ofEpochMilli(plugin.getDismissInsecureMessages()); // if not dismissed, it is EPOCH - return (whenDismissed.equals(Instant.EPOCH) || Instant.now().isBefore(whenDismissed)) && !insecureTemplates.isEmpty(); + + Instant whenDismissed = + Instant.ofEpochMilli(plugin.getDismissInsecureMessages()); // if not dismissed, it is EPOCH + return (whenDismissed.equals(Instant.EPOCH) || Instant.now().isBefore(whenDismissed)) + && !insecureTemplates.isEmpty(); } /** @@ -85,16 +85,16 @@ public boolean showInsecureTemplates() { @Override public boolean isActivated() { boolean maxTemplatesReached = false; - + ListIterator cloudIterator = Jenkins.get().clouds.listIterator(); - + // Let's clear the previously calculated wrong templates to populate the lists with them again veryInsecureTemplates.clear(); insecureTemplates.clear(); - + while (cloudIterator.hasNext() && !maxTemplatesReached) { Cloud cloud = cloudIterator.next(); - if (cloud instanceof EC2Cloud) { + if (cloud instanceof EC2Cloud) { maxTemplatesReached = gatherInsecureTemplate((EC2Cloud) cloud); } } @@ -109,35 +109,39 @@ public boolean isActivated() { private boolean gatherInsecureTemplate(EC2Cloud cloud) { List templates = cloud.getTemplates(); for (SlaveTemplate template : templates) { - // It's only for unix templates - if (!template.isUnixSlave() || !template.isMacAgent()) { + // It's only for ssh templates + if (!template.isSSHAgent()) { continue; } HostKeyVerificationStrategyEnum strategy = template.getHostKeyVerificationStrategy(); - if (veryInsecureTemplates.size() < MAX_TEMPLATES_FOUND && strategy.equals(HostKeyVerificationStrategyEnum.OFF)) { + if (veryInsecureTemplates.size() < MAX_TEMPLATES_FOUND + && strategy.equals(HostKeyVerificationStrategyEnum.OFF)) { veryInsecureTemplates.add(template.getDisplayName()); - } else if (insecureTemplates.size() < MAX_TEMPLATES_FOUND && (!strategy.equals(HostKeyVerificationStrategyEnum.CHECK_NEW_HARD))) { + } else if (insecureTemplates.size() < MAX_TEMPLATES_FOUND + && (!strategy.equals(HostKeyVerificationStrategyEnum.CHECK_NEW_HARD))) { // it is check-new-soft or accept-new insecureTemplates.add(template.getDisplayName()); } // stop collecting the status of the computers, we already have 5 each type - if (veryInsecureTemplates.size() >= MAX_TEMPLATES_FOUND || insecureTemplates.size() >= MAX_TEMPLATES_FOUND) { + if (veryInsecureTemplates.size() >= MAX_TEMPLATES_FOUND + || insecureTemplates.size() >= MAX_TEMPLATES_FOUND) { return true; } } - + return false; } - + @RequirePOST - public HttpResponse doAct(@QueryParameter String dismiss, @QueryParameter String dismissAllMessages) throws IOException { + public HttpResponse doAct(@QueryParameter String dismiss, @QueryParameter String dismissAllMessages) + throws IOException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); if (dismiss != null) { PluginImpl.get().saveDismissInsecureMessages(System.currentTimeMillis()); - } - + } + if (dismissAllMessages != null) { disable(true); } diff --git a/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategy.java b/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategy.java index 12155fd60..fe3574d2b 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategy.java +++ b/src/main/java/hudson/plugins/ec2/ssh/verifiers/SshHostKeyVerificationStrategy.java @@ -5,7 +5,7 @@ * Modified work Copyright (c) 2020-, M Ramon Leon, CloudBees, Inc. * Modified work: * - getHostKeyFromConsole method and called methods - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -35,16 +35,16 @@ import hudson.plugins.ec2.EC2Cloud; import hudson.plugins.ec2.EC2Computer; import hudson.plugins.ec2.InstanceState; -import jenkins.model.Jenkins; - +import java.security.PublicKey; import java.util.Base64; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.model.Jenkins; /** * A method for verifying the host key provided by the remote host during the * initiation of each connection. - * + * * @author Michael Clarke * @since TODO */ @@ -66,9 +66,22 @@ public SshHostKeyVerificationStrategyDescriptor getDescriptor() { */ public abstract boolean verify(EC2Computer computer, HostKey hostKey, TaskListener listener) throws Exception; - public static abstract class SshHostKeyVerificationStrategyDescriptor extends Descriptor { + /** + * Check if the given key is valid for the host identifier. + * @param computer the computer this connection is being initiated for + * @param serverKey the {@link PublicKey} that was transmitted by the remote host for the current connection. This + * is the key that should be checked to see if we trust it by the current verifier. + * @param listener the connection listener to write any output log to + * @return whether the provided {@link PublicKey} is trusted and the current connection can therefore continue. + * @since TODO + */ + public boolean verify(EC2Computer computer, PublicKey serverKey, TaskListener listener) throws Exception { + return verify(computer, HostKeyHelper.getInstance().getHostKey(serverKey), listener); } + public abstract static class SshHostKeyVerificationStrategyDescriptor + extends Descriptor {} + /** * Get the host key printed out in the console. * @param computer @@ -77,23 +90,31 @@ public static abstract class SshHostKeyVerificationStrategyDescriptor extends De * but an empty array as the key if the console is not blank and the key for such an algorithm couldn't be found. */ @Nullable - HostKey getHostKeyFromConsole(@NonNull final Logger logger, @NonNull final EC2Computer computer, @NonNull final String serverHostKeyAlgorithm) { + HostKey getHostKeyFromConsole( + @NonNull final Logger logger, + @NonNull final EC2Computer computer, + @NonNull final String serverHostKeyAlgorithm) { HostKey key; TaskListener listener = computer.getListener(); try { - if(!computer.getState().equals(InstanceState.RUNNING)) { - EC2Cloud.log(logger, Level.INFO, listener, "The instance " + computer.getName() + " is not running, waiting to validate the key against the console"); + if (!computer.getState().equals(InstanceState.RUNNING)) { + EC2Cloud.log( + logger, + Level.INFO, + listener, + "The instance " + computer.getName() + + " is not running, waiting to validate the key against the console"); } } catch (InterruptedException e) { return null; } String line = getLineWithKey(logger, computer, serverHostKeyAlgorithm); - if (line != null && line.length() > 0) { + if (line != null && !line.isEmpty()) { key = getKeyFromLine(logger, line, listener); } else if (line != null) { - key = new HostKey(serverHostKeyAlgorithm, new byte[]{}); + key = new HostKey(serverHostKeyAlgorithm, new byte[] {}); } else { key = null; } @@ -102,20 +123,28 @@ HostKey getHostKeyFromConsole(@NonNull final Logger logger, @NonNull final EC2Co } /** - * Get the line with the key for such an algorithm - * @param logger the logger to print the messages - * @param computer the computer + * Get the line with the key for such an algorithm + * @param logger the logger to print the messages + * @param computer the computer * @param serverHostKeyAlgorithm the algorithm to search for * @return the line where the key for the algorithm is on, null if the console is blank, "" if the console is not * blank and the line is not found. */ @CheckForNull - String getLineWithKey(@NonNull final Logger logger, @NonNull final EC2Computer computer, @NonNull final String serverHostKeyAlgorithm) { + String getLineWithKey( + @NonNull final Logger logger, + @NonNull final EC2Computer computer, + @NonNull final String serverHostKeyAlgorithm) { String line = null; String console = computer.getDecodedConsoleOutput(); if (console == null) { // The instance is running and the console is blank - EC2Cloud.log(logger, Level.INFO, computer.getListener(), "The instance " + computer.getName() + " has a blank console. Maybe the console is yet not available. If enough time has passed, consider changing the key verification strategy or the AMI used by one printing out the host key in the instance console"); + EC2Cloud.log( + logger, + Level.INFO, + computer.getListener(), + "The instance " + computer.getName() + + " has a blank console. Maybe the console is yet not available. If enough time has passed, consider changing the key verification strategy or the AMI used by one printing out the host key in the instance console"); return null; } @@ -126,7 +155,13 @@ String getLineWithKey(@NonNull final Logger logger, @NonNull final EC2Computer c line = console.substring(start, end); } else { // The instance printed on the console but the key was not printed with the expected format - EC2Cloud.log(logger, Level.INFO, computer.getListener(), String.format("The instance %s didn't print the host key. Expected a line starting with: \"%s\"", computer.getName(), serverHostKeyAlgorithm)); + EC2Cloud.log( + logger, + Level.INFO, + computer.getListener(), + String.format( + "The instance %s didn't print the host key. Expected a line starting with: \"%s\"", + computer.getName(), serverHostKeyAlgorithm)); return ""; } } catch (IllegalArgumentException ignored) { @@ -135,13 +170,20 @@ String getLineWithKey(@NonNull final Logger logger, @NonNull final EC2Computer c } @CheckForNull - HostKey getKeyFromLine(@NonNull final Logger logger, @NonNull final String line, @Nullable final TaskListener listener) { + HostKey getKeyFromLine( + @NonNull final Logger logger, @NonNull final String line, @Nullable final TaskListener listener) { String[] parts = line.split(" "); if (parts.length >= 2) { // The public SSH key in the console is Base64 encoded return new HostKey(parts[0], Base64.getDecoder().decode(parts[1])); } else { - EC2Cloud.log(logger, Level.INFO, listener, String.format("The line with the key doesn't have the required format. Found: \"%s\". Expected a line with this text: \"ALGORITHM THEHOSTKEY\", example: \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJbvbEIoY3tqKwkeRW/L1FnbCLLp8a1TwSOyZHKJqFFR \"", line)); + EC2Cloud.log( + logger, + Level.INFO, + listener, + String.format( + "The line with the key doesn't have the required format. Found: \"%s\". Expected a line with this text: \"ALGORITHM THEHOSTKEY\", example: \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJbvbEIoY3tqKwkeRW/L1FnbCLLp8a1TwSOyZHKJqFFR \"", + line)); return null; } } diff --git a/src/main/java/hudson/plugins/ec2/util/AmazonEC2Factory.java b/src/main/java/hudson/plugins/ec2/util/AmazonEC2Factory.java index 2961a1827..9650edfb5 100644 --- a/src/main/java/hudson/plugins/ec2/util/AmazonEC2Factory.java +++ b/src/main/java/hudson/plugins/ec2/util/AmazonEC2Factory.java @@ -1,27 +1,17 @@ package hudson.plugins.ec2.util; -import java.net.URL; - -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.services.ec2.AmazonEC2; - +import hudson.ExtensionList; import hudson.ExtensionPoint; -import jenkins.model.Jenkins; +import java.net.URI; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.ec2.Ec2Client; public interface AmazonEC2Factory extends ExtensionPoint { static AmazonEC2Factory getInstance() { - AmazonEC2Factory instance = null; - for (AmazonEC2Factory implementation : Jenkins.get().getExtensionList(AmazonEC2Factory.class)) { - if (instance != null) { - throw new IllegalStateException("Multiple implementations of " + AmazonEC2Factory.class.getName() - + " found. If overriding, please consider using ExtensionFilter"); - } - instance = implementation; - } - return instance; + return ExtensionList.lookupFirst(AmazonEC2Factory.class); } - AmazonEC2 connect(AWSCredentialsProvider credentialsProvider, URL ec2Endpoint); - + Ec2Client connect(AwsCredentialsProvider credentialsProvider, Region region, URI endpoint); } diff --git a/src/main/java/hudson/plugins/ec2/util/AmazonEC2FactoryImpl.java b/src/main/java/hudson/plugins/ec2/util/AmazonEC2FactoryImpl.java index e685c4fbb..5c988205c 100644 --- a/src/main/java/hudson/plugins/ec2/util/AmazonEC2FactoryImpl.java +++ b/src/main/java/hudson/plugins/ec2/util/AmazonEC2FactoryImpl.java @@ -1,21 +1,28 @@ package hudson.plugins.ec2.util; -import java.net.URL; - -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.services.ec2.AmazonEC2; -import com.amazonaws.services.ec2.AmazonEC2Client; - import hudson.Extension; import hudson.plugins.ec2.EC2Cloud; +import java.net.URI; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.ec2.Ec2Client; +import software.amazon.awssdk.services.ec2.Ec2ClientBuilder; @Extension public class AmazonEC2FactoryImpl implements AmazonEC2Factory { @Override - public AmazonEC2 connect(AWSCredentialsProvider credentialsProvider, URL ec2Endpoint) { - AmazonEC2 client = new AmazonEC2Client(credentialsProvider, EC2Cloud.createClientConfiguration(ec2Endpoint.getHost())); - client.setEndpoint(ec2Endpoint.toString()); - return client; + public Ec2Client connect(AwsCredentialsProvider credentialsProvider, Region region, URI endpoint) { + Ec2ClientBuilder ec2ClientBuilder = Ec2Client.builder() + .credentialsProvider(credentialsProvider) + .httpClient(EC2Cloud.getHttpClient()) + .overrideConfiguration(EC2Cloud.createClientOverrideConfiguration()); + if (region != null) { + ec2ClientBuilder.region(region); + } + if (endpoint != null) { + ec2ClientBuilder.endpointOverride(endpoint); + } + return ec2ClientBuilder.build(); } } diff --git a/src/main/java/hudson/plugins/ec2/util/DeviceMappingParser.java b/src/main/java/hudson/plugins/ec2/util/DeviceMappingParser.java index 3ea5ac7b2..d6a0816ba 100644 --- a/src/main/java/hudson/plugins/ec2/util/DeviceMappingParser.java +++ b/src/main/java/hudson/plugins/ec2/util/DeviceMappingParser.java @@ -23,38 +23,37 @@ */ package hudson.plugins.ec2.util; -import com.amazonaws.services.ec2.model.BlockDeviceMapping; -import com.amazonaws.services.ec2.model.EbsBlockDevice; -import org.apache.commons.lang.StringUtils; - import java.util.ArrayList; import java.util.List; +import org.apache.commons.lang.StringUtils; +import software.amazon.awssdk.services.ec2.model.BlockDeviceMapping; +import software.amazon.awssdk.services.ec2.model.EbsBlockDevice; public class DeviceMappingParser { - private DeviceMappingParser() { - } + private DeviceMappingParser() {} public static List parse(String customDeviceMapping) { - List deviceMappings = new ArrayList(); + List deviceMappings = new ArrayList<>(); for (String mapping : customDeviceMapping.split(",")) { String[] mappingPair = mapping.split("="); String device = mappingPair[0]; String blockDevice = mappingPair[1]; - BlockDeviceMapping deviceMapping = new BlockDeviceMapping().withDeviceName(device); + BlockDeviceMapping.Builder deviceMappingBuilder = + BlockDeviceMapping.builder().deviceName(device); if (blockDevice.equals("none")) { - deviceMapping.setNoDevice("none"); + deviceMappingBuilder.noDevice("none"); } else if (blockDevice.startsWith("ephemeral")) { - deviceMapping.setVirtualName(blockDevice); + deviceMappingBuilder.virtualName(blockDevice); } else { - deviceMapping.setEbs(parseEbs(blockDevice)); + deviceMappingBuilder.ebs(parseEbs(blockDevice)); } - deviceMappings.add(deviceMapping); + deviceMappings.add(deviceMappingBuilder.build()); } return deviceMappings; @@ -65,30 +64,30 @@ private static EbsBlockDevice parseEbs(String blockDevice) { String[] parts = blockDevice.split(":"); - EbsBlockDevice ebs = new EbsBlockDevice(); + EbsBlockDevice.Builder ebsBuilder = EbsBlockDevice.builder(); if (StringUtils.isNotBlank(getOrEmpty(parts, 0))) { - ebs.setSnapshotId(parts[0]); + ebsBuilder.snapshotId(parts[0]); } if (StringUtils.isNotBlank(getOrEmpty(parts, 1))) { - ebs.setVolumeSize(Integer.valueOf(parts[1])); + ebsBuilder.volumeSize(Integer.valueOf(parts[1])); } if (StringUtils.isNotBlank(getOrEmpty(parts, 2))) { - ebs.setDeleteOnTermination(Boolean.valueOf(parts[2])); + ebsBuilder.deleteOnTermination(Boolean.valueOf(parts[2])); } if (StringUtils.isNotBlank(getOrEmpty(parts, 3))) { - ebs.setVolumeType(parts[3]); + ebsBuilder.volumeType(parts[3]); } if (StringUtils.isNotBlank(getOrEmpty(parts, 4))) { - ebs.setIops(Integer.valueOf(parts[4])); + ebsBuilder.iops(Integer.valueOf(parts[4])); } if (StringUtils.isNotBlank(getOrEmpty(parts, 5))) { - ebs.setEncrypted(parts[5].equals("encrypted")); + ebsBuilder.encrypted(parts[5].equals("encrypted")); } if (StringUtils.isNotBlank(getOrEmpty(parts, 6))) { - ebs.setThroughput(Integer.valueOf(parts[6])); + ebsBuilder.throughput(Integer.valueOf(parts[6])); } - return ebs; + return ebsBuilder.build(); } private static String getOrEmpty(String[] arr, int idx) { diff --git a/src/main/java/hudson/plugins/ec2/util/EC2AgentConfig.java b/src/main/java/hudson/plugins/ec2/util/EC2AgentConfig.java index 444c5b88a..27b69cb26 100644 --- a/src/main/java/hudson/plugins/ec2/util/EC2AgentConfig.java +++ b/src/main/java/hudson/plugins/ec2/util/EC2AgentConfig.java @@ -1,12 +1,11 @@ package hudson.plugins.ec2.util; -import hudson.plugins.ec2.Tenancy; import hudson.model.Node; import hudson.plugins.ec2.AMITypeData; import hudson.plugins.ec2.ConnectionStrategy; import hudson.plugins.ec2.EC2Tag; +import hudson.plugins.ec2.Tenancy; import hudson.slaves.NodeProperty; - import java.util.List; public abstract class EC2AgentConfig { @@ -60,13 +59,17 @@ public static class OnDemand extends EC2AgentConfig { final String publicDNS; final String privateDNS; final Tenancy tenancy; + @Deprecated final boolean useDedicatedTenancy; + final Boolean metadataSupported; final Boolean metadataEndpointEnabled; final Boolean metadataTokensRequired; final Integer metadataHopsLimit; + final Boolean enclaveEnabled; + private OnDemand(OnDemandBuilder builder) { super(builder); this.instanceId = builder.getInstanceId(); @@ -79,6 +82,7 @@ private OnDemand(OnDemandBuilder builder) { this.metadataHopsLimit = builder.metadataHopsLimit; this.metadataEndpointEnabled = builder.metadataEndpointEnabled; this.metadataTokensRequired = builder.metadataTokensRequired; + this.enclaveEnabled = builder.enclaveEnabled; } } @@ -92,7 +96,7 @@ private Spot(SpotBuilder builder) { } } - private static abstract class Builder, C extends EC2AgentConfig> { + private abstract static class Builder, C extends EC2AgentConfig> { private String name; private String description; @@ -225,13 +229,17 @@ public static class OnDemandBuilder extends Builder { private String publicDNS; private String privateDNS; private Tenancy tenancy; + @Deprecated private boolean useDedicatedTenancy; + private Boolean metadataSupported; private Boolean metadataEndpointEnabled; private Boolean metadataTokensRequired; private Integer metadataHopsLimit; + private Boolean enclaveEnabled; + public OnDemandBuilder withInstanceId(String instanceId) { this.instanceId = instanceId; return this; @@ -279,16 +287,18 @@ public boolean isUseDedicatedTenancy() { return useDedicatedTenancy; } - public OnDemandBuilder withTenancyAttribute( Tenancy tenancy){ + public OnDemandBuilder withTenancyAttribute(Tenancy tenancy) { this.tenancy = tenancy; return this; } - public Tenancy getTenancyAttribute(){ return tenancy;} + public Tenancy getTenancyAttribute() { + return tenancy; + } public OnDemandBuilder withMetadataSupported(Boolean metadataSupported) { - this.metadataSupported = metadataSupported; - return this; + this.metadataSupported = metadataSupported; + return this; } public OnDemandBuilder withMetadataEndpointEnabled(Boolean metadataEndpointEnabled) { @@ -306,6 +316,11 @@ public OnDemandBuilder withMetadataHopsLimit(Integer metadataHopsLimit) { return this; } + public OnDemandBuilder withEnclaveEnabled(Boolean enclaveEnabled) { + this.enclaveEnabled = enclaveEnabled; + return this; + } + @Override protected OnDemandBuilder self() { return this; @@ -336,5 +351,4 @@ public Spot build() { return new Spot(this); } } - } diff --git a/src/main/java/hudson/plugins/ec2/util/EC2AgentFactory.java b/src/main/java/hudson/plugins/ec2/util/EC2AgentFactory.java index 59c79945c..5065e8483 100644 --- a/src/main/java/hudson/plugins/ec2/util/EC2AgentFactory.java +++ b/src/main/java/hudson/plugins/ec2/util/EC2AgentFactory.java @@ -1,27 +1,18 @@ package hudson.plugins.ec2.util; -import java.io.IOException; - +import hudson.ExtensionList; import hudson.model.Descriptor; -import hudson.plugins.ec2.*; -import jenkins.model.Jenkins; +import hudson.plugins.ec2.EC2OndemandSlave; +import hudson.plugins.ec2.EC2SpotSlave; +import java.io.IOException; public interface EC2AgentFactory { static EC2AgentFactory getInstance() { - EC2AgentFactory instance = null; - for (EC2AgentFactory implementation : Jenkins.get().getExtensionList(EC2AgentFactory.class)) { - if (instance != null) { - throw new IllegalStateException("Multiple implementations of " + EC2AgentFactory.class.getName() - + " found. If overriding, please consider using ExtensionFilter"); - } - instance = implementation; - } - return instance; + return ExtensionList.lookupFirst(EC2AgentFactory.class); } EC2OndemandSlave createOnDemandAgent(EC2AgentConfig.OnDemand config) throws Descriptor.FormException, IOException; EC2SpotSlave createSpotAgent(EC2AgentConfig.Spot config) throws Descriptor.FormException, IOException; - } diff --git a/src/main/java/hudson/plugins/ec2/util/EC2AgentFactoryImpl.java b/src/main/java/hudson/plugins/ec2/util/EC2AgentFactoryImpl.java index d4d4ba372..a42a0aca9 100644 --- a/src/main/java/hudson/plugins/ec2/util/EC2AgentFactoryImpl.java +++ b/src/main/java/hudson/plugins/ec2/util/EC2AgentFactoryImpl.java @@ -1,10 +1,10 @@ package hudson.plugins.ec2.util; -import java.io.IOException; - import hudson.Extension; import hudson.model.Descriptor; -import hudson.plugins.ec2.*; +import hudson.plugins.ec2.EC2OndemandSlave; +import hudson.plugins.ec2.EC2SpotSlave; +import java.io.IOException; @Extension public class EC2AgentFactoryImpl implements EC2AgentFactory { @@ -12,11 +12,60 @@ public class EC2AgentFactoryImpl implements EC2AgentFactory { @Override public EC2OndemandSlave createOnDemandAgent(EC2AgentConfig.OnDemand config) throws Descriptor.FormException, IOException { - return new EC2OndemandSlave(config.name, config.instanceId, config.description, config.remoteFS, config.numExecutors, config.labelString, config.mode, config.initScript, config.tmpDir, config.nodeProperties, config.remoteAdmin, config.javaPath, config.jvmopts, config.stopOnTerminate, config.idleTerminationMinutes, config.publicDNS, config.privateDNS, config.tags, config.cloudName, config.launchTimeout, config.amiType, config.connectionStrategy, config.maxTotalUses, config.tenancy, config.metadataEndpointEnabled, config.metadataTokensRequired, config.metadataHopsLimit, config.metadataSupported); + return new EC2OndemandSlave( + config.name, + config.instanceId, + config.description, + config.remoteFS, + config.numExecutors, + config.labelString, + config.mode, + config.initScript, + config.tmpDir, + config.nodeProperties, + config.remoteAdmin, + config.javaPath, + config.jvmopts, + config.stopOnTerminate, + config.idleTerminationMinutes, + config.publicDNS, + config.privateDNS, + config.tags, + config.cloudName, + config.launchTimeout, + config.amiType, + config.connectionStrategy, + config.maxTotalUses, + config.tenancy, + config.metadataEndpointEnabled, + config.metadataTokensRequired, + config.metadataHopsLimit, + config.metadataSupported, + config.enclaveEnabled); } @Override public EC2SpotSlave createSpotAgent(EC2AgentConfig.Spot config) throws Descriptor.FormException, IOException { - return new EC2SpotSlave(config.name, config.spotInstanceRequestId, config.description, config.remoteFS, config.numExecutors, config.mode, config.initScript, config.tmpDir, config.labelString, config.nodeProperties, config.remoteAdmin, config.javaPath, config.jvmopts, config.idleTerminationMinutes, config.tags, config.cloudName, config.launchTimeout, config.amiType, config.connectionStrategy, config.maxTotalUses); + return new EC2SpotSlave( + config.name, + config.spotInstanceRequestId, + config.description, + config.remoteFS, + config.numExecutors, + config.mode, + config.initScript, + config.tmpDir, + config.labelString, + config.nodeProperties, + config.remoteAdmin, + config.javaPath, + config.jvmopts, + config.idleTerminationMinutes, + config.tags, + config.cloudName, + config.launchTimeout, + config.amiType, + config.connectionStrategy, + config.maxTotalUses); } } diff --git a/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java new file mode 100644 index 000000000..11aa27d5b --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java @@ -0,0 +1,174 @@ +package hudson.plugins.ec2.util; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.plugins.ec2.Messages; +import java.io.IOException; +import java.net.URL; +import java.security.Key; +import java.security.UnrecoverableKeyException; +import java.security.interfaces.DSAKey; +import java.security.interfaces.ECKey; +import java.security.interfaces.RSAKey; +import jenkins.bouncycastle.api.PEMEncodable; +import jenkins.security.FIPS140; +import org.apache.commons.lang.StringUtils; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.params.DSAPublicKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.RSAKeyParameters; +import org.bouncycastle.crypto.util.OpenSSHPublicKeyUtil; + +/** + * FIPS related utility methods (check Private and Public keys, ...) + */ +public class FIPS140Utils { + + /** + * Checks if the key is allowed when FIPS mode is requested. + * Allowed key with the following algorithms and sizes: + *

    + *
  • DSA with key size >= 2048
  • + *
  • RSA with key size >= 2048
  • + *
  • Elliptic curve (ED25519) with field size >= 224
  • + *
+ * If the key is valid and allowed or not in FIPS mode method will just exit. + * If not it will throw an {@link IllegalArgumentException}. + * @param key The key to check. + */ + public static void ensureKeyInFipsMode(Key key) { + if (!FIPS140.useCompliantAlgorithms()) { + return; + } + if (key instanceof RSAKey) { + if (((RSAKey) key).getModulus().bitLength() < 2048) { + throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode()); + } + } else if (key instanceof DSAKey) { + if (((DSAKey) key).getParams().getP().bitLength() < 2048) { + throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode()); + } + } else if (key instanceof ECKey) { + if (((ECKey) key).getParams().getCurve().getField().getFieldSize() < 224) { + throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeECInFIPSMode()); + } + } else { + throw new IllegalArgumentException(Messages.EC2Cloud_keyIsNotApprovedInFIPSMode(key.getAlgorithm())); + } + } + + /** + * Password leak prevention when FIPS mode is requested. If FIPS mode is not requested, this method does nothing. + * Otherwise, ensure that no password can be leaked + * @param url the requested URL + * @param password the password used + * @throws IllegalArgumentException if there is a risk that the password will leak + */ + public static void ensureNoPasswordLeak(URL url, String password) { + ensureNoPasswordLeak("https".equals(url.getProtocol()), password); + } + + /** + * Password leak prevention when FIPS mode is requested. If FIPS mode is not requested, this method does nothing. + * Otherwise, ensure that no password can be leaked. + * @param useHTTPS is TLS used or not + * @param password the password used + * @throws IllegalArgumentException if there is a risk that the password will leak + */ + public static void ensureNoPasswordLeak(boolean useHTTPS, String password) { + ensureNoPasswordLeak(useHTTPS, !StringUtils.isEmpty(password)); + } + + /** + * Password leak prevention when FIPS mode is requested. If FIPS mode is not requested, this method does nothing. + * Otherwise, ensure that no password can be leaked. + * @param useHTTPS is TLS used or not + * @param usePassword is a password used + * @throws IllegalArgumentException if there is a risk that the password will leak + */ + public static void ensureNoPasswordLeak(boolean useHTTPS, boolean usePassword) { + if (FIPS140.useCompliantAlgorithms()) { + if (!useHTTPS && usePassword) { + throw new IllegalArgumentException(Messages.EC2Cloud_tlsIsRequiredInFIPSMode()); + } + } + } + + /** + * Password length check chen FIPS mode is requested. If FIPS mode is not requested, this method does nothing. + * Otherwise, ensure that the password length is at least 14 char long. + * @param password the password to check + * @throws IllegalArgumentException if FIPS mode is requested and the password is too short + */ + public static void ensurePasswordLength(String password) { + if (FIPS140.useCompliantAlgorithms()) { + if (StringUtils.isBlank(password) || password.length() < 14) { + throw new IllegalArgumentException(Messages.EC2Cloud_passwordLengthInFIPSMode()); + } + } + } + + /** + * Password leak prevention when FIPS mode is requested. If FIPS mode is not requested, this method does nothing. + * Otherwise, ensure that no password can be leaked. + * @param allowSelfSignedCertificate is self-signed certificate allowed + * @throws IllegalArgumentException if FIPS mode is requested and a self-signed certificate is allowed + */ + public static void ensureNoSelfSignedCertificate(boolean allowSelfSignedCertificate) { + if (FIPS140.useCompliantAlgorithms()) { + if (allowSelfSignedCertificate) { + throw new IllegalArgumentException(Messages.EC2Cloud_selfSignedCertificateNotAllowedInFIPSMode()); + } + } + } + + /** + * Checks if the private key is allowed when FIPS mode is requested. + * Allowed private key with the following algorithms and sizes: + *
    + *
  • DSA with key size >= 2048
  • + *
  • RSA with key size >= 2048
  • + *
  • Elliptic curve (ED25519) with field size >= 224
  • + *
+ * If the private key is valid and allowed or not in FIPS mode method will just exit. + * If not it will throw an {@link IllegalArgumentException}. + * @param privateKeyString String containing the private key PEM. + */ + public static void ensurePrivateKeyInFipsMode(String privateKeyString) { + if (!FIPS140.useCompliantAlgorithms()) { + return; + } + if (StringUtils.isBlank(privateKeyString)) { + throw new IllegalArgumentException(Messages.EC2Cloud_keyIsMandatoryInFIPSMode()); + } + try { + Key privateKey = PEMEncodable.decode(privateKeyString).toPrivateKey(); + ensureKeyInFipsMode(privateKey); + } catch (RuntimeException | UnrecoverableKeyException | IOException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + + public static void ensurePublicKeyInFipsMode(@NonNull String algorithm, @NonNull byte[] key) { + if (!FIPS140.useCompliantAlgorithms()) { + return; + } + + AsymmetricKeyParameter asymmetricKeyParameter = OpenSSHPublicKeyUtil.parsePublicKey(key); + + if (asymmetricKeyParameter instanceof RSAKeyParameters rsaKeyParameters) { + if (rsaKeyParameters.getModulus().bitLength() < 2048) { + throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode()); + } + } else if (asymmetricKeyParameter instanceof DSAPublicKeyParameters dsaPublicKeyParameters) { + if (dsaPublicKeyParameters.getParameters().getP().bitLength() < 2048) { + throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode()); + } + } else if (asymmetricKeyParameter instanceof ECPublicKeyParameters ecPublicKeyParameters) { + if (ecPublicKeyParameters.getParameters().getCurve().getFieldSize() < 224) { + throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeECInFIPSMode()); + } + } else { + throw new IllegalArgumentException(Messages.EC2Cloud_keyIsNotApprovedInFIPSMode(algorithm)); + } + } +} diff --git a/src/main/java/hudson/plugins/ec2/util/InstanceTypeCompat.java b/src/main/java/hudson/plugins/ec2/util/InstanceTypeCompat.java new file mode 100644 index 000000000..3d21cbd8c --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/util/InstanceTypeCompat.java @@ -0,0 +1,918 @@ +package hudson.plugins.ec2.util; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import software.amazon.awssdk.services.ec2.model.InstanceType; + +@Restricted(NoExternalUse.class) +public final class InstanceTypeCompat { + + private static final Map AWS_SDK_JAVA_V1; + + static { + Map map = new HashMap<>(); + map.put("A1Medium", "a1.medium"); + map.put("A1Large", "a1.large"); + map.put("A1Xlarge", "a1.xlarge"); + map.put("A12xlarge", "a1.2xlarge"); + map.put("A14xlarge", "a1.4xlarge"); + map.put("A1Metal", "a1.metal"); + map.put("C1Medium", "c1.medium"); + map.put("C1Xlarge", "c1.xlarge"); + map.put("C3Large", "c3.large"); + map.put("C3Xlarge", "c3.xlarge"); + map.put("C32xlarge", "c3.2xlarge"); + map.put("C34xlarge", "c3.4xlarge"); + map.put("C38xlarge", "c3.8xlarge"); + map.put("C4Large", "c4.large"); + map.put("C4Xlarge", "c4.xlarge"); + map.put("C42xlarge", "c4.2xlarge"); + map.put("C44xlarge", "c4.4xlarge"); + map.put("C48xlarge", "c4.8xlarge"); + map.put("C5Large", "c5.large"); + map.put("C5Xlarge", "c5.xlarge"); + map.put("C52xlarge", "c5.2xlarge"); + map.put("C54xlarge", "c5.4xlarge"); + map.put("C59xlarge", "c5.9xlarge"); + map.put("C512xlarge", "c5.12xlarge"); + map.put("C518xlarge", "c5.18xlarge"); + map.put("C524xlarge", "c5.24xlarge"); + map.put("C5Metal", "c5.metal"); + map.put("C5aLarge", "c5a.large"); + map.put("C5aXlarge", "c5a.xlarge"); + map.put("C5a2xlarge", "c5a.2xlarge"); + map.put("C5a4xlarge", "c5a.4xlarge"); + map.put("C5a8xlarge", "c5a.8xlarge"); + map.put("C5a12xlarge", "c5a.12xlarge"); + map.put("C5a16xlarge", "c5a.16xlarge"); + map.put("C5a24xlarge", "c5a.24xlarge"); + map.put("C5adLarge", "c5ad.large"); + map.put("C5adXlarge", "c5ad.xlarge"); + map.put("C5ad2xlarge", "c5ad.2xlarge"); + map.put("C5ad4xlarge", "c5ad.4xlarge"); + map.put("C5ad8xlarge", "c5ad.8xlarge"); + map.put("C5ad12xlarge", "c5ad.12xlarge"); + map.put("C5ad16xlarge", "c5ad.16xlarge"); + map.put("C5ad24xlarge", "c5ad.24xlarge"); + map.put("C5dLarge", "c5d.large"); + map.put("C5dXlarge", "c5d.xlarge"); + map.put("C5d2xlarge", "c5d.2xlarge"); + map.put("C5d4xlarge", "c5d.4xlarge"); + map.put("C5d9xlarge", "c5d.9xlarge"); + map.put("C5d12xlarge", "c5d.12xlarge"); + map.put("C5d18xlarge", "c5d.18xlarge"); + map.put("C5d24xlarge", "c5d.24xlarge"); + map.put("C5dMetal", "c5d.metal"); + map.put("C5nLarge", "c5n.large"); + map.put("C5nXlarge", "c5n.xlarge"); + map.put("C5n2xlarge", "c5n.2xlarge"); + map.put("C5n4xlarge", "c5n.4xlarge"); + map.put("C5n9xlarge", "c5n.9xlarge"); + map.put("C5n18xlarge", "c5n.18xlarge"); + map.put("C5nMetal", "c5n.metal"); + map.put("C6gMedium", "c6g.medium"); + map.put("C6gLarge", "c6g.large"); + map.put("C6gXlarge", "c6g.xlarge"); + map.put("C6g2xlarge", "c6g.2xlarge"); + map.put("C6g4xlarge", "c6g.4xlarge"); + map.put("C6g8xlarge", "c6g.8xlarge"); + map.put("C6g12xlarge", "c6g.12xlarge"); + map.put("C6g16xlarge", "c6g.16xlarge"); + map.put("C6gMetal", "c6g.metal"); + map.put("C6gdMedium", "c6gd.medium"); + map.put("C6gdLarge", "c6gd.large"); + map.put("C6gdXlarge", "c6gd.xlarge"); + map.put("C6gd2xlarge", "c6gd.2xlarge"); + map.put("C6gd4xlarge", "c6gd.4xlarge"); + map.put("C6gd8xlarge", "c6gd.8xlarge"); + map.put("C6gd12xlarge", "c6gd.12xlarge"); + map.put("C6gd16xlarge", "c6gd.16xlarge"); + map.put("C6gdMetal", "c6gd.metal"); + map.put("C6gnMedium", "c6gn.medium"); + map.put("C6gnLarge", "c6gn.large"); + map.put("C6gnXlarge", "c6gn.xlarge"); + map.put("C6gn2xlarge", "c6gn.2xlarge"); + map.put("C6gn4xlarge", "c6gn.4xlarge"); + map.put("C6gn8xlarge", "c6gn.8xlarge"); + map.put("C6gn12xlarge", "c6gn.12xlarge"); + map.put("C6gn16xlarge", "c6gn.16xlarge"); + map.put("C6iLarge", "c6i.large"); + map.put("C6iXlarge", "c6i.xlarge"); + map.put("C6i2xlarge", "c6i.2xlarge"); + map.put("C6i4xlarge", "c6i.4xlarge"); + map.put("C6i8xlarge", "c6i.8xlarge"); + map.put("C6i12xlarge", "c6i.12xlarge"); + map.put("C6i16xlarge", "c6i.16xlarge"); + map.put("C6i24xlarge", "c6i.24xlarge"); + map.put("C6i32xlarge", "c6i.32xlarge"); + map.put("C6iMetal", "c6i.metal"); + map.put("Cc14xlarge", "cc1.4xlarge"); + map.put("Cc28xlarge", "cc2.8xlarge"); + map.put("Cg14xlarge", "cg1.4xlarge"); + map.put("Cr18xlarge", "cr1.8xlarge"); + map.put("D2Xlarge", "d2.xlarge"); + map.put("D22xlarge", "d2.2xlarge"); + map.put("D24xlarge", "d2.4xlarge"); + map.put("D28xlarge", "d2.8xlarge"); + map.put("D3Xlarge", "d3.xlarge"); + map.put("D32xlarge", "d3.2xlarge"); + map.put("D34xlarge", "d3.4xlarge"); + map.put("D38xlarge", "d3.8xlarge"); + map.put("D3enXlarge", "d3en.xlarge"); + map.put("D3en2xlarge", "d3en.2xlarge"); + map.put("D3en4xlarge", "d3en.4xlarge"); + map.put("D3en6xlarge", "d3en.6xlarge"); + map.put("D3en8xlarge", "d3en.8xlarge"); + map.put("D3en12xlarge", "d3en.12xlarge"); + map.put("Dl124xlarge", "dl1.24xlarge"); + map.put("F12xlarge", "f1.2xlarge"); + map.put("F14xlarge", "f1.4xlarge"); + map.put("F116xlarge", "f1.16xlarge"); + map.put("G22xlarge", "g2.2xlarge"); + map.put("G28xlarge", "g2.8xlarge"); + map.put("G34xlarge", "g3.4xlarge"); + map.put("G38xlarge", "g3.8xlarge"); + map.put("G316xlarge", "g3.16xlarge"); + map.put("G3sXlarge", "g3s.xlarge"); + map.put("G4adXlarge", "g4ad.xlarge"); + map.put("G4ad2xlarge", "g4ad.2xlarge"); + map.put("G4ad4xlarge", "g4ad.4xlarge"); + map.put("G4ad8xlarge", "g4ad.8xlarge"); + map.put("G4ad16xlarge", "g4ad.16xlarge"); + map.put("G4dnXlarge", "g4dn.xlarge"); + map.put("G4dn2xlarge", "g4dn.2xlarge"); + map.put("G4dn4xlarge", "g4dn.4xlarge"); + map.put("G4dn8xlarge", "g4dn.8xlarge"); + map.put("G4dn12xlarge", "g4dn.12xlarge"); + map.put("G4dn16xlarge", "g4dn.16xlarge"); + map.put("G4dnMetal", "g4dn.metal"); + map.put("G5Xlarge", "g5.xlarge"); + map.put("G52xlarge", "g5.2xlarge"); + map.put("G54xlarge", "g5.4xlarge"); + map.put("G58xlarge", "g5.8xlarge"); + map.put("G512xlarge", "g5.12xlarge"); + map.put("G516xlarge", "g5.16xlarge"); + map.put("G524xlarge", "g5.24xlarge"); + map.put("G548xlarge", "g5.48xlarge"); + map.put("G5gXlarge", "g5g.xlarge"); + map.put("G5g2xlarge", "g5g.2xlarge"); + map.put("G5g4xlarge", "g5g.4xlarge"); + map.put("G5g8xlarge", "g5g.8xlarge"); + map.put("G5g16xlarge", "g5g.16xlarge"); + map.put("G5gMetal", "g5g.metal"); + map.put("Hi14xlarge", "hi1.4xlarge"); + map.put("Hpc6a48xlarge", "hpc6a.48xlarge"); + map.put("Hs18xlarge", "hs1.8xlarge"); + map.put("H12xlarge", "h1.2xlarge"); + map.put("H14xlarge", "h1.4xlarge"); + map.put("H18xlarge", "h1.8xlarge"); + map.put("H116xlarge", "h1.16xlarge"); + map.put("I2Xlarge", "i2.xlarge"); + map.put("I22xlarge", "i2.2xlarge"); + map.put("I24xlarge", "i2.4xlarge"); + map.put("I28xlarge", "i2.8xlarge"); + map.put("I3Large", "i3.large"); + map.put("I3Xlarge", "i3.xlarge"); + map.put("I32xlarge", "i3.2xlarge"); + map.put("I34xlarge", "i3.4xlarge"); + map.put("I38xlarge", "i3.8xlarge"); + map.put("I316xlarge", "i3.16xlarge"); + map.put("I3Metal", "i3.metal"); + map.put("I3enLarge", "i3en.large"); + map.put("I3enXlarge", "i3en.xlarge"); + map.put("I3en2xlarge", "i3en.2xlarge"); + map.put("I3en3xlarge", "i3en.3xlarge"); + map.put("I3en6xlarge", "i3en.6xlarge"); + map.put("I3en12xlarge", "i3en.12xlarge"); + map.put("I3en24xlarge", "i3en.24xlarge"); + map.put("I3enMetal", "i3en.metal"); + map.put("Im4gnLarge", "im4gn.large"); + map.put("Im4gnXlarge", "im4gn.xlarge"); + map.put("Im4gn2xlarge", "im4gn.2xlarge"); + map.put("Im4gn4xlarge", "im4gn.4xlarge"); + map.put("Im4gn8xlarge", "im4gn.8xlarge"); + map.put("Im4gn16xlarge", "im4gn.16xlarge"); + map.put("Inf1Xlarge", "inf1.xlarge"); + map.put("Inf12xlarge", "inf1.2xlarge"); + map.put("Inf16xlarge", "inf1.6xlarge"); + map.put("Inf124xlarge", "inf1.24xlarge"); + map.put("Is4genMedium", "is4gen.medium"); + map.put("Is4genLarge", "is4gen.large"); + map.put("Is4genXlarge", "is4gen.xlarge"); + map.put("Is4gen2xlarge", "is4gen.2xlarge"); + map.put("Is4gen4xlarge", "is4gen.4xlarge"); + map.put("Is4gen8xlarge", "is4gen.8xlarge"); + map.put("M1Small", "m1.small"); + map.put("M1Medium", "m1.medium"); + map.put("M1Large", "m1.large"); + map.put("M1Xlarge", "m1.xlarge"); + map.put("M2Xlarge", "m2.xlarge"); + map.put("M22xlarge", "m2.2xlarge"); + map.put("M24xlarge", "m2.4xlarge"); + map.put("M3Medium", "m3.medium"); + map.put("M3Large", "m3.large"); + map.put("M3Xlarge", "m3.xlarge"); + map.put("M32xlarge", "m3.2xlarge"); + map.put("M4Large", "m4.large"); + map.put("M4Xlarge", "m4.xlarge"); + map.put("M42xlarge", "m4.2xlarge"); + map.put("M44xlarge", "m4.4xlarge"); + map.put("M410xlarge", "m4.10xlarge"); + map.put("M416xlarge", "m4.16xlarge"); + map.put("M5Large", "m5.large"); + map.put("M5Xlarge", "m5.xlarge"); + map.put("M52xlarge", "m5.2xlarge"); + map.put("M54xlarge", "m5.4xlarge"); + map.put("M58xlarge", "m5.8xlarge"); + map.put("M512xlarge", "m5.12xlarge"); + map.put("M516xlarge", "m5.16xlarge"); + map.put("M524xlarge", "m5.24xlarge"); + map.put("M5Metal", "m5.metal"); + map.put("M5aLarge", "m5a.large"); + map.put("M5aXlarge", "m5a.xlarge"); + map.put("M5a2xlarge", "m5a.2xlarge"); + map.put("M5a4xlarge", "m5a.4xlarge"); + map.put("M5a8xlarge", "m5a.8xlarge"); + map.put("M5a12xlarge", "m5a.12xlarge"); + map.put("M5a16xlarge", "m5a.16xlarge"); + map.put("M5a24xlarge", "m5a.24xlarge"); + map.put("M5adLarge", "m5ad.large"); + map.put("M5adXlarge", "m5ad.xlarge"); + map.put("M5ad2xlarge", "m5ad.2xlarge"); + map.put("M5ad4xlarge", "m5ad.4xlarge"); + map.put("M5ad8xlarge", "m5ad.8xlarge"); + map.put("M5ad12xlarge", "m5ad.12xlarge"); + map.put("M5ad16xlarge", "m5ad.16xlarge"); + map.put("M5ad24xlarge", "m5ad.24xlarge"); + map.put("M5dLarge", "m5d.large"); + map.put("M5dXlarge", "m5d.xlarge"); + map.put("M5d2xlarge", "m5d.2xlarge"); + map.put("M5d4xlarge", "m5d.4xlarge"); + map.put("M5d8xlarge", "m5d.8xlarge"); + map.put("M5d12xlarge", "m5d.12xlarge"); + map.put("M5d16xlarge", "m5d.16xlarge"); + map.put("M5d24xlarge", "m5d.24xlarge"); + map.put("M5dMetal", "m5d.metal"); + map.put("M5dnLarge", "m5dn.large"); + map.put("M5dnXlarge", "m5dn.xlarge"); + map.put("M5dn2xlarge", "m5dn.2xlarge"); + map.put("M5dn4xlarge", "m5dn.4xlarge"); + map.put("M5dn8xlarge", "m5dn.8xlarge"); + map.put("M5dn12xlarge", "m5dn.12xlarge"); + map.put("M5dn16xlarge", "m5dn.16xlarge"); + map.put("M5dn24xlarge", "m5dn.24xlarge"); + map.put("M5dnMetal", "m5dn.metal"); + map.put("M5nLarge", "m5n.large"); + map.put("M5nXlarge", "m5n.xlarge"); + map.put("M5n2xlarge", "m5n.2xlarge"); + map.put("M5n4xlarge", "m5n.4xlarge"); + map.put("M5n8xlarge", "m5n.8xlarge"); + map.put("M5n12xlarge", "m5n.12xlarge"); + map.put("M5n16xlarge", "m5n.16xlarge"); + map.put("M5n24xlarge", "m5n.24xlarge"); + map.put("M5nMetal", "m5n.metal"); + map.put("M5znLarge", "m5zn.large"); + map.put("M5znXlarge", "m5zn.xlarge"); + map.put("M5zn2xlarge", "m5zn.2xlarge"); + map.put("M5zn3xlarge", "m5zn.3xlarge"); + map.put("M5zn6xlarge", "m5zn.6xlarge"); + map.put("M5zn12xlarge", "m5zn.12xlarge"); + map.put("M5znMetal", "m5zn.metal"); + map.put("M6aLarge", "m6a.large"); + map.put("M6aXlarge", "m6a.xlarge"); + map.put("M6a2xlarge", "m6a.2xlarge"); + map.put("M6a4xlarge", "m6a.4xlarge"); + map.put("M6a8xlarge", "m6a.8xlarge"); + map.put("M6a12xlarge", "m6a.12xlarge"); + map.put("M6a16xlarge", "m6a.16xlarge"); + map.put("M6a24xlarge", "m6a.24xlarge"); + map.put("M6a32xlarge", "m6a.32xlarge"); + map.put("M6a48xlarge", "m6a.48xlarge"); + map.put("M6gMetal", "m6g.metal"); + map.put("M6gMedium", "m6g.medium"); + map.put("M6gLarge", "m6g.large"); + map.put("M6gXlarge", "m6g.xlarge"); + map.put("M6g2xlarge", "m6g.2xlarge"); + map.put("M6g4xlarge", "m6g.4xlarge"); + map.put("M6g8xlarge", "m6g.8xlarge"); + map.put("M6g12xlarge", "m6g.12xlarge"); + map.put("M6g16xlarge", "m6g.16xlarge"); + map.put("M6gdMetal", "m6gd.metal"); + map.put("M6gdMedium", "m6gd.medium"); + map.put("M6gdLarge", "m6gd.large"); + map.put("M6gdXlarge", "m6gd.xlarge"); + map.put("M6gd2xlarge", "m6gd.2xlarge"); + map.put("M6gd4xlarge", "m6gd.4xlarge"); + map.put("M6gd8xlarge", "m6gd.8xlarge"); + map.put("M6gd12xlarge", "m6gd.12xlarge"); + map.put("M6gd16xlarge", "m6gd.16xlarge"); + map.put("M6iLarge", "m6i.large"); + map.put("M6iXlarge", "m6i.xlarge"); + map.put("M6i2xlarge", "m6i.2xlarge"); + map.put("M6i4xlarge", "m6i.4xlarge"); + map.put("M6i8xlarge", "m6i.8xlarge"); + map.put("M6i12xlarge", "m6i.12xlarge"); + map.put("M6i16xlarge", "m6i.16xlarge"); + map.put("M6i24xlarge", "m6i.24xlarge"); + map.put("M6i32xlarge", "m6i.32xlarge"); + map.put("M6iMetal", "m6i.metal"); + map.put("Mac1Metal", "mac1.metal"); + map.put("P2Xlarge", "p2.xlarge"); + map.put("P28xlarge", "p2.8xlarge"); + map.put("P216xlarge", "p2.16xlarge"); + map.put("P32xlarge", "p3.2xlarge"); + map.put("P38xlarge", "p3.8xlarge"); + map.put("P316xlarge", "p3.16xlarge"); + map.put("P3dn24xlarge", "p3dn.24xlarge"); + map.put("P4d24xlarge", "p4d.24xlarge"); + map.put("R3Large", "r3.large"); + map.put("R3Xlarge", "r3.xlarge"); + map.put("R32xlarge", "r3.2xlarge"); + map.put("R34xlarge", "r3.4xlarge"); + map.put("R38xlarge", "r3.8xlarge"); + map.put("R4Large", "r4.large"); + map.put("R4Xlarge", "r4.xlarge"); + map.put("R42xlarge", "r4.2xlarge"); + map.put("R44xlarge", "r4.4xlarge"); + map.put("R48xlarge", "r4.8xlarge"); + map.put("R416xlarge", "r4.16xlarge"); + map.put("R5Large", "r5.large"); + map.put("R5Xlarge", "r5.xlarge"); + map.put("R52xlarge", "r5.2xlarge"); + map.put("R54xlarge", "r5.4xlarge"); + map.put("R58xlarge", "r5.8xlarge"); + map.put("R512xlarge", "r5.12xlarge"); + map.put("R516xlarge", "r5.16xlarge"); + map.put("R524xlarge", "r5.24xlarge"); + map.put("R5Metal", "r5.metal"); + map.put("R5aLarge", "r5a.large"); + map.put("R5aXlarge", "r5a.xlarge"); + map.put("R5a2xlarge", "r5a.2xlarge"); + map.put("R5a4xlarge", "r5a.4xlarge"); + map.put("R5a8xlarge", "r5a.8xlarge"); + map.put("R5a12xlarge", "r5a.12xlarge"); + map.put("R5a16xlarge", "r5a.16xlarge"); + map.put("R5a24xlarge", "r5a.24xlarge"); + map.put("R5adLarge", "r5ad.large"); + map.put("R5adXlarge", "r5ad.xlarge"); + map.put("R5ad2xlarge", "r5ad.2xlarge"); + map.put("R5ad4xlarge", "r5ad.4xlarge"); + map.put("R5ad8xlarge", "r5ad.8xlarge"); + map.put("R5ad12xlarge", "r5ad.12xlarge"); + map.put("R5ad16xlarge", "r5ad.16xlarge"); + map.put("R5ad24xlarge", "r5ad.24xlarge"); + map.put("R5bLarge", "r5b.large"); + map.put("R5bXlarge", "r5b.xlarge"); + map.put("R5b2xlarge", "r5b.2xlarge"); + map.put("R5b4xlarge", "r5b.4xlarge"); + map.put("R5b8xlarge", "r5b.8xlarge"); + map.put("R5b12xlarge", "r5b.12xlarge"); + map.put("R5b16xlarge", "r5b.16xlarge"); + map.put("R5b24xlarge", "r5b.24xlarge"); + map.put("R5bMetal", "r5b.metal"); + map.put("R5dLarge", "r5d.large"); + map.put("R5dXlarge", "r5d.xlarge"); + map.put("R5d2xlarge", "r5d.2xlarge"); + map.put("R5d4xlarge", "r5d.4xlarge"); + map.put("R5d8xlarge", "r5d.8xlarge"); + map.put("R5d12xlarge", "r5d.12xlarge"); + map.put("R5d16xlarge", "r5d.16xlarge"); + map.put("R5d24xlarge", "r5d.24xlarge"); + map.put("R5dMetal", "r5d.metal"); + map.put("R5dnLarge", "r5dn.large"); + map.put("R5dnXlarge", "r5dn.xlarge"); + map.put("R5dn2xlarge", "r5dn.2xlarge"); + map.put("R5dn4xlarge", "r5dn.4xlarge"); + map.put("R5dn8xlarge", "r5dn.8xlarge"); + map.put("R5dn12xlarge", "r5dn.12xlarge"); + map.put("R5dn16xlarge", "r5dn.16xlarge"); + map.put("R5dn24xlarge", "r5dn.24xlarge"); + map.put("R5dnMetal", "r5dn.metal"); + map.put("R5nLarge", "r5n.large"); + map.put("R5nXlarge", "r5n.xlarge"); + map.put("R5n2xlarge", "r5n.2xlarge"); + map.put("R5n4xlarge", "r5n.4xlarge"); + map.put("R5n8xlarge", "r5n.8xlarge"); + map.put("R5n12xlarge", "r5n.12xlarge"); + map.put("R5n16xlarge", "r5n.16xlarge"); + map.put("R5n24xlarge", "r5n.24xlarge"); + map.put("R5nMetal", "r5n.metal"); + map.put("R6gMedium", "r6g.medium"); + map.put("R6gLarge", "r6g.large"); + map.put("R6gXlarge", "r6g.xlarge"); + map.put("R6g2xlarge", "r6g.2xlarge"); + map.put("R6g4xlarge", "r6g.4xlarge"); + map.put("R6g8xlarge", "r6g.8xlarge"); + map.put("R6g12xlarge", "r6g.12xlarge"); + map.put("R6g16xlarge", "r6g.16xlarge"); + map.put("R6gMetal", "r6g.metal"); + map.put("R6gdMedium", "r6gd.medium"); + map.put("R6gdLarge", "r6gd.large"); + map.put("R6gdXlarge", "r6gd.xlarge"); + map.put("R6gd2xlarge", "r6gd.2xlarge"); + map.put("R6gd4xlarge", "r6gd.4xlarge"); + map.put("R6gd8xlarge", "r6gd.8xlarge"); + map.put("R6gd12xlarge", "r6gd.12xlarge"); + map.put("R6gd16xlarge", "r6gd.16xlarge"); + map.put("R6gdMetal", "r6gd.metal"); + map.put("R6iLarge", "r6i.large"); + map.put("R6iXlarge", "r6i.xlarge"); + map.put("R6i2xlarge", "r6i.2xlarge"); + map.put("R6i4xlarge", "r6i.4xlarge"); + map.put("R6i8xlarge", "r6i.8xlarge"); + map.put("R6i12xlarge", "r6i.12xlarge"); + map.put("R6i16xlarge", "r6i.16xlarge"); + map.put("R6i24xlarge", "r6i.24xlarge"); + map.put("R6i32xlarge", "r6i.32xlarge"); + map.put("R6iMetal", "r6i.metal"); + map.put("T1Micro", "t1.micro"); + map.put("T2Nano", "t2.nano"); + map.put("T2Micro", "t2.micro"); + map.put("T2Small", "t2.small"); + map.put("T2Medium", "t2.medium"); + map.put("T2Large", "t2.large"); + map.put("T2Xlarge", "t2.xlarge"); + map.put("T22xlarge", "t2.2xlarge"); + map.put("T3Nano", "t3.nano"); + map.put("T3Micro", "t3.micro"); + map.put("T3Small", "t3.small"); + map.put("T3Medium", "t3.medium"); + map.put("T3Large", "t3.large"); + map.put("T3Xlarge", "t3.xlarge"); + map.put("T32xlarge", "t3.2xlarge"); + map.put("T3aNano", "t3a.nano"); + map.put("T3aMicro", "t3a.micro"); + map.put("T3aSmall", "t3a.small"); + map.put("T3aMedium", "t3a.medium"); + map.put("T3aLarge", "t3a.large"); + map.put("T3aXlarge", "t3a.xlarge"); + map.put("T3a2xlarge", "t3a.2xlarge"); + map.put("T4gNano", "t4g.nano"); + map.put("T4gMicro", "t4g.micro"); + map.put("T4gSmall", "t4g.small"); + map.put("T4gMedium", "t4g.medium"); + map.put("T4gLarge", "t4g.large"); + map.put("T4gXlarge", "t4g.xlarge"); + map.put("T4g2xlarge", "t4g.2xlarge"); + map.put("U6tb156xlarge", "u-6tb1.56xlarge"); + map.put("U6tb1112xlarge", "u-6tb1.112xlarge"); + map.put("U9tb1112xlarge", "u-9tb1.112xlarge"); + map.put("U12tb1112xlarge", "u-12tb1.112xlarge"); + map.put("U6tb1Metal", "u-6tb1.metal"); + map.put("U9tb1Metal", "u-9tb1.metal"); + map.put("U12tb1Metal", "u-12tb1.metal"); + map.put("U18tb1Metal", "u-18tb1.metal"); + map.put("U24tb1Metal", "u-24tb1.metal"); + map.put("Vt13xlarge", "vt1.3xlarge"); + map.put("Vt16xlarge", "vt1.6xlarge"); + map.put("Vt124xlarge", "vt1.24xlarge"); + map.put("X116xlarge", "x1.16xlarge"); + map.put("X132xlarge", "x1.32xlarge"); + map.put("X1eXlarge", "x1e.xlarge"); + map.put("X1e2xlarge", "x1e.2xlarge"); + map.put("X1e4xlarge", "x1e.4xlarge"); + map.put("X1e8xlarge", "x1e.8xlarge"); + map.put("X1e16xlarge", "x1e.16xlarge"); + map.put("X1e32xlarge", "x1e.32xlarge"); + map.put("X2iezn2xlarge", "x2iezn.2xlarge"); + map.put("X2iezn4xlarge", "x2iezn.4xlarge"); + map.put("X2iezn6xlarge", "x2iezn.6xlarge"); + map.put("X2iezn8xlarge", "x2iezn.8xlarge"); + map.put("X2iezn12xlarge", "x2iezn.12xlarge"); + map.put("X2ieznMetal", "x2iezn.metal"); + map.put("X2gdMedium", "x2gd.medium"); + map.put("X2gdLarge", "x2gd.large"); + map.put("X2gdXlarge", "x2gd.xlarge"); + map.put("X2gd2xlarge", "x2gd.2xlarge"); + map.put("X2gd4xlarge", "x2gd.4xlarge"); + map.put("X2gd8xlarge", "x2gd.8xlarge"); + map.put("X2gd12xlarge", "x2gd.12xlarge"); + map.put("X2gd16xlarge", "x2gd.16xlarge"); + map.put("X2gdMetal", "x2gd.metal"); + map.put("Z1dLarge", "z1d.large"); + map.put("Z1dXlarge", "z1d.xlarge"); + map.put("Z1d2xlarge", "z1d.2xlarge"); + map.put("Z1d3xlarge", "z1d.3xlarge"); + map.put("Z1d6xlarge", "z1d.6xlarge"); + map.put("Z1d12xlarge", "z1d.12xlarge"); + map.put("Z1dMetal", "z1d.metal"); + map.put("X2idn16xlarge", "x2idn.16xlarge"); + map.put("X2idn24xlarge", "x2idn.24xlarge"); + map.put("X2idn32xlarge", "x2idn.32xlarge"); + map.put("X2iednXlarge", "x2iedn.xlarge"); + map.put("X2iedn2xlarge", "x2iedn.2xlarge"); + map.put("X2iedn4xlarge", "x2iedn.4xlarge"); + map.put("X2iedn8xlarge", "x2iedn.8xlarge"); + map.put("X2iedn16xlarge", "x2iedn.16xlarge"); + map.put("X2iedn24xlarge", "x2iedn.24xlarge"); + map.put("X2iedn32xlarge", "x2iedn.32xlarge"); + map.put("C6aLarge", "c6a.large"); + map.put("C6aXlarge", "c6a.xlarge"); + map.put("C6a2xlarge", "c6a.2xlarge"); + map.put("C6a4xlarge", "c6a.4xlarge"); + map.put("C6a8xlarge", "c6a.8xlarge"); + map.put("C6a12xlarge", "c6a.12xlarge"); + map.put("C6a16xlarge", "c6a.16xlarge"); + map.put("C6a24xlarge", "c6a.24xlarge"); + map.put("C6a32xlarge", "c6a.32xlarge"); + map.put("C6a48xlarge", "c6a.48xlarge"); + map.put("C6aMetal", "c6a.metal"); + map.put("M6aMetal", "m6a.metal"); + map.put("I4iLarge", "i4i.large"); + map.put("I4iXlarge", "i4i.xlarge"); + map.put("I4i2xlarge", "i4i.2xlarge"); + map.put("I4i4xlarge", "i4i.4xlarge"); + map.put("I4i8xlarge", "i4i.8xlarge"); + map.put("I4i16xlarge", "i4i.16xlarge"); + map.put("I4i32xlarge", "i4i.32xlarge"); + map.put("I4iMetal", "i4i.metal"); + map.put("X2idnMetal", "x2idn.metal"); + map.put("X2iednMetal", "x2iedn.metal"); + map.put("C7gMedium", "c7g.medium"); + map.put("C7gLarge", "c7g.large"); + map.put("C7gXlarge", "c7g.xlarge"); + map.put("C7g2xlarge", "c7g.2xlarge"); + map.put("C7g4xlarge", "c7g.4xlarge"); + map.put("C7g8xlarge", "c7g.8xlarge"); + map.put("C7g12xlarge", "c7g.12xlarge"); + map.put("C7g16xlarge", "c7g.16xlarge"); + map.put("Mac2Metal", "mac2.metal"); + map.put("C6idLarge", "c6id.large"); + map.put("C6idXlarge", "c6id.xlarge"); + map.put("C6id2xlarge", "c6id.2xlarge"); + map.put("C6id4xlarge", "c6id.4xlarge"); + map.put("C6id8xlarge", "c6id.8xlarge"); + map.put("C6id12xlarge", "c6id.12xlarge"); + map.put("C6id16xlarge", "c6id.16xlarge"); + map.put("C6id24xlarge", "c6id.24xlarge"); + map.put("C6id32xlarge", "c6id.32xlarge"); + map.put("C6idMetal", "c6id.metal"); + map.put("M6idLarge", "m6id.large"); + map.put("M6idXlarge", "m6id.xlarge"); + map.put("M6id2xlarge", "m6id.2xlarge"); + map.put("M6id4xlarge", "m6id.4xlarge"); + map.put("M6id8xlarge", "m6id.8xlarge"); + map.put("M6id12xlarge", "m6id.12xlarge"); + map.put("M6id16xlarge", "m6id.16xlarge"); + map.put("M6id24xlarge", "m6id.24xlarge"); + map.put("M6id32xlarge", "m6id.32xlarge"); + map.put("M6idMetal", "m6id.metal"); + map.put("R6idLarge", "r6id.large"); + map.put("R6idXlarge", "r6id.xlarge"); + map.put("R6id2xlarge", "r6id.2xlarge"); + map.put("R6id4xlarge", "r6id.4xlarge"); + map.put("R6id8xlarge", "r6id.8xlarge"); + map.put("R6id12xlarge", "r6id.12xlarge"); + map.put("R6id16xlarge", "r6id.16xlarge"); + map.put("R6id24xlarge", "r6id.24xlarge"); + map.put("R6id32xlarge", "r6id.32xlarge"); + map.put("R6idMetal", "r6id.metal"); + map.put("R6aLarge", "r6a.large"); + map.put("R6aXlarge", "r6a.xlarge"); + map.put("R6a2xlarge", "r6a.2xlarge"); + map.put("R6a4xlarge", "r6a.4xlarge"); + map.put("R6a8xlarge", "r6a.8xlarge"); + map.put("R6a12xlarge", "r6a.12xlarge"); + map.put("R6a16xlarge", "r6a.16xlarge"); + map.put("R6a24xlarge", "r6a.24xlarge"); + map.put("R6a32xlarge", "r6a.32xlarge"); + map.put("R6a48xlarge", "r6a.48xlarge"); + map.put("R6aMetal", "r6a.metal"); + map.put("P4de24xlarge", "p4de.24xlarge"); + map.put("U3tb156xlarge", "u-3tb1.56xlarge"); + map.put("U18tb1112xlarge", "u-18tb1.112xlarge"); + map.put("U24tb1112xlarge", "u-24tb1.112xlarge"); + map.put("Trn12xlarge", "trn1.2xlarge"); + map.put("Trn132xlarge", "trn1.32xlarge"); + map.put("Hpc6id32xlarge", "hpc6id.32xlarge"); + map.put("C6inLarge", "c6in.large"); + map.put("C6inXlarge", "c6in.xlarge"); + map.put("C6in2xlarge", "c6in.2xlarge"); + map.put("C6in4xlarge", "c6in.4xlarge"); + map.put("C6in8xlarge", "c6in.8xlarge"); + map.put("C6in12xlarge", "c6in.12xlarge"); + map.put("C6in16xlarge", "c6in.16xlarge"); + map.put("C6in24xlarge", "c6in.24xlarge"); + map.put("C6in32xlarge", "c6in.32xlarge"); + map.put("M6inLarge", "m6in.large"); + map.put("M6inXlarge", "m6in.xlarge"); + map.put("M6in2xlarge", "m6in.2xlarge"); + map.put("M6in4xlarge", "m6in.4xlarge"); + map.put("M6in8xlarge", "m6in.8xlarge"); + map.put("M6in12xlarge", "m6in.12xlarge"); + map.put("M6in16xlarge", "m6in.16xlarge"); + map.put("M6in24xlarge", "m6in.24xlarge"); + map.put("M6in32xlarge", "m6in.32xlarge"); + map.put("M6idnLarge", "m6idn.large"); + map.put("M6idnXlarge", "m6idn.xlarge"); + map.put("M6idn2xlarge", "m6idn.2xlarge"); + map.put("M6idn4xlarge", "m6idn.4xlarge"); + map.put("M6idn8xlarge", "m6idn.8xlarge"); + map.put("M6idn12xlarge", "m6idn.12xlarge"); + map.put("M6idn16xlarge", "m6idn.16xlarge"); + map.put("M6idn24xlarge", "m6idn.24xlarge"); + map.put("M6idn32xlarge", "m6idn.32xlarge"); + map.put("R6inLarge", "r6in.large"); + map.put("R6inXlarge", "r6in.xlarge"); + map.put("R6in2xlarge", "r6in.2xlarge"); + map.put("R6in4xlarge", "r6in.4xlarge"); + map.put("R6in8xlarge", "r6in.8xlarge"); + map.put("R6in12xlarge", "r6in.12xlarge"); + map.put("R6in16xlarge", "r6in.16xlarge"); + map.put("R6in24xlarge", "r6in.24xlarge"); + map.put("R6in32xlarge", "r6in.32xlarge"); + map.put("R6idnLarge", "r6idn.large"); + map.put("R6idnXlarge", "r6idn.xlarge"); + map.put("R6idn2xlarge", "r6idn.2xlarge"); + map.put("R6idn4xlarge", "r6idn.4xlarge"); + map.put("R6idn8xlarge", "r6idn.8xlarge"); + map.put("R6idn12xlarge", "r6idn.12xlarge"); + map.put("R6idn16xlarge", "r6idn.16xlarge"); + map.put("R6idn24xlarge", "r6idn.24xlarge"); + map.put("R6idn32xlarge", "r6idn.32xlarge"); + map.put("C7gMetal", "c7g.metal"); + map.put("M7gMedium", "m7g.medium"); + map.put("M7gLarge", "m7g.large"); + map.put("M7gXlarge", "m7g.xlarge"); + map.put("M7g2xlarge", "m7g.2xlarge"); + map.put("M7g4xlarge", "m7g.4xlarge"); + map.put("M7g8xlarge", "m7g.8xlarge"); + map.put("M7g12xlarge", "m7g.12xlarge"); + map.put("M7g16xlarge", "m7g.16xlarge"); + map.put("M7gMetal", "m7g.metal"); + map.put("R7gMedium", "r7g.medium"); + map.put("R7gLarge", "r7g.large"); + map.put("R7gXlarge", "r7g.xlarge"); + map.put("R7g2xlarge", "r7g.2xlarge"); + map.put("R7g4xlarge", "r7g.4xlarge"); + map.put("R7g8xlarge", "r7g.8xlarge"); + map.put("R7g12xlarge", "r7g.12xlarge"); + map.put("R7g16xlarge", "r7g.16xlarge"); + map.put("R7gMetal", "r7g.metal"); + map.put("C6inMetal", "c6in.metal"); + map.put("M6inMetal", "m6in.metal"); + map.put("M6idnMetal", "m6idn.metal"); + map.put("R6inMetal", "r6in.metal"); + map.put("R6idnMetal", "r6idn.metal"); + map.put("Inf2Xlarge", "inf2.xlarge"); + map.put("Inf28xlarge", "inf2.8xlarge"); + map.put("Inf224xlarge", "inf2.24xlarge"); + map.put("Inf248xlarge", "inf2.48xlarge"); + map.put("Trn1n32xlarge", "trn1n.32xlarge"); + map.put("I4gLarge", "i4g.large"); + map.put("I4gXlarge", "i4g.xlarge"); + map.put("I4g2xlarge", "i4g.2xlarge"); + map.put("I4g4xlarge", "i4g.4xlarge"); + map.put("I4g8xlarge", "i4g.8xlarge"); + map.put("I4g16xlarge", "i4g.16xlarge"); + map.put("Hpc7g4xlarge", "hpc7g.4xlarge"); + map.put("Hpc7g8xlarge", "hpc7g.8xlarge"); + map.put("Hpc7g16xlarge", "hpc7g.16xlarge"); + map.put("C7gnMedium", "c7gn.medium"); + map.put("C7gnLarge", "c7gn.large"); + map.put("C7gnXlarge", "c7gn.xlarge"); + map.put("C7gn2xlarge", "c7gn.2xlarge"); + map.put("C7gn4xlarge", "c7gn.4xlarge"); + map.put("C7gn8xlarge", "c7gn.8xlarge"); + map.put("C7gn12xlarge", "c7gn.12xlarge"); + map.put("C7gn16xlarge", "c7gn.16xlarge"); + map.put("P548xlarge", "p5.48xlarge"); + map.put("M7iLarge", "m7i.large"); + map.put("M7iXlarge", "m7i.xlarge"); + map.put("M7i2xlarge", "m7i.2xlarge"); + map.put("M7i4xlarge", "m7i.4xlarge"); + map.put("M7i8xlarge", "m7i.8xlarge"); + map.put("M7i12xlarge", "m7i.12xlarge"); + map.put("M7i16xlarge", "m7i.16xlarge"); + map.put("M7i24xlarge", "m7i.24xlarge"); + map.put("M7i48xlarge", "m7i.48xlarge"); + map.put("M7iFlexLarge", "m7i-flex.large"); + map.put("M7iFlexXlarge", "m7i-flex.xlarge"); + map.put("M7iFlex2xlarge", "m7i-flex.2xlarge"); + map.put("M7iFlex4xlarge", "m7i-flex.4xlarge"); + map.put("M7iFlex8xlarge", "m7i-flex.8xlarge"); + map.put("M7aMedium", "m7a.medium"); + map.put("M7aLarge", "m7a.large"); + map.put("M7aXlarge", "m7a.xlarge"); + map.put("M7a2xlarge", "m7a.2xlarge"); + map.put("M7a4xlarge", "m7a.4xlarge"); + map.put("M7a8xlarge", "m7a.8xlarge"); + map.put("M7a12xlarge", "m7a.12xlarge"); + map.put("M7a16xlarge", "m7a.16xlarge"); + map.put("M7a24xlarge", "m7a.24xlarge"); + map.put("M7a32xlarge", "m7a.32xlarge"); + map.put("M7a48xlarge", "m7a.48xlarge"); + map.put("M7aMetal48xl", "m7a.metal-48xl"); + map.put("Hpc7a12xlarge", "hpc7a.12xlarge"); + map.put("Hpc7a24xlarge", "hpc7a.24xlarge"); + map.put("Hpc7a48xlarge", "hpc7a.48xlarge"); + map.put("Hpc7a96xlarge", "hpc7a.96xlarge"); + map.put("C7gdMedium", "c7gd.medium"); + map.put("C7gdLarge", "c7gd.large"); + map.put("C7gdXlarge", "c7gd.xlarge"); + map.put("C7gd2xlarge", "c7gd.2xlarge"); + map.put("C7gd4xlarge", "c7gd.4xlarge"); + map.put("C7gd8xlarge", "c7gd.8xlarge"); + map.put("C7gd12xlarge", "c7gd.12xlarge"); + map.put("C7gd16xlarge", "c7gd.16xlarge"); + map.put("M7gdMedium", "m7gd.medium"); + map.put("M7gdLarge", "m7gd.large"); + map.put("M7gdXlarge", "m7gd.xlarge"); + map.put("M7gd2xlarge", "m7gd.2xlarge"); + map.put("M7gd4xlarge", "m7gd.4xlarge"); + map.put("M7gd8xlarge", "m7gd.8xlarge"); + map.put("M7gd12xlarge", "m7gd.12xlarge"); + map.put("M7gd16xlarge", "m7gd.16xlarge"); + map.put("R7gdMedium", "r7gd.medium"); + map.put("R7gdLarge", "r7gd.large"); + map.put("R7gdXlarge", "r7gd.xlarge"); + map.put("R7gd2xlarge", "r7gd.2xlarge"); + map.put("R7gd4xlarge", "r7gd.4xlarge"); + map.put("R7gd8xlarge", "r7gd.8xlarge"); + map.put("R7gd12xlarge", "r7gd.12xlarge"); + map.put("R7gd16xlarge", "r7gd.16xlarge"); + map.put("R7aMedium", "r7a.medium"); + map.put("R7aLarge", "r7a.large"); + map.put("R7aXlarge", "r7a.xlarge"); + map.put("R7a2xlarge", "r7a.2xlarge"); + map.put("R7a4xlarge", "r7a.4xlarge"); + map.put("R7a8xlarge", "r7a.8xlarge"); + map.put("R7a12xlarge", "r7a.12xlarge"); + map.put("R7a16xlarge", "r7a.16xlarge"); + map.put("R7a24xlarge", "r7a.24xlarge"); + map.put("R7a32xlarge", "r7a.32xlarge"); + map.put("R7a48xlarge", "r7a.48xlarge"); + map.put("C7iLarge", "c7i.large"); + map.put("C7iXlarge", "c7i.xlarge"); + map.put("C7i2xlarge", "c7i.2xlarge"); + map.put("C7i4xlarge", "c7i.4xlarge"); + map.put("C7i8xlarge", "c7i.8xlarge"); + map.put("C7i12xlarge", "c7i.12xlarge"); + map.put("C7i16xlarge", "c7i.16xlarge"); + map.put("C7i24xlarge", "c7i.24xlarge"); + map.put("C7i48xlarge", "c7i.48xlarge"); + map.put("Mac2M2proMetal", "mac2-m2pro.metal"); + map.put("R7izLarge", "r7iz.large"); + map.put("R7izXlarge", "r7iz.xlarge"); + map.put("R7iz2xlarge", "r7iz.2xlarge"); + map.put("R7iz4xlarge", "r7iz.4xlarge"); + map.put("R7iz8xlarge", "r7iz.8xlarge"); + map.put("R7iz12xlarge", "r7iz.12xlarge"); + map.put("R7iz16xlarge", "r7iz.16xlarge"); + map.put("R7iz32xlarge", "r7iz.32xlarge"); + map.put("C7aMedium", "c7a.medium"); + map.put("C7aLarge", "c7a.large"); + map.put("C7aXlarge", "c7a.xlarge"); + map.put("C7a2xlarge", "c7a.2xlarge"); + map.put("C7a4xlarge", "c7a.4xlarge"); + map.put("C7a8xlarge", "c7a.8xlarge"); + map.put("C7a12xlarge", "c7a.12xlarge"); + map.put("C7a16xlarge", "c7a.16xlarge"); + map.put("C7a24xlarge", "c7a.24xlarge"); + map.put("C7a32xlarge", "c7a.32xlarge"); + map.put("C7a48xlarge", "c7a.48xlarge"); + map.put("C7aMetal48xl", "c7a.metal-48xl"); + map.put("R7aMetal48xl", "r7a.metal-48xl"); + map.put("R7iLarge", "r7i.large"); + map.put("R7iXlarge", "r7i.xlarge"); + map.put("R7i2xlarge", "r7i.2xlarge"); + map.put("R7i4xlarge", "r7i.4xlarge"); + map.put("R7i8xlarge", "r7i.8xlarge"); + map.put("R7i12xlarge", "r7i.12xlarge"); + map.put("R7i16xlarge", "r7i.16xlarge"); + map.put("R7i24xlarge", "r7i.24xlarge"); + map.put("R7i48xlarge", "r7i.48xlarge"); + map.put("Dl2q24xlarge", "dl2q.24xlarge"); + map.put("Mac2M2Metal", "mac2-m2.metal"); + map.put("I4i12xlarge", "i4i.12xlarge"); + map.put("I4i24xlarge", "i4i.24xlarge"); + map.put("C7iMetal24xl", "c7i.metal-24xl"); + map.put("C7iMetal48xl", "c7i.metal-48xl"); + map.put("M7iMetal24xl", "m7i.metal-24xl"); + map.put("M7iMetal48xl", "m7i.metal-48xl"); + map.put("R7iMetal24xl", "r7i.metal-24xl"); + map.put("R7iMetal48xl", "r7i.metal-48xl"); + map.put("R7izMetal16xl", "r7iz.metal-16xl"); + map.put("R7izMetal32xl", "r7iz.metal-32xl"); + map.put("C7gdMetal", "c7gd.metal"); + map.put("M7gdMetal", "m7gd.metal"); + map.put("R7gdMetal", "r7gd.metal"); + map.put("G6Xlarge", "g6.xlarge"); + map.put("G62xlarge", "g6.2xlarge"); + map.put("G64xlarge", "g6.4xlarge"); + map.put("G68xlarge", "g6.8xlarge"); + map.put("G612xlarge", "g6.12xlarge"); + map.put("G616xlarge", "g6.16xlarge"); + map.put("G624xlarge", "g6.24xlarge"); + map.put("G648xlarge", "g6.48xlarge"); + map.put("Gr64xlarge", "gr6.4xlarge"); + map.put("Gr68xlarge", "gr6.8xlarge"); + map.put("C7iFlexLarge", "c7i-flex.large"); + map.put("C7iFlexXlarge", "c7i-flex.xlarge"); + map.put("C7iFlex2xlarge", "c7i-flex.2xlarge"); + map.put("C7iFlex4xlarge", "c7i-flex.4xlarge"); + map.put("C7iFlex8xlarge", "c7i-flex.8xlarge"); + map.put("U7i12tb224xlarge", "u7i-12tb.224xlarge"); + map.put("U7in16tb224xlarge", "u7in-16tb.224xlarge"); + map.put("U7in24tb224xlarge", "u7in-24tb.224xlarge"); + map.put("U7in32tb224xlarge", "u7in-32tb.224xlarge"); + map.put("U7ib12tb224xlarge", "u7ib-12tb.224xlarge"); + map.put("C7gnMetal", "c7gn.metal"); + map.put("R8gMedium", "r8g.medium"); + map.put("R8gLarge", "r8g.large"); + map.put("R8gXlarge", "r8g.xlarge"); + map.put("R8g2xlarge", "r8g.2xlarge"); + map.put("R8g4xlarge", "r8g.4xlarge"); + map.put("R8g8xlarge", "r8g.8xlarge"); + map.put("R8g12xlarge", "r8g.12xlarge"); + map.put("R8g16xlarge", "r8g.16xlarge"); + map.put("R8g24xlarge", "r8g.24xlarge"); + map.put("R8g48xlarge", "r8g.48xlarge"); + map.put("R8gMetal24xl", "r8g.metal-24xl"); + map.put("R8gMetal48xl", "r8g.metal-48xl"); + map.put("M8gMedium", "m8g.medium"); + map.put("M8gLarge", "m8g.large"); + map.put("M8gXlarge", "m8g.xlarge"); + map.put("M8g2xlarge", "m8g.2xlarge"); + map.put("M8g4xlarge", "m8g.4xlarge"); + map.put("M8g8xlarge", "m8g.8xlarge"); + map.put("M8g12xlarge", "m8g.12xlarge"); + map.put("M8g16xlarge", "m8g.16xlarge"); + map.put("M8g24xlarge", "m8g.24xlarge"); + map.put("M8g48xlarge", "m8g.48xlarge"); + map.put("M8gMetal24xl", "m8g.metal-24xl"); + map.put("M8gMetal48xl", "m8g.metal-48xl"); + map.put("M8gdMedium", "m8gd.medium"); + map.put("M8gdLarge", "m8gd.large"); + map.put("M8gdXlarge", "m8gd.xlarge"); + map.put("M8gd2xlarge", "m8gd.2xlarge"); + map.put("M8gd4xlarge", "m8gd.4xlarge"); + map.put("M8gd8xlarge", "m8gd.8xlarge"); + map.put("M8gd12xlarge", "m8gd.12xlarge"); + map.put("M8gd16xlarge", "m8gd.16xlarge"); + map.put("M8gd24xlarge", "m8gd.24xlarge"); + map.put("M8gd48xlarge", "m8gd.48xlarge"); + map.put("M8gdMetal24xl", "m8gd.metal-24xl"); + map.put("M8gdMetal48xl", "m8gd.metal-48xl"); + map.put("C8gMedium", "c8g.medium"); + map.put("C8gLarge", "c8g.large"); + map.put("C8gXlarge", "c8g.xlarge"); + map.put("C8g2xlarge", "c8g.2xlarge"); + map.put("C8g4xlarge", "c8g.4xlarge"); + map.put("C8g8xlarge", "c8g.8xlarge"); + map.put("C8g12xlarge", "c8g.12xlarge"); + map.put("C8g16xlarge", "c8g.16xlarge"); + map.put("C8g24xlarge", "c8g.24xlarge"); + map.put("C8g48xlarge", "c8g.48xlarge"); + map.put("C8gMetal24xl", "c8g.metal-24xl"); + map.put("C8gMetal48xl", "c8g.metal-48xl"); + map.put("C8gdMedium", "c8gd.medium"); + map.put("C8gdLarge", "c8gd.large"); + map.put("C8gdXlarge", "c8gd.xlarge"); + map.put("C8gd2xlarge", "c8gd.2xlarge"); + map.put("C8gd4xlarge", "c8gd.4xlarge"); + map.put("C8gd8xlarge", "c8gd.8xlarge"); + map.put("C8gd12xlarge", "c8gd.12xlarge"); + map.put("C8gd16xlarge", "c8gd.16xlarge"); + map.put("C8gd24xlarge", "c8gd.24xlarge"); + map.put("C8gd48xlarge", "c8gd.48xlarge"); + map.put("C8gdMetal24xl", "c8gd.metal-24xl"); + map.put("C8gdMetal48xl", "c8gd.metal-48xl"); + map.put("C8gnMedium", "c8gn.medium"); + map.put("C8gnLarge", "c8gn.large"); + map.put("C8gnXlarge", "c8gn.xlarge"); + map.put("C8gn2xlarge", "c8gn.2xlarge"); + map.put("C8gn4xlarge", "c8gn.4xlarge"); + map.put("C8gn8xlarge", "c8gn.8xlarge"); + map.put("C8gn12xlarge", "c8gn.12xlarge"); + map.put("C8gn16xlarge", "c8gn.16xlarge"); + map.put("C8gn24xlarge", "c8gn.24xlarge"); + map.put("C8gn48xlarge", "c8gn.48xlarge"); + map.put("C8gnMetal24xl", "c8gn.metal-24xl"); + map.put("C8gnMetal48xl", "c8gn.metal-48xl"); + map.put("Mac2M1ultraMetal", "mac2-m1ultra.metal"); + AWS_SDK_JAVA_V1 = Collections.unmodifiableMap(map); + } + + public static InstanceType of(String instanceType) { + /* + * Attempt to find correct AWS SDK for Java v2 instance type from instance type string. Accept the value of the + * enum, e.g. m1.large. + */ + InstanceType foundInstanceType = InstanceType.fromValue(instanceType); + if (foundInstanceType == InstanceType.UNKNOWN_TO_SDK_VERSION) { + /* + * Also accept the name of the Enum in the AWS SDK for Java v1, e.g. M1Large. Name of the enum in AWS SDK + * for Java v2 is different, e.g. M1_LARGE. + */ + String oldInstanceType = AWS_SDK_JAVA_V1.get(instanceType); + if (oldInstanceType != null) { + foundInstanceType = InstanceType.fromValue(oldInstanceType); + } + } + return foundInstanceType; + } +} diff --git a/src/main/java/hudson/plugins/ec2/util/KeyHelper.java b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java new file mode 100644 index 000000000..300492012 --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/util/KeyHelper.java @@ -0,0 +1,188 @@ +package hudson.plugins.ec2.util; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import java.io.StringReader; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.DSAParams; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Locale; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.digest.BuiltinDigests; +import org.apache.sshd.common.util.buffer.BufferUtils; +import org.bouncycastle.asn1.ASN1BitString; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.jcajce.interfaces.EdDSAPrivateKey; +import org.bouncycastle.openssl.PEMEncryptedKeyPair; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; +import org.bouncycastle.util.Properties; + +/** + * Utility class to parse PEM. + */ +public abstract class KeyHelper { + + private static final Logger LOGGER = Logger.getLogger(KeyHelper.class.getName()); + + private KeyHelper() {} + + /** + * Decodes a PEM-encoded key pair into a {@link KeyPair} object. This method supports + * various types of PEM input such as encrypted private keys, public keys, and key pairs. + * + * @param pem The PEM-formatted string containing the key data. + * @param password The password used to decrypt encrypted key pairs, if applicable. Can be null if no password is required. + * @return A {@link KeyPair} containing the public and private keys. If a public key is provided without a matching private key, + * the private key in the returned {@link KeyPair} will be null. + * @throws IOException If an error occurs during parsing or decryption of the PEM input. + * @throws IllegalArgumentException If the provided PEM input cannot be parsed or is of an unsupported type. + */ + public static KeyPair decodeKeyPair(@NonNull String pem, @NonNull String password) throws IOException { + try (org.bouncycastle.openssl.PEMParser pemParser = + new org.bouncycastle.openssl.PEMParser(new StringReader(pem))) { + Object object = pemParser.readObject(); + if (object == null) { + throw new IllegalArgumentException("Failed to parse PEM input"); + } + JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); + + if (object instanceof PEMEncryptedKeyPair) { + PEMKeyPair decryptedKeyPair = ((PEMEncryptedKeyPair) object) + .decryptKeyPair(new JcePEMDecryptorProviderBuilder().build(password.toCharArray())); + PrivateKey privateKey = converter.getPrivateKey(decryptedKeyPair.getPrivateKeyInfo()); + FIPS140Utils.ensureKeyInFipsMode(privateKey); + PublicKey publicKey = converter.getPublicKey(decryptedKeyPair.getPublicKeyInfo()); + return new KeyPair(publicKey, privateKey); + } else if (object instanceof PrivateKeyInfo privateKeyInfo) { + PrivateKey privateKey = converter.getPrivateKey(privateKeyInfo); + FIPS140Utils.ensureKeyInFipsMode(privateKey); + PublicKey publicKey = generatePublicKeyFromPrivateKey(privateKeyInfo, privateKey); + return new KeyPair(publicKey, privateKey); + } else if (object instanceof SubjectPublicKeyInfo) { + PublicKey publicKey = converter.getPublicKey((SubjectPublicKeyInfo) object); + FIPS140Utils.ensureKeyInFipsMode(publicKey); + return new KeyPair(publicKey, null); + } else if (object instanceof PEMKeyPair) { + SubjectPublicKeyInfo publicKeyInfo = ((PEMKeyPair) object).getPublicKeyInfo(); + PrivateKeyInfo privateKeyInfo = ((PEMKeyPair) object).getPrivateKeyInfo(); + PrivateKey privateKey = converter.getPrivateKey(privateKeyInfo); + FIPS140Utils.ensureKeyInFipsMode(privateKey); + PublicKey publicKey = converter.getPublicKey(publicKeyInfo); + return new KeyPair(publicKey, privateKey); + } else { + throw new IllegalArgumentException( + "Unsupported PEM object type: " + object.getClass().getName()); + } + } catch (Exception e) { + throw new IOException("Failed to parse PEM input", e); + } + } + + /* visible for testing */ + /** + * Extract a {@link PublicKey} from the given {@link PrivateKey} + * @param privateKey the private key to extract from + * @return the corresponding public key or null if the extraction is not possible + */ + static PublicKey generatePublicKeyFromPrivateKey(PrivateKeyInfo privateKeyInfo, @NonNull PrivateKey privateKey) { + try { + if (privateKey instanceof RSAPrivateCrtKey) + return KeyFactory.getInstance("RSA") + .generatePublic(new RSAPublicKeySpec( + ((RSAPrivateCrtKey) privateKey).getModulus(), + ((RSAPrivateCrtKey) privateKey).getPublicExponent())); + else if (privateKey instanceof DSAPrivateKey) { + DSAParams dsaParams = ((DSAPrivateKey) privateKey).getParams(); + return KeyFactory.getInstance("DSA") + .generatePublic(new DSAPublicKeySpec( + dsaParams.getG().modPow(((DSAPrivateKey) privateKey).getX(), dsaParams.getP()), + dsaParams.getP(), + dsaParams.getQ(), + dsaParams.getG())); + } else if (privateKey instanceof ECPrivateKey) { + ASN1BitString asn1BitString = org.bouncycastle.asn1.sec.ECPrivateKey.getInstance( + privateKeyInfo.getPrivateKey().getOctets()) + .getPublicKey(); + return KeyFactory.getInstance("EC") + .generatePublic(new X509EncodedKeySpec( + new SubjectPublicKeyInfo(privateKeyInfo.getPrivateKeyAlgorithm(), asn1BitString) + .getEncoded())); + } else if (privateKey instanceof EdDSAPrivateKey) { + return ((EdDSAPrivateKey) privateKey).getPublicKey(); + } else { + return null; + } + } catch (NoSuchAlgorithmException | IOException | InvalidKeySpecException e) { + return null; + } + } + + /** + * Determines the SSH algorithm identifier corresponding to the given server public key. + * This method matches the key type to the appropriate SSH algorithm string. + * When an {@link ECPublicKey} is given, an NIST curse will be assumed. + * + * @param serverKey The server's {@link PublicKey} object for which the SSH algorithm identifier + * needs to be determined. + * @return A {@code String} representing the SSH algorithm identifier for the given server key, + * or {@code null} if the key type is unsupported or cannot be determined. + */ + public static String getSshAlgorithm(@NonNull PublicKey serverKey) { + // Emulate Oracle so that the algorithm returned by + // org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey.getAlgorithm + // is the one expected by org.apache.sshd.common.config.keys.KeyUtils + try { + Properties.setThreadOverride(Properties.EMULATE_ORACLE, true); + String sshAlgorithm = KeyUtils.getKeyType(serverKey); + // java.security takes precedence over thread local configuration. + // Check the algorithm name used by BC when EMULATE_ORACLE is not set. + if (sshAlgorithm == null && "Ed25519".equals(serverKey.getAlgorithm())) { + sshAlgorithm = "ssh-ed25519"; + } + return sshAlgorithm; + } finally { + Properties.removeThreadOverride(Properties.EMULATE_ORACLE); + } + } + + /** + * Computes the MD5 fingerprint of the given server public key. + * The fingerprint is formatted as a colon-separated hexadecimal string. + * + * @param serverKey The server's {@link PublicKey} object for which the fingerprint needs to be computed. + * @return A {@code String} representing the MD5 fingerprint of the given server key in + * colon-separated hexadecimal format, or an empty string if an error occurs during computation. + */ + public static String getFingerprint(@NonNull PublicKey serverKey) { + // Emulate Oracle so that the algorithm returned by + // org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey.getAlgorithm + // is the one expected by org.apache.sshd.common.config.keys.KeyUtils + try { + Properties.setThreadOverride(Properties.EMULATE_ORACLE, true); + // Generate MD5 fingerprint just like ssh-keygen + byte[] rawFingerprint = KeyUtils.getRawFingerprint(BuiltinDigests.md5.get(), serverKey); + return BufferUtils.toHex(':', rawFingerprint).toLowerCase(Locale.ROOT); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error computing fingerprint", e); + return ""; + } finally { + Properties.removeThreadOverride(Properties.EMULATE_ORACLE); + } + } +} diff --git a/src/main/java/hudson/plugins/ec2/util/KeyPair.java b/src/main/java/hudson/plugins/ec2/util/KeyPair.java new file mode 100644 index 000000000..964fe1cba --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/util/KeyPair.java @@ -0,0 +1,22 @@ +package hudson.plugins.ec2.util; + +import java.util.Objects; +import software.amazon.awssdk.services.ec2.model.KeyPairInfo; + +public class KeyPair { + private final KeyPairInfo keyPairInfo; + private final String material; + + public KeyPair(KeyPairInfo keyPairInfo, String material) { + this.keyPairInfo = Objects.requireNonNull(keyPairInfo); + this.material = Objects.requireNonNull(material); + } + + public KeyPairInfo getKeyPairInfo() { + return keyPairInfo; + } + + public String getMaterial() { + return material; + } +} diff --git a/src/main/java/hudson/plugins/ec2/util/MinimumInstanceChecker.java b/src/main/java/hudson/plugins/ec2/util/MinimumInstanceChecker.java index b573a1dfe..d00bde014 100644 --- a/src/main/java/hudson/plugins/ec2/util/MinimumInstanceChecker.java +++ b/src/main/java/hudson/plugins/ec2/util/MinimumInstanceChecker.java @@ -1,35 +1,65 @@ package hudson.plugins.ec2.util; +import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import hudson.plugins.ec2.EC2Cloud; -import hudson.plugins.ec2.EC2Computer; -import hudson.plugins.ec2.SlaveTemplate; +import hudson.init.Terminator; import hudson.model.Computer; import hudson.model.Label; import hudson.model.Queue; -import jenkins.model.Jenkins; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; - -import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.plugins.ec2.EC2AbstractSlave; +import hudson.plugins.ec2.EC2Cloud; +import hudson.plugins.ec2.EC2Computer; +import hudson.plugins.ec2.SlaveTemplate; import java.time.Clock; import java.time.LocalDateTime; import java.time.LocalTime; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.stream.Stream; +import jenkins.model.Jenkins; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; @Restricted(NoExternalUse.class) public class MinimumInstanceChecker { + private static final Logger LOGGER = Logger.getLogger(MinimumInstanceChecker.class.getName()); + + /** + * Executor for deferred minimum-instance checks. Heavy provisioning (EC2 API, cloud.provision) + * runs here so callers (taskAccepted, EC2SlaveMonitor) return immediately. + */ + private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "MinimumInstanceChecker"); + t.setDaemon(true); + return t; + }); + + /** + * Schedules a minimum-instance check to run asynchronously. Use this instead of + * {@link #checkForMinimumInstances()} when the caller must return immediately (e.g. taskAccepted). + */ + public static void scheduleCheck() { + EXECUTOR.execute(MinimumInstanceChecker::checkForMinimumInstances); + } + @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Needs to be overridden from tests") public static Clock clock = Clock.systemDefaultZone(); - private static Stream agentsForTemplate(@NonNull SlaveTemplate agentTemplate) { - return (Stream) Arrays.stream(Jenkins.get().getComputers()) - .filter(computer -> computer instanceof EC2Computer) + private static Stream agentsForTemplate(@NonNull SlaveTemplate agentTemplate) { + return Arrays.stream(Jenkins.get().getComputers()) + .filter(EC2Computer.class::isInstance) + .map(EC2Computer.class::cast) .filter(computer -> { - SlaveTemplate computerTemplate = ((EC2Computer) computer).getSlaveTemplate(); + SlaveTemplate computerTemplate = computer.getSlaveTemplate(); return computerTemplate != null && Objects.equals(computerTemplate.description, agentTemplate.description); }); @@ -39,52 +69,70 @@ public static int countCurrentNumberOfAgents(@NonNull SlaveTemplate agentTemplat return (int) agentsForTemplate(agentTemplate).count(); } + private static Stream idleAgents(@NonNull SlaveTemplate agentTemplate) { + return agentsForTemplate(agentTemplate).filter(Computer::isIdle); + } + public static int countCurrentNumberOfSpareAgents(@NonNull SlaveTemplate agentTemplate) { - return (int) agentsForTemplate(agentTemplate) - .filter(computer -> computer.countBusy() == 0) - .filter(computer -> computer.isOnline()) - .count(); + return (int) idleAgents(agentTemplate).filter(Computer::isOnline).count(); } public static int countCurrentNumberOfProvisioningAgents(@NonNull SlaveTemplate agentTemplate) { - return (int) agentsForTemplate(agentTemplate) - .filter(computer -> computer.countBusy() == 0) - .filter(computer -> computer.isOffline()) - .filter(computer -> computer.isConnecting()) - .count(); + return (int) idleAgents(agentTemplate) + .filter(Computer::isOffline) + .filter(Computer::isConnecting) + .count(); } /* Get the number of queued builds that match an AMI (agentTemplate) */ public static int countQueueItemsForAgentTemplate(@NonNull SlaveTemplate agentTemplate) { - return (int) - Queue - .getInstance() - .getBuildableItems() - .stream() - .map((Queue.Item item) -> item.getAssignedLabel()) - .filter(Objects::nonNull) - .filter((Label label) -> label.matches(agentTemplate.getLabelSet())) - .count(); + return (int) Queue.getInstance().getBuildableItems().stream() + .map((Queue.Item item) -> item.getAssignedLabel()) + .filter(Objects::nonNull) + .filter((Label label) -> label.matches(agentTemplate.getLabelSet())) + .count(); } - public static void checkForMinimumInstances() { - Jenkins.get().clouds.stream() - .filter(cloud -> cloud instanceof EC2Cloud) - .map(cloud -> (EC2Cloud) cloud) - .forEach(cloud -> { - cloud.getTemplates().forEach(agentTemplate -> { + /** + * Checks all EC2 cloud templates and provisions agents to meet minimum instance requirements. + * Synchronized to prevent concurrent provisioning decisions that could lead to over-provisioning + * when multiple agents accept tasks simultaneously. + * + * @see JENKINS-76171 + */ + public static synchronized void checkForMinimumInstances() { + Jenkins jenkins = Jenkins.get(); + + // Early exit if no templates have minimum instance requirements + boolean hasMinimumRequirements = jenkins.clouds.stream() + .filter(EC2Cloud.class::isInstance) + .map(EC2Cloud.class::cast) + .flatMap(cloud -> cloud.getTemplates().stream()) + .anyMatch(template -> + template.getMinimumNumberOfInstances() > 0 || template.getMinimumNumberOfSpareInstances() > 0); + + if (!hasMinimumRequirements) { + // No templates require minimum instances - exit immediately + return; + } + + jenkins.clouds.stream() + .filter(EC2Cloud.class::isInstance) + .map(EC2Cloud.class::cast) + .forEach(cloud -> cloud.getTemplates().forEach(agentTemplate -> { // Minimum instances now have a time range, check to see // if we are within that time range and return early if not. - if (! minimumInstancesActive(agentTemplate.getMinimumNumberOfInstancesTimeRangeConfig())) { + if (!minimumInstancesActive(agentTemplate.getMinimumNumberOfInstancesTimeRangeConfig())) { return; } int requiredMinAgents = agentTemplate.getMinimumNumberOfInstances(); int requiredMinSpareAgents = agentTemplate.getMinimumNumberOfSpareInstances(); int currentNumberOfAgentsForTemplate = countCurrentNumberOfAgents(agentTemplate); int currentNumberOfSpareAgentsForTemplate = countCurrentNumberOfSpareAgents(agentTemplate); - int currentNumberOfProvisioningAgentsForTemplate = countCurrentNumberOfProvisioningAgents(agentTemplate); + int currentNumberOfProvisioningAgentsForTemplate = + countCurrentNumberOfProvisioningAgents(agentTemplate); int currentBuildsWaitingForTemplate = countQueueItemsForAgentTemplate(agentTemplate); int provisionForMinAgents = 0; int provisionForMinSpareAgents = 0; @@ -92,33 +140,40 @@ public static void checkForMinimumInstances() { // Check if we need to provision any agents because we // don't have the minimum number of agents provisionForMinAgents = requiredMinAgents - currentNumberOfAgentsForTemplate; - if (provisionForMinAgents < 0){ + if (provisionForMinAgents < 0) { provisionForMinAgents = 0; } // Check if we need to provision any agents because we // don't have the minimum number of spare agents. // Don't double provision if minAgents and minSpareAgents are set. - provisionForMinSpareAgents = (requiredMinSpareAgents + currentBuildsWaitingForTemplate) - - ( - currentNumberOfSpareAgentsForTemplate + - provisionForMinAgents + - currentNumberOfProvisioningAgentsForTemplate - ); - if (provisionForMinSpareAgents < 0){ - provisionForMinSpareAgents = 0; + if (requiredMinSpareAgents > 0) { + provisionForMinSpareAgents = (requiredMinSpareAgents + currentBuildsWaitingForTemplate) + - (currentNumberOfSpareAgentsForTemplate + + provisionForMinAgents + + currentNumberOfProvisioningAgentsForTemplate); + if (provisionForMinSpareAgents < 0) { + provisionForMinSpareAgents = 0; + } } int numberToProvision = provisionForMinAgents + provisionForMinSpareAgents; + + if (numberToProvision > 0 || requiredMinAgents > 0 || requiredMinSpareAgents > 0) { + LOGGER.log( + Level.FINE, + "MinimumInstanceChecker for template {0}: toProvision={1}", + new Object[] {agentTemplate.description, numberToProvision}); + } + if (numberToProvision > 0) { cloud.provision(agentTemplate, numberToProvision); } - }); - }); + })); } public static boolean minimumInstancesActive( - MinimumNumberOfInstancesTimeRangeConfig minimumNumberOfInstancesTimeRangeConfig) { + MinimumNumberOfInstancesTimeRangeConfig minimumNumberOfInstancesTimeRangeConfig) { if (minimumNumberOfInstancesTimeRangeConfig == null) { return true; } @@ -126,7 +181,7 @@ public static boolean minimumInstancesActive( LocalTime toTime = minimumNumberOfInstancesTimeRangeConfig.getMinimumNoInstancesActiveTimeRangeToAsTime(); LocalDateTime now = LocalDateTime.now(clock); - LocalTime nowTime = LocalTime.from(now); //No date. Easier for comparison on time only. + LocalTime nowTime = LocalTime.from(now); // No date. Easier for comparison on time only. boolean passingMidnight = false; if (toTime.isBefore(fromTime)) { @@ -138,7 +193,7 @@ public static boolean minimumInstancesActive( String today = now.getDayOfWeek().name().toLowerCase(); return minimumNumberOfInstancesTimeRangeConfig.getDay(today); } else if (nowTime.isBefore(toTime)) { - //We've gone past midnight and want to check yesterday's setting. + // We've gone past midnight and want to check yesterday's setting. String yesterday = now.minusDays(1).getDayOfWeek().name().toLowerCase(); return minimumNumberOfInstancesTimeRangeConfig.getDay(yesterday); } @@ -150,4 +205,26 @@ public static boolean minimumInstancesActive( } return false; } + + @Terminator + public static void discardIdleInstances() throws Exception { + LOGGER.fine("Looking for idle instances to discard"); + List> futures = new ArrayList<>(); + Jenkins.get().clouds.stream() + .filter(EC2Cloud.class::isInstance) + .map(EC2Cloud.class::cast) + .forEach(cloud -> cloud.getTemplates().stream() + .filter(SlaveTemplate::getTerminateIdleDuringShutdown) + .forEach(agentTemplate -> idleAgents(agentTemplate).forEach(computer -> { + EC2AbstractSlave agent = computer.getNode(); + if (agent != null) { + LOGGER.info(() -> "discarding idle instance " + agent.getInstanceId()); + futures.add(agent.terminate()); + } + }))); + // Must wait; otherwise task could run too late during shutdown, leading to NoClassDefFoundError. + for (Future future : futures) { + future.get(5, TimeUnit.SECONDS); + } + } } diff --git a/src/main/java/hudson/plugins/ec2/util/MinimumNumberOfInstancesTimeRangeConfig.java b/src/main/java/hudson/plugins/ec2/util/MinimumNumberOfInstancesTimeRangeConfig.java index 5562245b7..9a4a58a66 100644 --- a/src/main/java/hudson/plugins/ec2/util/MinimumNumberOfInstancesTimeRangeConfig.java +++ b/src/main/java/hudson/plugins/ec2/util/MinimumNumberOfInstancesTimeRangeConfig.java @@ -1,15 +1,12 @@ package hudson.plugins.ec2.util; -import net.sf.json.JSONObject; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.DataBoundSetter; - import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; -import java.util.HashMap; import java.util.Locale; import java.util.Map; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; public class MinimumNumberOfInstancesTimeRangeConfig { @@ -28,10 +25,8 @@ public class MinimumNumberOfInstancesTimeRangeConfig { private Boolean saturday; private Boolean sunday; - @DataBoundConstructor - public MinimumNumberOfInstancesTimeRangeConfig() { - } + public MinimumNumberOfInstancesTimeRangeConfig() {} protected Object readResolve() { if (minimumNoInstancesActiveTimeRangeDays != null && !minimumNoInstancesActiveTimeRangeDays.isEmpty()) { @@ -157,14 +152,22 @@ public void setSunday(Boolean sunday) { public boolean getDay(String day) { switch (day.toLowerCase()) { - case "monday": return Boolean.TRUE.equals(this.monday); - case "tuesday": return Boolean.TRUE.equals(this.tuesday); - case "wednesday": return Boolean.TRUE.equals(this.wednesday); - case "thursday": return Boolean.TRUE.equals(this.thursday); - case "friday": return Boolean.TRUE.equals(this.friday); - case "saturday": return Boolean.TRUE.equals(this.saturday); - case "sunday": return Boolean.TRUE.equals(this.sunday); - default: throw new IllegalArgumentException("Can only get days"); + case "monday": + return Boolean.TRUE.equals(this.monday); + case "tuesday": + return Boolean.TRUE.equals(this.tuesday); + case "wednesday": + return Boolean.TRUE.equals(this.wednesday); + case "thursday": + return Boolean.TRUE.equals(this.thursday); + case "friday": + return Boolean.TRUE.equals(this.friday); + case "saturday": + return Boolean.TRUE.equals(this.saturday); + case "sunday": + return Boolean.TRUE.equals(this.sunday); + default: + throw new IllegalArgumentException("Can only get days"); } } } diff --git a/src/main/java/hudson/plugins/ec2/util/ResettableCountDownLatch.java b/src/main/java/hudson/plugins/ec2/util/ResettableCountDownLatch.java index e39c3adb6..aa97c36b4 100644 --- a/src/main/java/hudson/plugins/ec2/util/ResettableCountDownLatch.java +++ b/src/main/java/hudson/plugins/ec2/util/ResettableCountDownLatch.java @@ -3,9 +3,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; - -import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; @Restricted(NoExternalUse.class) public class ResettableCountDownLatch { @@ -44,4 +43,4 @@ public boolean await(long timeout, TimeUnit unit) throws InterruptedException { public long getCount() { return latchHolder.get().getCount(); } -} \ No newline at end of file +} diff --git a/src/main/java/hudson/plugins/ec2/util/SSHClientHelper.java b/src/main/java/hudson/plugins/ec2/util/SSHClientHelper.java new file mode 100644 index 000000000..0682439ab --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/util/SSHClientHelper.java @@ -0,0 +1,93 @@ +package hudson.plugins.ec2.util; + +import hudson.plugins.ec2.EC2Computer; +import hudson.plugins.ec2.ssh.verifiers.HostKey; +import hudson.plugins.ec2.ssh.verifiers.HostKeyHelper; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.signature.BuiltinSignatures; +import org.apache.sshd.common.signature.Signature; + +public final class SSHClientHelper { + + private static final SSHClientHelper INSTANCE = new SSHClientHelper(); + + private SSHClientHelper() {} + + public static SSHClientHelper getInstance() { + return INSTANCE; + } + + /** + * Set up an SSH client configured for the given {@link EC2Computer}. + * + * @param computer the {@link EC2Computer} the created client will connect to + * @return an SSH client configured for this {@link EC2Computer} + */ + public SshClient setupSshClient(EC2Computer computer) { + SshClient client = SshClient.setUpDefaultClient(); + + List preferred = getPreferredSignatures(computer); + if (!preferred.isEmpty()) { + LinkedHashSet> signatureFactoriesSet = new LinkedHashSet<>(preferred); + signatureFactoriesSet.addAll(client.getSignatureFactories()); + client.setSignatureFactories(new ArrayList<>(signatureFactoriesSet)); + } + + return client; + } + + /** + * Return an ordered list of signature algorithms that should be used. Noticeably, if a {@link HostKey} already exists for this + * {@link EC2Computer}, the {@link HostKey} algorithm will be attempted first. + * + * @param computer return a list of signature for this computer. + * @return an ordered list of signature algorithms that should be used. + */ + public List getPreferredSignatures(EC2Computer computer) { + String trustedAlgorithm; + try { + HostKey trustedHostKey = HostKeyHelper.getInstance().getHostKey(computer); + if (trustedHostKey == null) { + return List.of(); + } + trustedAlgorithm = trustedHostKey.getAlgorithm(); + } catch (IOException e) { + return List.of(); + } + + List preferred; + switch (trustedAlgorithm) { + case "ssh-rsa": + preferred = List.of( + BuiltinSignatures.rsa, + BuiltinSignatures.rsaSHA256, + BuiltinSignatures.rsaSHA256_cert, + BuiltinSignatures.rsaSHA512, + BuiltinSignatures.rsaSHA512_cert); + break; + case "ecdsa-sha2-nistp256": + preferred = List.of(BuiltinSignatures.nistp256, BuiltinSignatures.nistp256_cert); + break; + case "ecdsa-sha2-nistp384": + preferred = List.of(BuiltinSignatures.nistp384, BuiltinSignatures.nistp384_cert); + break; + case "ecdsa-sha2-nistp521": + preferred = List.of(BuiltinSignatures.nistp521, BuiltinSignatures.nistp521_cert); + break; + case "ssh-ed25519": + preferred = List.of( + BuiltinSignatures.ed25519, BuiltinSignatures.ed25519_cert, BuiltinSignatures.sk_ssh_ed25519); + break; + default: + return List.of(); + } + + // Keep only supported algorithms + return NamedFactory.setUpBuiltinFactories(true, preferred); + } +} diff --git a/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java b/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java index 3795be6fd..2d9dd6f7c 100644 --- a/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java +++ b/src/main/java/hudson/plugins/ec2/win/EC2WindowsLauncher.java @@ -1,34 +1,36 @@ package hudson.plugins.ec2.win; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Util; import hudson.model.Descriptor; import hudson.model.TaskListener; -import hudson.plugins.ec2.*; +import hudson.os.WindowsUtil; +import hudson.plugins.ec2.EC2AbstractSlave; +import hudson.plugins.ec2.EC2Computer; +import hudson.plugins.ec2.EC2ComputerLauncher; +import hudson.plugins.ec2.EC2HostAddressProvider; +import hudson.plugins.ec2.EC2PrivateKey; +import hudson.plugins.ec2.SlaveTemplate; import hudson.plugins.ec2.win.winrm.WindowsProcess; import hudson.remoting.Channel; import hudson.remoting.Channel.Listener; import hudson.slaves.ComputerLauncher; -import hudson.Util; -import hudson.os.WindowsUtil; - +import hudson.slaves.OfflineCause; import java.io.EOFException; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.concurrent.TimeUnit; - -import hudson.slaves.OfflineCause; -import edu.umd.cs.findbugs.annotations.NonNull; - +import javax.net.ssl.SSLException; import jenkins.model.Jenkins; import org.apache.commons.io.IOUtils; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.services.ec2.model.Instance; -import com.amazonaws.services.ec2.model.GetPasswordDataRequest; -import com.amazonaws.services.ec2.model.GetPasswordDataResult; - -import javax.net.ssl.SSLException; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.ec2.model.GetPasswordDataRequest; +import software.amazon.awssdk.services.ec2.model.GetPasswordDataResponse; +import software.amazon.awssdk.services.ec2.model.Instance; public class EC2WindowsLauncher extends EC2ComputerLauncher { private static final String AGENT_JAR = "remoting.jar"; @@ -36,8 +38,8 @@ public class EC2WindowsLauncher extends EC2ComputerLauncher { final long sleepBetweenAttempts = TimeUnit.SECONDS.toMillis(10); @Override - protected void launchScript(EC2Computer computer, TaskListener listener) throws IOException, - AmazonClientException, InterruptedException { + protected void launchScript(EC2Computer computer, TaskListener listener) + throws IOException, SdkException, InterruptedException { final PrintStream logger = listener.getLogger(); EC2AbstractSlave node = computer.getNode(); if (node == null) { @@ -50,42 +52,53 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws } final WinConnection connection = connectToWinRM(computer, node, template, logger); - + try { String initScript = node.initScript; - String tmpDir = (node.tmpDir != null && !node.tmpDir.equals("") ? WindowsUtil.quoteArgument(Util.ensureEndsWith(node.tmpDir,"\\")) + String tmpDir = (node.tmpDir != null && !node.tmpDir.isEmpty() + ? WindowsUtil.quoteArgument(Util.ensureEndsWith(node.tmpDir, "\\")) : "C:\\Windows\\Temp\\"); logger.println("Creating tmp directory if it does not exist"); WindowsProcess mkdirProcess = connection.execute("if not exist " + tmpDir + " mkdir " + tmpDir); - int exitCode = mkdirProcess.waitFor(); - if (exitCode != 0) { - logger.println("Creating tmpdir failed=" + exitCode); - return; + try { + int exitCode = mkdirProcess.waitFor(); + if (exitCode != 0) { + logger.println("Creating tmpdir failed=" + exitCode); + return; + } + } catch (Exception e) { + mkdirProcess.destroy(); + throw e; } - if (initScript != null && initScript.trim().length() > 0 && !connection.exists(tmpDir + ".jenkins-init")) { + if (initScript != null && !initScript.trim().isEmpty() && !connection.exists(tmpDir + ".jenkins-init")) { logger.println("Executing init script"); - try(OutputStream init = connection.putFile(tmpDir + "init.bat")) { - init.write(initScript.getBytes("utf-8")); + try (OutputStream init = connection.putFile(tmpDir + "init.bat")) { + init.write(initScript.getBytes(StandardCharsets.UTF_8)); } WindowsProcess initProcess = connection.execute("cmd /c " + tmpDir + "init.bat"); - IOUtils.copy(initProcess.getStdout(), logger); + try { + IOUtils.copy(initProcess.getStdout(), logger); - int exitStatus = initProcess.waitFor(); - if (exitStatus != 0) { - logger.println("init script failed: exit code=" + exitStatus); - return; + int exitStatus = initProcess.waitFor(); + if (exitStatus != 0) { + logger.println("init script failed: exit code=" + exitStatus); + return; + } + } catch (Exception e) { + initProcess.destroy(); + throw e; } - try(OutputStream initGuard = connection.putFile(tmpDir + ".jenkins-init")) { + try (OutputStream initGuard = connection.putFile(tmpDir + ".jenkins-init")) { initGuard.write("init ran".getBytes(StandardCharsets.UTF_8)); } logger.println("init script ran successfully"); } - try(OutputStream agentJar = connection.putFile(tmpDir + AGENT_JAR)) { + try (OutputStream agentJar = connection.putFile(tmpDir + AGENT_JAR)) { agentJar.write(Jenkins.get().getJnlpJars(AGENT_JAR).readFully()); } @@ -95,22 +108,31 @@ protected void launchScript(EC2Computer computer, TaskListener listener) throws final String jvmopts = node.jvmopts; final String remoteFS = WindowsUtil.quoteArgument(node.getRemoteFS()); final String workDir = Util.fixEmptyAndTrim(remoteFS) != null ? remoteFS : tmpDir; - final String launchString = javaPath + " " + (jvmopts != null ? jvmopts : "") + " -jar " + tmpDir + AGENT_JAR + " -workDir " + workDir; + final String launchString = javaPath + " " + (jvmopts != null ? jvmopts : "") + " -jar " + tmpDir + + AGENT_JAR + " -workDir " + workDir; logger.println("Launching via WinRM:" + launchString); final WindowsProcess process = connection.execute(launchString, 86400); - computer.setChannel(process.getStdout(), process.getStdin(), logger, new Listener() { - @Override - public void onClosed(Channel channel, IOException cause) { - process.destroy(); - connection.close(); - } - }); + try { + computer.setChannel(process.getStdout(), process.getStdin(), logger, new Listener() { + @Override + public void onClosed(Channel channel, IOException cause) { + process.destroy(); + connection.close(); + } + }); + } catch (Exception e) { + process.destroy(); + throw e; + } } catch (EOFException eof) { - // When we launch java with connection.execute(launchString... it keeps running, but if java is not installed - //the computer.setChannel fails with EOFException because the stream is already closed. It fails on - // setChannel - build - negotiate - is.read() == -1. Let's print a clear message to help diagnose the problem + // When we launch java with connection.execute(launchString... it keeps running, but if java is not + // installed + // the computer.setChannel fails with EOFException because the stream is already closed. It fails on + // setChannel - build - negotiate - is.read() == -1. Let's print a clear message to help diagnose the + // problem // In other case you see a EOFException which gives you few clues about the problem. - logger.println("The stream with the java process on the instance was closed. Maybe java is not installed there."); + logger.println( + "The stream with the java process on the instance was closed. Maybe java is not installed there."); eof.printStackTrace(logger); } catch (Throwable ioe) { logger.println("Ouch:"); @@ -121,26 +143,30 @@ public void onClosed(Channel channel, IOException cause) { } @NonNull - private WinConnection connectToWinRM(EC2Computer computer, EC2AbstractSlave node, SlaveTemplate template, PrintStream logger) throws AmazonClientException, - InterruptedException { + private WinConnection connectToWinRM( + EC2Computer computer, EC2AbstractSlave node, SlaveTemplate template, PrintStream logger) + throws SdkException, InterruptedException { final long minTimeout = 3000; long timeout = node.getLaunchTimeoutInMillis(); // timeout is less than 0 when jenkins is booting up. if (timeout < minTimeout) { timeout = minTimeout; } - final long startTime = System.currentTimeMillis(); + final Instant startTime = Instant.now(); logger.println(node.getDisplayName() + " booted at " + node.getCreatedTime()); - boolean alreadyBooted = (startTime - node.getCreatedTime()) > TimeUnit.MINUTES.toMillis(3); + boolean alreadyBooted = + node.getCreatedTime().until(startTime, ChronoUnit.MILLIS) > TimeUnit.MINUTES.toMillis(3); WinConnection connection = null; while (true) { boolean allowSelfSignedCertificate = node.isAllowSelfSignedCertificate(); try { - long waitTime = System.currentTimeMillis() - startTime; + long waitTime = startTime.until(Instant.now(), ChronoUnit.MILLIS); if (waitTime > timeout) { - throw new AmazonClientException("Timed out after " + (waitTime / 1000) - + " seconds of waiting for winrm to be connected"); + throw SdkException.builder() + .message("Timed out after " + (waitTime / 1000) + + " seconds of waiting for winrm to be connected") + .build(); } if (connection == null) { @@ -149,40 +175,51 @@ private WinConnection connectToWinRM(EC2Computer computer, EC2AbstractSlave node // Check when host is null or we will keep trying and receiving a hostname cannot be null forever. if (host == null || "0.0.0.0".equals(host)) { - logger.println("Invalid host (null or 0.0.0.0). Your host is most likely waiting for an IP address."); + logger.println( + "Invalid host (null or 0.0.0.0). Your host is most likely waiting for an IP address."); throw new IOException("goto sleep"); } if (!node.isSpecifyPassword()) { - GetPasswordDataResult result; + GetPasswordDataResponse result; try { - result = node.getCloud().connect().getPasswordData(new GetPasswordDataRequest(instance.getInstanceId())); + result = node.getCloud() + .connect() + .getPasswordData(GetPasswordDataRequest.builder() + .instanceId(instance.instanceId()) + .build()); } catch (Exception e) { - logger.println("Unexpected Exception: " + e.toString()); + logger.println("Unexpected Exception: " + e); Thread.sleep(sleepBetweenAttempts); continue; } - String passwordData = result.getPasswordData(); + String passwordData = result.passwordData(); if (passwordData == null || passwordData.isEmpty()) { logger.println("Waiting for password to be available. Sleeping 10s."); Thread.sleep(sleepBetweenAttempts); continue; } EC2PrivateKey ec2PrivateKey = node.getCloud().resolvePrivateKey(); - if (ec2PrivateKey == null){ - logger.println("Waiting for privateKey to be available. Consider checking the credentials in the cloud configuration. Sleeping 10s."); + if (ec2PrivateKey == null) { + logger.println( + "Waiting for privateKey to be available. Consider checking the credentials in the cloud configuration. Sleeping 10s."); Thread.sleep(sleepBetweenAttempts); continue; } String password = ec2PrivateKey.decryptWindowsPassword(passwordData); if (!node.getRemoteAdmin().equals("Administrator")) { - logger.println("WARNING: For password retrieval remote admin must be Administrator, ignoring user provided value"); + logger.println( + "WARNING: For password retrieval remote admin must be Administrator, ignoring user provided value"); } logger.println("Connecting to " + "(" + host + ") with WinRM as Administrator"); connection = new WinConnection(host, "Administrator", password, allowSelfSignedCertificate); - } else { //password Specified + } else { // password Specified logger.println("Connecting to " + "(" + host + ") with WinRM as " + node.getRemoteAdmin()); - connection = new WinConnection(host, node.getRemoteAdmin(), node.getAdminPassword().getPlainText(), allowSelfSignedCertificate); + connection = new WinConnection( + host, + node.getRemoteAdmin(), + node.getAdminPassword().getPlainText(), + allowSelfSignedCertificate); } connection.setUseHTTPS(node.isUseHTTPS()); } @@ -196,8 +233,8 @@ private WinConnection connectToWinRM(EC2Computer computer, EC2AbstractSlave node if (!alreadyBooted || node.stopOnTerminate) { int bootDelay = node.getBootDelay(); if (bootDelay > 0) { - logger.println("WinRM service responded. Waiting " + bootDelay + "ms for WinRM service to stabilize on " - + node.getDisplayName()); + logger.println("WinRM service responded. Waiting " + bootDelay + + "ms for WinRM service to stabilize on " + node.getDisplayName()); Thread.sleep(bootDelay); logger.println("WinRM should now be ok on " + node.getDisplayName()); } @@ -215,8 +252,9 @@ private WinConnection connectToWinRM(EC2Computer computer, EC2AbstractSlave node if (e instanceof SSLException) { // To avoid reconnecting continuously computer.setTemporarilyOffline(true, OfflineCause.create(Messages._OfflineCause_SSLException())); - // avoid waiting and trying again, this connection needs human intervention to change the certificate - throw new AmazonClientException("The SSL connection failed while negotiating SSL", e); + // avoid waiting and trying again, this connection needs human intervention to change the + // certificate + throw SdkException.create("The SSL connection failed while negotiating SSL", e); } logger.println("Waiting for WinRM to come up. Sleeping 10s."); Thread.sleep(sleepBetweenAttempts); diff --git a/src/main/java/hudson/plugins/ec2/win/SelfSignedCertificateAllowedMonitor.java b/src/main/java/hudson/plugins/ec2/win/SelfSignedCertificateAllowedMonitor.java index 9b6d16547..3d05ddde7 100644 --- a/src/main/java/hudson/plugins/ec2/win/SelfSignedCertificateAllowedMonitor.java +++ b/src/main/java/hudson/plugins/ec2/win/SelfSignedCertificateAllowedMonitor.java @@ -30,21 +30,20 @@ import hudson.plugins.ec2.SlaveTemplate; import hudson.plugins.ec2.WindowsData; import hudson.slaves.Cloud; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; import jenkins.model.Jenkins; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.interceptor.RequirePOST; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.ListIterator; - @Extension public class SelfSignedCertificateAllowedMonitor extends AdministrativeMonitor { - private final static int MAX_TEMPLATES_FOUND = 5; - + private static final int MAX_TEMPLATES_FOUND = 5; + List insecureTemplates = new ArrayList<>(MAX_TEMPLATES_FOUND); @Override @@ -65,15 +64,15 @@ public String getSelfSignedCertAllowedTemplates() { @Override public boolean isActivated() { boolean maxTemplatesReached = false; - + ListIterator cloudIterator = Jenkins.get().clouds.listIterator(); - + // Let's clear the previously calculated wrong templates to populate the lists with them again insecureTemplates.clear(); - + while (cloudIterator.hasNext() && !maxTemplatesReached) { Cloud cloud = cloudIterator.next(); - if (cloud instanceof EC2Cloud) { + if (cloud instanceof EC2Cloud) { maxTemplatesReached = gatherInsecureTemplate((EC2Cloud) cloud); } } @@ -84,13 +83,15 @@ public boolean isActivated() { private boolean gatherInsecureTemplate(EC2Cloud cloud) { List templates = cloud.getTemplates(); for (SlaveTemplate template : templates) { - // It's only for window templates - if (!template.isWindowsSlave()) { + // It's only for WinRM templates + if (!template.isWinRMAgent()) { continue; } AMITypeData amiTypeData = template.getAmiType(); - if (insecureTemplates.size() < MAX_TEMPLATES_FOUND && amiTypeData.isWindows() && ((WindowsData)amiTypeData).isAllowSelfSignedCertificate()) { + if (insecureTemplates.size() < MAX_TEMPLATES_FOUND + && amiTypeData.isWinRMAgent() + && ((WindowsData) amiTypeData).isAllowSelfSignedCertificate()) { // it is insecure insecureTemplates.add(template.getDisplayName()); } @@ -100,17 +101,17 @@ private boolean gatherInsecureTemplate(EC2Cloud cloud) { return true; } } - + return false; } - + @RequirePOST @SuppressWarnings("unused") // used by message.jelly public HttpResponse doAct(@QueryParameter String dismiss) throws IOException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); if (dismiss != null) { disable(true); - } + } return HttpResponses.forwardToPreviousPage(); } } diff --git a/src/main/java/hudson/plugins/ec2/win/WinConnection.java b/src/main/java/hudson/plugins/ec2/win/WinConnection.java index c807dd68a..3b8cf51a2 100644 --- a/src/main/java/hudson/plugins/ec2/win/WinConnection.java +++ b/src/main/java/hudson/plugins/ec2/win/WinConnection.java @@ -1,30 +1,27 @@ package hudson.plugins.ec2.win; +import com.hierynomus.msdtyp.AccessMask; +import com.hierynomus.mssmb2.SMB2CreateDisposition; +import com.hierynomus.mssmb2.SMB2ShareAccess; import com.hierynomus.protocol.transport.TransportException; -import com.hierynomus.security.bc.BCSecurityProvider; -import com.hierynomus.smbj.SmbConfig; +import com.hierynomus.smbj.SMBClient; +import com.hierynomus.smbj.auth.AuthenticationContext; +import com.hierynomus.smbj.connection.Connection; +import com.hierynomus.smbj.session.Session; +import com.hierynomus.smbj.share.DiskShare; +import hudson.plugins.ec2.Messages; import hudson.plugins.ec2.win.winrm.WinRM; import hudson.plugins.ec2.win.winrm.WindowsProcess; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.util.EnumSet; - -import com.hierynomus.smbj.auth.AuthenticationContext; -import com.hierynomus.smbj.SMBClient; -import com.hierynomus.smbj.share.DiskShare; -import com.hierynomus.smbj.connection.Connection; -import com.hierynomus.smbj.session.Session; -import com.hierynomus.msdtyp.AccessMask; -import com.hierynomus.mssmb2.SMB2ShareAccess; -import com.hierynomus.mssmb2.SMB2CreateDisposition; - -import javax.net.ssl.SSLException; import java.util.logging.Level; import java.util.logging.Logger; +import javax.net.ssl.SSLException; +import jenkins.security.FIPS140; public class WinConnection { private static final Logger LOGGER = Logger.getLogger(WinConnection.class.getName()); @@ -40,15 +37,18 @@ public class WinConnection { private Session session; private boolean useHTTPS; - private static final int TIMEOUT=8000; //8 seconds + private static final int TIMEOUT = 8000; // 8 seconds private boolean allowSelfSignedCertificate; @Deprecated public WinConnection(String host, String username, String password) { this(host, username, password, true); } - + public WinConnection(String host, String username, String password, boolean allowSelfSignedCertificate) { + if (FIPS140.useCompliantAlgorithms()) { + throw new IllegalArgumentException(Messages.EC2Cloud_ntlmNotAllowedInFIPSMode()); + } this.host = host; this.username = username; this.password = password; @@ -78,31 +78,31 @@ public WindowsProcess execute(String commandLine, int timeout) { } private DiskShare getSmbShare(String path) throws IOException { - if(this.connection == null) { + if (this.connection == null) { this.connection = smbclient.connect(host); } - if(this.session == null) { + if (this.session == null) { this.session = connection.authenticate(this.authentication); } return (DiskShare) session.connectShare(toAdministrativeShare(path)); } public OutputStream putFile(String path) throws IOException { - return getSmbShare(path).openFile(toFilePath(path), - EnumSet.of(AccessMask.GENERIC_READ, - AccessMask.GENERIC_WRITE), - null, - SMB2ShareAccess.ALL, - SMB2CreateDisposition.FILE_OVERWRITE_IF, - null).getOutputStream(); + return getSmbShare(path) + .openFile( + toFilePath(path), + EnumSet.of(AccessMask.GENERIC_READ, AccessMask.GENERIC_WRITE), + null, + SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_OVERWRITE_IF, + null) + .getOutputStream(); } public InputStream getFile(String path) throws IOException { - return getSmbShare(path).openFile(toFilePath(path), - EnumSet.of(AccessMask.GENERIC_READ), - null, SMB2ShareAccess.ALL, - null, - null).getInputStream(); + return getSmbShare(path) + .openFile(toFilePath(path), EnumSet.of(AccessMask.GENERIC_READ), null, SMB2ShareAccess.ALL, null, null) + .getInputStream(); } public boolean exists(String path) throws IOException { @@ -111,11 +111,11 @@ public boolean exists(String path) throws IOException { private static String toAdministrativeShare(String path) { // administrative windows share are DRIVE$path like - return path.substring(0, 1) + "$"; + return path.charAt(0) + "$"; } private static String toFilePath(String path) { - //Strip drive and leading forward slash + // Strip drive and leading forward slash return path.substring(3); } @@ -127,14 +127,12 @@ public boolean ping() { return false; } } - + public boolean pingFailingIfSSHHandShakeError() throws IOException { LOGGER.log(Level.FINE, () -> "checking SMB connection to " + host); - try ( - Socket socket = new Socket(); - Connection connection = smbclient.connect(host); - Session session = connection.authenticate(authentication); - ) { + try (Socket socket = new Socket(); + Connection connection = smbclient.connect(host); + Session session = connection.authenticate(authentication)) { socket.connect(new InetSocketAddress(host, 445), TIMEOUT); winrm().ping(); session.connectShare("IPC$"); @@ -154,21 +152,21 @@ public boolean pingFailingIfSSHHandShakeError() throws IOException { } public void close() { - if(this.session != null) { + if (this.session != null) { try { this.session.close(); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Failed to close session", e); } } - if(this.connection != null) { + if (this.connection != null) { try { this.connection.close(); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Failed to close connection", e); } } - if(this.smbclient != null) { + if (this.smbclient != null) { try { this.smbclient.close(); } catch (Exception e) { diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/NegotiateNTLMSchemaFactory.java b/src/main/java/hudson/plugins/ec2/win/winrm/NegotiateNTLMSchemaFactory.java index 9583553ab..9c0b7e7a5 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/NegotiateNTLMSchemaFactory.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/NegotiateNTLMSchemaFactory.java @@ -2,7 +2,11 @@ import org.apache.http.Header; import org.apache.http.HttpRequest; -import org.apache.http.auth.*; +import org.apache.http.auth.AuthScheme; +import org.apache.http.auth.AuthSchemeProvider; +import org.apache.http.auth.AuthenticationException; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.NTCredentials; import org.apache.http.client.config.AuthSchemes; import org.apache.http.impl.auth.NTLMScheme; import org.apache.http.message.BufferedHeader; @@ -11,6 +15,7 @@ public class NegotiateNTLMSchemaFactory implements AuthSchemeProvider { + @Override public AuthScheme create(HttpContext context) { return new NegotiateNTLM(); } @@ -25,10 +30,11 @@ public String getSchemeName() { public Header authenticate(Credentials credentials, HttpRequest request) throws AuthenticationException { Credentials ntCredentials = credentials; if (!(credentials instanceof NTCredentials)) { - ntCredentials = new NTCredentials(credentials.getUserPrincipal().getName(), credentials.getPassword(), null, null); + ntCredentials = new NTCredentials( + credentials.getUserPrincipal().getName(), credentials.getPassword(), null, null); } Header header = super.authenticate(ntCredentials, request); - //need replace NTLM with Negotiate + // need replace NTLM with Negotiate CharArrayBuffer buffer = new CharArrayBuffer(512); buffer.append(header.getName()); buffer.append(": "); diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/WinRM.java b/src/main/java/hudson/plugins/ec2/win/winrm/WinRM.java index ed34b5063..bce9dcbb5 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/WinRM.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/WinRM.java @@ -1,5 +1,6 @@ package hudson.plugins.ec2.win.winrm; +import hudson.plugins.ec2.util.FIPS140Utils; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; @@ -20,8 +21,10 @@ public class WinRM { public WinRM(String host, String username, String password) { this(host, username, password, true); } - + public WinRM(String host, String username, String password, boolean allowSelfSignedCertificate) { + FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate); + this.host = host; this.username = username; this.password = password; @@ -57,6 +60,8 @@ public WindowsProcess execute(String commandLine) { } public URL buildURL() { + FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); + String scheme = useHTTPS ? "https" : "http"; int port = useHTTPS ? 5986 : 5985; @@ -79,6 +84,7 @@ public boolean isUseHTTPS() { * the useHTTPS to set */ public void setUseHTTPS(boolean useHTTPS) { + FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); this.useHTTPS = useHTTPS; } @@ -101,7 +107,7 @@ public void setTimeout(int timeout) { * # Convert the number of seconds to an ISO8601 duration format # @see * http://tools.ietf.org/html/rfc2445#section-4.3.6 # @param [Fixnum] seconds The amount of seconds for this * duration - * + * * @param seconds * @return */ diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java index 2e76ebbf1..e32f05259 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMClient.java @@ -1,5 +1,6 @@ package hudson.plugins.ec2.win.winrm; +import hudson.plugins.ec2.util.FIPS140Utils; import hudson.plugins.ec2.win.winrm.request.RequestFactory; import hudson.plugins.ec2.win.winrm.soap.Namespaces; import hudson.remoting.FastPipedOutputStream; @@ -14,7 +15,6 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; - import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.ParseException; @@ -22,10 +22,10 @@ import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.HttpClient; import org.apache.http.client.config.AuthSchemes; import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.config.Lookup; import org.apache.http.config.RegistryBuilder; @@ -66,19 +66,22 @@ public class WinRMClient { private boolean useHTTPS; private BasicCredentialsProvider credsProvider; private final boolean allowSelfSignedCertificate; - + @Deprecated public WinRMClient(URL url, String username, String password) { this(url, username, password, false); } public WinRMClient(URL url, String username, String password, boolean allowSelfSignedCertificate) { + FIPS140Utils.ensureNoPasswordLeak(url, password); + FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate); + this.url = url; this.username = username; this.password = password; this.factory = new RequestFactory(url); this.allowSelfSignedCertificate = allowSelfSignedCertificate; - + setupHTTPClient(); } @@ -93,7 +96,7 @@ public void executeCommand(String command) { LOGGER.log(Level.FINE, () -> "winrm execute on " + shellId + " command: " + command); Document request = factory.newExecuteCommandRequest(shellId, command).build(); commandId = first(sendRequest(request), "//" + Namespaces.NS_WIN_SHELL.getPrefix() + ":CommandId"); - LOGGER.log(Level.FINER, ()-> "winrm started execution on " + shellId + " commandId: " + commandId); + LOGGER.log(Level.FINER, () -> "winrm started execution on " + shellId + " commandId: " + commandId); } public void deleteShell() { @@ -105,7 +108,6 @@ public void deleteShell() { Document request = factory.newDeleteShellRequest(shellId).build(); sendRequest(request); - } public void signal() { @@ -122,13 +124,16 @@ public void signal() { public void sendInput(byte[] input) { LOGGER.log(Level.FINE, () -> "--> sending " + input.length); - Document request = factory.newSendInputRequest(input, shellId, commandId).build(); + Document request = + factory.newSendInputRequest(input, shellId, commandId).build(); sendRequest(request); } public boolean slurpOutput(FastPipedOutputStream stdout, FastPipedOutputStream stderr) throws IOException { LOGGER.log(Level.FINE, () -> "--> SlurpOutput"); - Map streams = new HashMap<>(); streams.put("stdout", stdout); streams.put("stderr", stderr); + Map streams = new HashMap<>(); + streams.put("stdout", stdout); + streams.put("stderr", stderr); Document request = factory.newGetOutputRequest(shellId, commandId).build(); Document response = sendRequest(request); @@ -139,18 +144,21 @@ public boolean slurpOutput(FastPipedOutputStream stdout, FastPipedOutputStream s xpath.setNamespaceContext(namespaceContext); for (Node node : xpath.selectNodes(response)) { - if (node instanceof Element) { - Element e = (Element) node; - FastPipedOutputStream stream = streams.get(e.attribute("Name").getText().toLowerCase()); + if (node instanceof Element e) { + FastPipedOutputStream stream = + streams.get(e.attribute("Name").getText().toLowerCase()); final byte[] decode = Base64.getDecoder().decode(e.getText()); - LOGGER.log(Level.FINE, () -> "piping " + decode.length + " bytes from " - + e.attribute("Name").getText().toLowerCase()); + LOGGER.log( + Level.FINE, + () -> "piping " + decode.length + " bytes from " + + e.attribute("Name").getText().toLowerCase()); stream.write(decode); } } - XPath done = DocumentHelper.createXPath("//*[@State='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done']"); + XPath done = DocumentHelper.createXPath( + "//*[@State='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done']"); done.setNamespaceContext(namespaceContext); final List nodes = done.selectNodes(response); if (nodes != null && nodes.isEmpty()) { @@ -178,46 +186,51 @@ private static String first(Document doc, String selector) { private void setupHTTPClient() { credsProvider = new BasicCredentialsProvider(); - credsProvider.setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT), new UsernamePasswordCredentials(username, password)); + credsProvider.setCredentials( + new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT), + new UsernamePasswordCredentials(username, password)); } private HttpClient buildHTTPClient() { + // This can occur if setUseHTTPS is not called + FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); + // This is a double check and should be caught by the check in the constructor + FIPS140Utils.ensureNoPasswordLeak(url, password); + // This is a double check and should be caught by the check in the constructor + FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate); + HttpClientBuilder builder = HttpClientBuilder.create().setDefaultCredentialsProvider(credsProvider); - if(! (username.contains("\\")|| username.contains("/"))) { - //user is not a domain user + if (!(username.contains("\\") || username.contains("/"))) { + // user is not a domain user Lookup authSchemeRegistry = RegistryBuilder.create() - .register(AuthSchemes.BASIC, new BasicSchemeFactory()) - .register(AuthSchemes.SPNEGO,new NegotiateNTLMSchemaFactory()) - .build(); + .register(AuthSchemes.BASIC, new BasicSchemeFactory()) + .register(AuthSchemes.SPNEGO, new NegotiateNTLMSchemaFactory()) + .build(); builder.setDefaultAuthSchemeRegistry(authSchemeRegistry); } if (useHTTPS) { - WinRMConnectionManagerFactory.WinRMHttpConnectionManager connectionManager = - allowSelfSignedCertificate ? WinRMConnectionManagerFactory.SSL_ALLOW_SELF_SIGNED - : WinRMConnectionManagerFactory.SSL; + WinRMConnectionManagerFactory.WinRMHttpConnectionManager connectionManager = allowSelfSignedCertificate + ? WinRMConnectionManagerFactory.SSL_ALLOW_SELF_SIGNED + : WinRMConnectionManagerFactory.SSL; builder.setSSLSocketFactory(connectionManager.getSocketFactory()); builder.setConnectionManager(connectionManager.getConnectionManager()); } else { builder.setConnectionManager(WinRMConnectionManagerFactory.DEFAULT.getConnectionManager()); } RequestConfig requestConfig = RequestConfig.custom() - .setConnectTimeout(5000) - .setSocketTimeout(0) - .build(); + .setConnectTimeout(5000) + .setSocketTimeout(0) + .build(); builder.setDefaultRequestConfig(requestConfig); - SocketConfig socketConfig = SocketConfig.custom() - .setTcpNoDelay(true) - .build(); + SocketConfig socketConfig = SocketConfig.custom().setTcpNoDelay(true).build(); builder.setDefaultSocketConfig(socketConfig); // Add sleep between re-tries, by-default the call gets retried immediately. builder.setRetryHandler(new DefaultHttpRequestRetryHandler() { @Override public boolean retryRequest( - final IOException exception, - final int executionCount, - final HttpContext context) { + final IOException exception, final int executionCount, final HttpContext context) { boolean retryRequest = super.retryRequest(exception, executionCount, context); - if ( retryRequest ) { + if (retryRequest) { // sleep before retrying, increase the sleep time on each re-try int sleepTime = executionCount * 5; try { @@ -266,7 +279,10 @@ private Document sendRequest(Document request, int retry) { // check for possible timeout if (response.getStatusLine().getStatusCode() == 500 - && (responseEntity.getContentType() != null && entity.getContentType().getValue().startsWith(ContentType.APPLICATION_SOAP_XML.getMimeType()))) { + && (responseEntity.getContentType() != null + && entity.getContentType() + .getValue() + .startsWith(ContentType.APPLICATION_SOAP_XML.getMimeType()))) { String respStr = EntityUtils.toString(responseEntity); if (respStr.contains("TimedOut")) { return DocumentHelper.parseText(respStr); @@ -279,7 +295,8 @@ private Document sendRequest(Document request, int retry) { if (response.getStatusLine().getStatusCode() == 401) { // we need to force using new connections here // throw away our auth cache - LOGGER.log(Level.WARNING, "winrm returned 401 - shouldn't happen though - retrying in 2 minutes"); + LOGGER.log( + Level.WARNING, "winrm returned 401 - shouldn't happen though - retrying in 2 minutes"); try { Thread.sleep(TimeUnit.MINUTES.toMillis(2)); } catch (InterruptedException e) { @@ -289,12 +306,15 @@ private Document sendRequest(Document request, int retry) { LOGGER.log(Level.WARNING, "winrm returned 401 - retrying now"); return sendRequest(request, ++retry); } - LOGGER.log(Level.WARNING, "winrm service " + shellId + " unexpected HTTP Response (" - + response.getStatusLine().getReasonPhrase() + "): " - + EntityUtils.toString(response.getEntity())); - - throw new RuntimeException("Unexpected HTTP response " + response.getStatusLine().getStatusCode() - + " on " + url + ": " + response.getStatusLine().getReasonPhrase()); + LOGGER.log( + Level.WARNING, + "winrm service " + shellId + " unexpected HTTP Response (" + + response.getStatusLine().getReasonPhrase() + "): " + + EntityUtils.toString(response.getEntity())); + + throw new RuntimeException("Unexpected HTTP response " + + response.getStatusLine().getStatusCode() + " on " + url + ": " + + response.getStatusLine().getReasonPhrase()); } } @@ -337,6 +357,7 @@ public void setTimeout(String timeout) { } public void setUseHTTPS(boolean useHTTPS) { + FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password); this.useHTTPS = useHTTPS; } } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectException.java b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectException.java index 85db112f3..368f036e8 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectException.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectException.java @@ -5,5 +5,4 @@ public class WinRMConnectException extends RuntimeIOException { public WinRMConnectException(String message, Throwable cause) { super(message, cause); } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectionManagerFactory.java b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectionManagerFactory.java index 0721343c5..690fc12c0 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectionManagerFactory.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/WinRMConnectionManagerFactory.java @@ -1,5 +1,10 @@ package hudson.plugins.ec2.win.winrm; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.util.logging.Level; +import java.util.logging.Logger; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.socket.ConnectionSocketFactory; @@ -9,14 +14,8 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.ssl.SSLContextBuilder; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.util.logging.Level; -import java.util.logging.Logger; - public class WinRMConnectionManagerFactory { - private static final Logger log = Logger.getLogger(WinRMClient.class.getName()); + private static final Logger log = Logger.getLogger(WinRMConnectionManagerFactory.class.getName()); static final WinRMHttpConnectionManager DEFAULT = new WinRMHttpConnectionManager(); static final WinRMHttpConnectionManager SSL = new WinRMHttpConnectionManager(false); @@ -26,8 +25,8 @@ static class WinRMHttpConnectionManager { private final PoolingHttpClientConnectionManager connectionManager; private SSLConnectionSocketFactory socketFactory; - final static int DEFAULT_MAX_PER_ROUTE = 50; - final static int MAX_TOTAL = 2500; + static final int DEFAULT_MAX_PER_ROUTE = 50; + static final int MAX_TOTAL = 2500; WinRMHttpConnectionManager() { connectionManager = new PoolingHttpClientConnectionManager(); @@ -54,7 +53,9 @@ private Registry getSslSocketFactory(boolean allowSelfS try { if (allowSelfSignedCertificate) { this.socketFactory = new SSLConnectionSocketFactory( - new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build(), + new SSLContextBuilder() + .loadTrustMaterial(null, new TrustSelfSignedStrategy()) + .build(), NoopHostnameVerifier.INSTANCE); log.log(Level.FINE, "Allowing self-signed certificates"); } else { @@ -70,4 +71,4 @@ private Registry getSslSocketFactory(boolean allowSelfS .build(); } } -} \ No newline at end of file +} diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/WindowsProcess.java b/src/main/java/hudson/plugins/ec2/win/winrm/WindowsProcess.java index d255b700c..1f0f14fea 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/WindowsProcess.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/WindowsProcess.java @@ -86,8 +86,19 @@ public synchronized void destroy() { return; } - client.signal(); - client.deleteShell(); + // Instance may already be terminated, causing WinRM operations to fail + try { + client.signal(); + } catch (Exception e) { + LOGGER.log(Level.FINE, () -> "Failed to signal WinRM shell: " + e.getMessage()); + } + + try { + client.deleteShell(); + } catch (Exception e) { + LOGGER.log(Level.FINE, () -> "Failed to delete WinRM shell: " + e.getMessage()); + } + terminated = true; Closeables.closeQuietly(toCallersStdout); Closeables.closeQuietly(toCallersStdin); @@ -102,7 +113,7 @@ private void startStdoutCopyThread() { @Override public void run() { try { - for (;;) { + for (; ; ) { if (!client.slurpOutput(toCallersStdout, toCallersStderr)) { LOGGER.log(Level.FINE, () -> "no more output for " + command); break; @@ -127,13 +138,15 @@ private void startStdinCopyThread() { public void run() { try { byte[] buf = new byte[INPUT_BUFFER]; - for (;;) { + for (; ; ) { int n = toCallersStdin.read(buf); - if (n == -1) + if (n == -1) { break; - if (n == 0) + } + if (n == 0) { continue; + } byte[] bufToSend = new byte[n]; System.arraycopy(buf, 0, bufToSend, 0, n); diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/request/AbstractWinRMRequest.java b/src/main/java/hudson/plugins/ec2/win/winrm/request/AbstractWinRMRequest.java index 14bd5d1e4..0d9a1c6e1 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/request/AbstractWinRMRequest.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/request/AbstractWinRMRequest.java @@ -1,13 +1,11 @@ package hudson.plugins.ec2.win.winrm.request; +import hudson.plugins.ec2.win.winrm.soap.HeaderBuilder; +import hudson.plugins.ec2.win.winrm.soap.MessageBuilder; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.UUID; - -import hudson.plugins.ec2.win.winrm.soap.HeaderBuilder; -import hudson.plugins.ec2.win.winrm.soap.MessageBuilder; - import org.dom4j.Document; import org.dom4j.Element; @@ -28,13 +26,19 @@ public AbstractWinRMRequest(URL url) { protected abstract void construct(); + @Override public Document build() { construct(); return message.build(); } protected HeaderBuilder defaultHeader() throws URISyntaxException { - return header.to(url.toURI()).replyTo(new URI("http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous")).maxEnvelopeSize(envelopSize).id(generateUUID()).locale(locale).timeout(timeout); + return header.to(url.toURI()) + .replyTo(new URI("http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous")) + .maxEnvelopeSize(envelopSize) + .id(generateUUID()) + .locale(locale) + .timeout(timeout); } protected void setBody(Element body) { @@ -69,5 +73,4 @@ public String getLocale() { public void setLocale(String locale) { this.locale = locale; } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/request/DeleteShellRequest.java b/src/main/java/hudson/plugins/ec2/win/winrm/request/DeleteShellRequest.java index 5bdb7f673..3646355c6 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/request/DeleteShellRequest.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/request/DeleteShellRequest.java @@ -16,12 +16,14 @@ public DeleteShellRequest(URL url, String shellId) { @Override protected void construct() { try { - defaultHeader().action(new URI("http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete")).shellId(shellId).resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")); + defaultHeader() + .action(new URI("http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete")) + .shellId(shellId) + .resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")); setBody(null); } catch (URISyntaxException e) { throw new RuntimeException("Error while building request content", e); } } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/request/ExecuteCommandRequest.java b/src/main/java/hudson/plugins/ec2/win/winrm/request/ExecuteCommandRequest.java index df936b63d..9e3db9d0e 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/request/ExecuteCommandRequest.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/request/ExecuteCommandRequest.java @@ -2,12 +2,10 @@ import hudson.plugins.ec2.win.winrm.soap.Namespaces; import hudson.plugins.ec2.win.winrm.soap.Option; - import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Collections; - import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.QName; @@ -26,7 +24,11 @@ public ExecuteCommandRequest(URL url, String shellId, String command) { @Override protected void construct() { try { - defaultHeader().action(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command")).resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")).shellId(shellId).options(Collections.singletonList(new Option("WINRS_CONSOLEMODE_STDIN", "FALSE"))); + defaultHeader() + .action(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command")) + .resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")) + .shellId(shellId) + .options(Collections.singletonList(new Option("WINRS_CONSOLEMODE_STDIN", "FALSE"))); Element body = DocumentHelper.createElement(QName.get("CommandLine", Namespaces.NS_WIN_SHELL)); body.addElement(QName.get("Command", Namespaces.NS_WIN_SHELL)).addText("\"" + command + "\""); @@ -35,5 +37,4 @@ protected void construct() { throw new RuntimeException("Error while building request content", e); } } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/request/GetOutputRequest.java b/src/main/java/hudson/plugins/ec2/win/winrm/request/GetOutputRequest.java index 2b8e15a80..c4b5834a0 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/request/GetOutputRequest.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/request/GetOutputRequest.java @@ -1,11 +1,9 @@ package hudson.plugins.ec2.win.winrm.request; import hudson.plugins.ec2.win.winrm.soap.Namespaces; - import java.net.URI; import java.net.URISyntaxException; import java.net.URL; - import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.QName; @@ -23,14 +21,18 @@ public GetOutputRequest(URL url, String shellId, String commandId) { @Override protected void construct() { try { - defaultHeader().action(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive")).resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")).shellId(shellId); + defaultHeader() + .action(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive")) + .resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")) + .shellId(shellId); Element body = DocumentHelper.createElement(QName.get("Receive", Namespaces.NS_WIN_SHELL)); - body.addElement(QName.get("DesiredStream", Namespaces.NS_WIN_SHELL)).addAttribute("CommandId", commandId).addText("stdout stderr"); + body.addElement(QName.get("DesiredStream", Namespaces.NS_WIN_SHELL)) + .addAttribute("CommandId", commandId) + .addText("stdout stderr"); setBody(body); } catch (URISyntaxException e) { throw new RuntimeException("Error while building request content", e); } } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/request/OpenShellRequest.java b/src/main/java/hudson/plugins/ec2/win/winrm/request/OpenShellRequest.java index cdacfb908..92ae5fe05 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/request/OpenShellRequest.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/request/OpenShellRequest.java @@ -2,12 +2,10 @@ import hudson.plugins.ec2.win.winrm.soap.Namespaces; import hudson.plugins.ec2.win.winrm.soap.Option; - import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Arrays; - import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.QName; @@ -18,9 +16,14 @@ public OpenShellRequest(URL url) { super(url); } + @Override protected void construct() { try { - defaultHeader().action(new URI("http://schemas.xmlsoap.org/ws/2004/09/transfer/Create")).resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")).options(Arrays.asList(new Option("WINRS_NOPROFILE", "FALSE"), new Option("WINRS_CODEPAGE", "437"))); + defaultHeader() + .action(new URI("http://schemas.xmlsoap.org/ws/2004/09/transfer/Create")) + .resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")) + .options( + Arrays.asList(new Option("WINRS_NOPROFILE", "FALSE"), new Option("WINRS_CODEPAGE", "437"))); Element body = DocumentHelper.createElement(QName.get("Shell", Namespaces.NS_WIN_SHELL)); body.addElement(QName.get("InputStreams", Namespaces.NS_WIN_SHELL)).addText("stdin"); @@ -29,7 +32,5 @@ protected void construct() { } catch (URISyntaxException e) { throw new RuntimeException("Error while building request content", e); } - } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/request/RequestFactory.java b/src/main/java/hudson/plugins/ec2/win/winrm/request/RequestFactory.java index 0d01e5ad7..d77a5d5c9 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/request/RequestFactory.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/request/RequestFactory.java @@ -77,5 +77,4 @@ public String getLocale() { public void setLocale(String locale) { this.locale = locale; } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/request/SendInputRequest.java b/src/main/java/hudson/plugins/ec2/win/winrm/request/SendInputRequest.java index d483ae2f4..6a63dc869 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/request/SendInputRequest.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/request/SendInputRequest.java @@ -1,12 +1,10 @@ package hudson.plugins.ec2.win.winrm.request; import hudson.plugins.ec2.win.winrm.soap.Namespaces; - import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Base64; - import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.QName; @@ -26,17 +24,19 @@ public SendInputRequest(URL url, byte[] input, String shellId, String commandId) @Override protected void construct() { try { - defaultHeader().action(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Send")).resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")).shellId(shellId); + defaultHeader() + .action(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Send")) + .resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")) + .shellId(shellId); Element body = DocumentHelper.createElement(QName.get("Send", Namespaces.NS_WIN_SHELL)); body.addElement(QName.get("Stream", Namespaces.NS_WIN_SHELL)) - .addAttribute("Name", "stdin") - .addAttribute("CommandId", commandId) - .addText(Base64.getEncoder().encodeToString(input)); + .addAttribute("Name", "stdin") + .addAttribute("CommandId", commandId) + .addText(Base64.getEncoder().encodeToString(input)); setBody(body); } catch (URISyntaxException e) { throw new RuntimeException("Error while building request content", e); } } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/request/SignalRequest.java b/src/main/java/hudson/plugins/ec2/win/winrm/request/SignalRequest.java index 7e7c21975..4267fd3c3 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/request/SignalRequest.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/request/SignalRequest.java @@ -1,11 +1,9 @@ package hudson.plugins.ec2.win.winrm.request; import hudson.plugins.ec2.win.winrm.soap.Namespaces; - import java.net.URI; import java.net.URISyntaxException; import java.net.URL; - import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.QName; @@ -23,15 +21,19 @@ public SignalRequest(URL url, String shellId, String commandId) { @Override protected void construct() { try { - defaultHeader().action(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command")).resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")).shellId(shellId); + defaultHeader() + .action(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command")) + .resourceURI(new URI("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd")) + .shellId(shellId); - Element body = DocumentHelper.createElement(QName.get("Signal", Namespaces.NS_WIN_SHELL)).addAttribute("CommandId", commandId); + Element body = DocumentHelper.createElement(QName.get("Signal", Namespaces.NS_WIN_SHELL)) + .addAttribute("CommandId", commandId); - body.addElement(QName.get("Code", Namespaces.NS_WIN_SHELL)).addText("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate"); + body.addElement(QName.get("Code", Namespaces.NS_WIN_SHELL)) + .addText("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate"); setBody(body); } catch (URISyntaxException e) { throw new RuntimeException("Error while building request content", e); } } - } diff --git a/src/main/java/hudson/plugins/ec2/win/winrm/soap/Header.java b/src/main/java/hudson/plugins/ec2/win/winrm/soap/Header.java index d4292f862..be21aebb2 100644 --- a/src/main/java/hudson/plugins/ec2/win/winrm/soap/Header.java +++ b/src/main/java/hudson/plugins/ec2/win/winrm/soap/Header.java @@ -1,25 +1,33 @@ package hudson.plugins.ec2.win.winrm.soap; -import org.dom4j.Element; -import org.dom4j.QName; - -import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.dom4j.Element; +import org.dom4j.QName; public class Header { private final String to; - private final String replyTo; - private final String maxEnvelopeSize; - private final String timeout; - private final String locale; - private final String id; - private final String action; - private final String shellId; - private final String resourceURI; + private final String replyTo; + private final String maxEnvelopeSize; + private final String timeout; + private final String locale; + private final String id; + private final String action; + private final String shellId; + private final String resourceURI; private final List