diff --git a/src/main/java/hudson/plugins/ec2/AMITypeData.java b/src/main/java/hudson/plugins/ec2/AMITypeData.java index 6d27b95c8..e75bbb584 100644 --- a/src/main/java/hudson/plugins/ec2/AMITypeData.java +++ b/src/main/java/hudson/plugins/ec2/AMITypeData.java @@ -5,6 +5,9 @@ import java.util.concurrent.TimeUnit; public abstract class AMITypeData extends AbstractDescribableImpl { + + public abstract boolean isWindowsSSH(); + public abstract boolean isWindows(); public abstract boolean isUnix(); diff --git a/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java b/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java index d648d8d10..17ac7549e 100644 --- a/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java +++ b/src/main/java/hudson/plugins/ec2/EC2OndemandSlave.java @@ -7,12 +7,12 @@ import hudson.plugins.ec2.ssh.EC2UnixLauncher; import hudson.plugins.ec2.win.EC2WindowsLauncher; import hudson.plugins.ec2.ssh.EC2MacLauncher; +import hudson.plugins.ec2.ssh.EC2WindowsSSHLauncher; import hudson.slaves.NodeProperty; import java.io.IOException; import java.util.Collections; import java.util.List; -import java.util.concurrent.CountDownLatch; import java.util.logging.Level; import java.util.logging.Logger; @@ -73,8 +73,8 @@ public EC2OndemandSlave(String name, String instanceId, String templateDescripti @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) 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.isWindows() ? new EC2WindowsLauncher() : (amiType.isMac() ? new EC2MacLauncher(): (amiType.isWindowsSSH() ? new EC2WindowsSSHLauncher() : + new EC2UnixLauncher()))), new EC2RetentionStrategy(idleTerminationMinutes), initScript, tmpDir, nodeProperties, remoteAdmin, javaPath, jvmopts, stopOnTerminate, idleTerminationMinutes, tags, cloudName, launchTimeout, amiType, connectionStrategy, maxTotalUses, tenancy, metadataEndpointEnabled, metadataTokensRequired, metadataHopsLimit, metadataSupported); this.publicDNS = publicDNS; this.privateDNS = privateDNS; } diff --git a/src/main/java/hudson/plugins/ec2/MacData.java b/src/main/java/hudson/plugins/ec2/MacData.java index 7fc77a5d9..8d95c3704 100644 --- a/src/main/java/hudson/plugins/ec2/MacData.java +++ b/src/main/java/hudson/plugins/ec2/MacData.java @@ -29,6 +29,11 @@ protected Object readResolve() { return this; } + @Override + public boolean isWindowsSSH() { + return false; + } + @Override public boolean isWindows() { return false; 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..f99c1a180 --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/SSHData.java @@ -0,0 +1,119 @@ +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 isWindowsSSH() { + return false; + } + + @Override + public boolean isWindows() { + return false; + } + + @Override + public boolean isUnix() { + return false; + } + + @Override + public boolean isMac() { + 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; + } + + 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/UnixData.java b/src/main/java/hudson/plugins/ec2/UnixData.java index dba888018..250885652 100644 --- a/src/main/java/hudson/plugins/ec2/UnixData.java +++ b/src/main/java/hudson/plugins/ec2/UnixData.java @@ -1,40 +1,15 @@ package hudson.plugins.ec2; +import edu.umd.cs.findbugs.annotations.NonNull; 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; + super(rootCommandPrefix, slaveCommandPrefix, slaveCommandSuffix, sshPort, bootDelay); } @Override @@ -42,85 +17,12 @@ public boolean isUnix() { return true; } - @Override - public boolean isMac() { - return false; - } - @Extension public static class DescriptorImpl extends Descriptor { @Override + @NonNull 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..b32c1d98a 100644 --- a/src/main/java/hudson/plugins/ec2/WindowsData.java +++ b/src/main/java/hudson/plugins/ec2/WindowsData.java @@ -40,6 +40,11 @@ public WindowsData(String password, boolean useHTTPS, String bootDelay) { this(password, useHTTPS, bootDelay, false); } + @Override + public boolean isWindowsSSH() { + return false; + } + @Override public boolean isWindows() { return true; 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..be151ec8f --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/WindowsSSHData.java @@ -0,0 +1,30 @@ +package hudson.plugins.ec2; + +import edu.umd.cs.findbugs.annotations.NonNull; +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 isWindowsSSH() { + return true; + } + + + @Extension + public static class DescriptorImpl extends Descriptor { + @Override + @NonNull + public String getDisplayName() { + return "windows-ssh"; + } + } +} diff --git a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java index f45b54309..b85762a96 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2MacLauncher.java @@ -25,462 +25,67 @@ 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 com.trilead.ssh2.Connection; 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.SlaveTemplate; 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.nio.charset.StandardCharsets; -import java.nio.file.Files; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; import java.util.logging.Level; -import java.util.logging.Logger; /** * {@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 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) - readinessTries = Integer.parseInt(prop); - prop = System.getProperty(READINESS_SLEEP_MS); - 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; - PrintStream logger = listener.getLogger(); - EC2AbstractSlave node = computer.getNode(); - SlaveTemplate template = computer.getSlaveTemplate(); - - if(node == null) { - throw new IllegalStateException(); - } - - if (template == null) { - throw new IOException("Could not find corresponding agent template for " + computer.getDisplayName()); - } - - if (node instanceof EC2Readiness) { - EC2Readiness readinessNode = (EC2Readiness) node; - 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 new AmazonClientException("Node still not ready, timed out after " + (readinessTries * readinessSleepMs / 1000) + "s with status " + readinessNode.getEc2ReadinessStatus()); - } - } - - logInfo(computer, listener, "Launching instance: " + node.getInstanceId()); - + protected void runAmiTypeSpecificLaunchScript(EC2Computer computer, String javaPath, Connection conn, PrintStream logger, TaskListener listener) throws IOException, AmazonClientException { + // TODO: parse the version number. maven-enforcer-plugin might help 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"); - } - // 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 { - 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(); - } - - // 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); - } - 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); - } - } 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"); - } - } + 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); } 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) - cleanupConn.close(); - } - } - - 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; + 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); } + } catch (InterruptedException ex) { + LOGGER.warning(ex.getMessage()); } - 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(); - } 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"); + @Override + protected void configureConnectBySSHProcess(EC2Computer computer, TaskListener listener, SlaveTemplate slaveTemplate, EC2AbstractSlave node, String launchString) throws IOException, InterruptedException { + File identityKeyFile = createIdentityKeyFile(computer); + String ec2HostAddress = getEC2HostAddress(computer, slaveTemplate); + File hostKeyFile; + String userKnownHostsFileFlag = ""; + if (!slaveTemplate.amiType.isMac()) { + hostKeyFile = createHostKeyFile(computer, ec2HostAddress, listener); + if (hostKeyFile != null) { + userKnownHostsFileFlag = String.format(" -o \"UserKnownHostsFile=%s\"", hostKeyFile.getAbsolutePath()); } - 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 (isAuthenticated) { - break; - } - logWarning(computer, listener, "Authentication failed. Trying again..."); - Thread.sleep(bootstrapAuthSleepMs); - } - if (!isAuthenticated) { - logWarning(computer, listener, "Authentication failed"); - return false; - } + // 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", slaveTemplate.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 (bootstrapConn != null) { - bootstrapConn.close(); - } - } - 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); - } + if(!identityKeyFile.delete()) { + LOGGER.log(Level.WARNING, "Failed to delete identity key file"); } } } - - /** - * 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..8f3300b39 --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2SSHLauncher.java @@ -0,0 +1,536 @@ +/* + * 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.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.HostKeyHelper; +import hudson.plugins.ec2.ssh.verifiers.Messages; +import hudson.remoting.Channel; +import hudson.remoting.Channel.Listener; +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.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Base64; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * {@link ComputerLauncher} that connects to a Unix agent on EC2 by using SSH. + * + * @author Kohsuke Kawaguchi + */ +public abstract class EC2SSHLauncher extends EC2ComputerLauncher { + + protected static final Logger LOGGER = Logger.getLogger(EC2SSHLauncher.class.getName()); + protected static final String AGENT_JAR = "remoting.jar"; + + protected static final String BOOTSTRAP_AUTH_SLEEP_MS = "jenkins.ec2.bootstrapAuthSleepMs"; + protected static final String BOOTSTRAP_AUTH_TRIES = "jenkins.ec2.bootstrapAuthTries"; + protected static final String READINESS_SLEEP_MS = "jenkins.ec2.readinessSleepMs"; + protected static final String READINESS_TRIES = "jenkins.ec2.readinessTries"; + + private static int bootstrapAuthSleepMs = 30000; + private static int bootstrapAuthTries = 30; + + 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) + readinessTries = Integer.parseInt(prop); + prop = System.getProperty(READINESS_SLEEP_MS); + 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; + } + + protected abstract void runAmiTypeSpecificLaunchScript(EC2Computer computer, String javaPath, final Connection conn, PrintStream logger, TaskListener listener) throws IOException, AmazonClientException, InterruptedException; + + + @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; + PrintStream logger = listener.getLogger(); + EC2AbstractSlave node = computer.getNode(); + SlaveTemplate template = computer.getSlaveTemplate(); + + if (node == null) { + throw new IllegalStateException(); + } + + if (template == null) { + throw new IOException("Could not find corresponding agent template for " + computer.getDisplayName()); + } + + if (node instanceof EC2Readiness) { + EC2Readiness readinessNode = (EC2Readiness) node; + 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 new AmazonClientException("Node still not ready, timed out after " + (readinessTries * readinessSleepMs / 1000) + "s with status " + readinessNode.getEc2ReadinessStatus()); + } + } + + 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"); + } + + // 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 { + 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 (runInitScript(computer, listener, initScript, conn, logger, scp, tmpDir)) return; + + // TODO: parse the version number. maven-enforcer-plugin might help + final String javaPath = node.javaPath; + runAmiTypeSpecificLaunchScript(computer, javaPath, conn, 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(AGENT_JAR).readFully(), AGENT_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 + "/" + AGENT_JAR + " -workDir " + workDir + suffix; + // launchString = launchString.trim(); + + checkIfConnectBySSHProcess(computer, listener, template, node, launchString, conn, logger); + + successful = true; + } finally { + if (cleanupConn != null && (!successful || template.isConnectBySSHProcess())) + cleanupConn.close(); + } + } + + + protected void checkIfConnectBySSHProcess(EC2Computer computer, TaskListener listener, SlaveTemplate slaveTemplate, EC2AbstractSlave node, String launchString, Connection conn, PrintStream logger) throws IOException, InterruptedException { + if (slaveTemplate != null && slaveTemplate.isConnectBySSHProcess()) { + configureConnectBySSHProcess(computer, listener, slaveTemplate, node, launchString); + } 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(); + } + }); + } + } + + protected void configureConnectBySSHProcess(EC2Computer computer, TaskListener listener, SlaveTemplate slaveTemplate, EC2AbstractSlave node, String launchString) throws IOException, InterruptedException { + File identityKeyFile = createIdentityKeyFile(computer); + String ec2HostAddress = getEC2HostAddress(computer, slaveTemplate); + File hostKeyFile = createHostKeyFile(computer, ec2HostAddress, listener);; + 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, 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"); + } + } + } + + + protected boolean runInitScript(EC2Computer computer, TaskListener listener, String initScript, Connection conn, PrintStream logger, SCPClient scp, String tmpDir) throws IOException, InterruptedException { + 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 true; + } + 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 true; + } + sess.close(); + } + return false; + } + + protected 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; + } + } + return true; + } + + 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 { + FileOutputStream fileOutputStream = new FileOutputStream(tempFile); + OutputStreamWriter writer = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8); + try { + writer.write(privateKey); + writer.flush(); + } 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); + } + } + + 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, 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 (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(); + } + } + return true; + } + + protected 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. + */ + protected 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); + } + } + + + protected static String getEC2HostAddress(EC2Computer computer, SlaveTemplate template) throws InterruptedException { + Instance instance = computer.updateInstanceDescription(); + ConnectionStrategy strategy = template.connectionStrategy; + if (template.amiType.isWindowsSSH()) + return EC2HostAddressProvider.windows(instance, strategy); + else if (template.amiType.isMac()) + return EC2HostAddressProvider.mac(instance, strategy); + else + return 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 ""; + } + + protected 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/EC2UnixLauncher.java b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java index 663a356d6..aa6053617 100644 --- a/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2UnixLauncher.java @@ -23,507 +23,25 @@ */ package hudson.plugins.ec2.ssh; -import hudson.FilePath; -import hudson.Util; -import hudson.ProxyConfiguration; -import hudson.model.Descriptor; +import com.amazonaws.AmazonClientException; +import com.trilead.ssh2.Connection; 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.slaves.CommandLauncher; +import hudson.plugins.ec2.EC2Computer; 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.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; /** * {@link ComputerLauncher} that connects to a Unix agent on EC2 by using SSH. * * @author Kohsuke Kawaguchi */ -public class EC2UnixLauncher extends EC2ComputerLauncher { - - 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 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) - readinessTries = Integer.parseInt(prop); - prop = System.getProperty(READINESS_SLEEP_MS); - 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; - PrintStream logger = listener.getLogger(); - EC2AbstractSlave node = computer.getNode(); - SlaveTemplate template = computer.getSlaveTemplate(); - - if(node == null) { - throw new IllegalStateException(); - } - - if (template == null) { - throw new IOException("Could not find corresponding agent template for " + computer.getDisplayName()); - } - - if (node instanceof EC2Readiness) { - EC2Readiness readinessNode = (EC2Readiness) node; - 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 new AmazonClientException("Node still not ready, timed out after " + (readinessTries * readinessSleepMs / 1000) + "s with status " + readinessNode.getEc2ReadinessStatus()); - } - } - - 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"); - } - - // 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 { - 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(); - } - - // 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(); - - 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 { - 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(); - } - } - - 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; - } - } - 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(); - } 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 (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(); - } - } - 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; - } +public class EC2UnixLauncher extends EC2SSHLauncher { @Override - public Descriptor getDescriptor() { - throw new UnsupportedOperationException(); + protected void runAmiTypeSpecificLaunchScript(EC2Computer computer, String javaPath, Connection conn, PrintStream logger, TaskListener listener) throws IOException, AmazonClientException, InterruptedException { + 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); } } 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..ffd1450f2 --- /dev/null +++ b/src/main/java/hudson/plugins/ec2/ssh/EC2WindowsSSHLauncher.java @@ -0,0 +1,67 @@ +package hudson.plugins.ec2.ssh; + +import com.amazonaws.AmazonClientException; +import com.trilead.ssh2.Connection; +import com.trilead.ssh2.SCPClient; +import com.trilead.ssh2.Session; +import hudson.model.TaskListener; +import hudson.plugins.ec2.EC2Computer; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.PrintStream; + +public class EC2WindowsSSHLauncher extends EC2SSHLauncher { + + @Override + protected void runAmiTypeSpecificLaunchScript(EC2Computer computer, String javaPath, Connection conn, PrintStream logger, TaskListener listener) throws IOException, AmazonClientException, InterruptedException { + executeRemote(computer, conn, javaPath + " -fullversion", "choco install temurin17 -y;", logger, listener); + } + + @Override + protected boolean runInitScript(EC2Computer computer, TaskListener listener, String initScript, Connection conn, PrintStream logger, SCPClient scp, String tmpDir) throws IOException, InterruptedException { + if (initScript != null && !initScript.trim().isEmpty() + && conn.exec("test -e " + tmpDir + " init.bat", logger) != 0) { + logInfo(computer, listener, "Executing init script"); + scp.put(initScript.getBytes("UTF-8"), "init.bat", tmpDir, "0700"); + Session sess = conn.openSession(); + sess.requestDumbPTY(); // so that the remote side bundles stdout + // and stderr + sess.execCommand(buildUpCommand(computer, tmpDir + "/init.bat")); + + 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 true; + } + sess.close(); + + logInfo(computer, listener, "Creating " + tmpDir + ".jenkins-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, "Set-Content -Path " + tmpDir + ".jenkins-init -Value $null")); + + 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 true; + } + sess.close(); + } + return false; + } + +} diff --git a/src/main/resources/hudson/plugins/ec2/WindowsSSHData/config.jelly b/src/main/resources/hudson/plugins/ec2/WindowsSSHData/config.jelly new file mode 100644 index 000000000..1515a3c3e --- /dev/null +++ b/src/main/resources/hudson/plugins/ec2/WindowsSSHData/config.jelly @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/ec2/WindowsSSHData/help-bootDelay.html b/src/main/resources/hudson/plugins/ec2/WindowsSSHData/help-bootDelay.html new file mode 100644 index 000000000..164fe8ac2 --- /dev/null +++ b/src/main/resources/hudson/plugins/ec2/WindowsSSHData/help-bootDelay.html @@ -0,0 +1,6 @@ +
+Indicate here the time in seconds to wait for the machine to be ready once the plugin detects SSH is available. +Unfortunately, on Windows during the boot, the SSH service might be started, and then several minutes after will be +restarted. If this restart happens during the slave provisioning, Windows will prevent subsequent SSH connections and +the slave will not be correctly provisioned. +