diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 8a409c6c319..25389be95db 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -25,5 +25,5 @@ jobs: - name: Build with Maven # qa skips documentation - we check it on Jenkins CI # We skip checkstyle too - we check it on Jenkins CI - run: ./apache-maven-3.9.9/bin/mvn -B -e -ntp install -Pstaging -Pqa '-Dcheckstyle.skip=true' + run: ./apache-maven-3.9.9/bin/mvn -B -e -ntp validate -Pstaging -Pqa '-Dcheckstyle.skip=true' diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 962f1065ade..c5ac377767f 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -25,5 +25,5 @@ jobs: - name: Build with Maven # qa skips documentation - we check it on Jenkins CI # We skip checkstyle too - we check it on Jenkins CI - run: ./apache-maven-3.9.9/bin/mvn -B -e -ntp install -Pstaging -Pqa '-Dcheckstyle.skip=true' + run: ./apache-maven-3.9.9/bin/mvn -B -e -ntp install -Pstaging -Pqa '-Dcheckstyle.skip=true' -pl :ssh-cluster-tests -am diff --git a/Jenkinsfile b/Jenkinsfile index c2a0b3307b8..2d442464b4c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -188,7 +188,7 @@ spec: dumpSysInfo() sh ''' # Validate the structure in all submodules (especially version ids) - mvn -B -e -fae clean validate -Ptck,set-version-id,staging + mvn -B -e -fae crash -Ptck,set-version-id,staging # Until we fix ANTLR in cmp-support-sqlstore, broken in parallel builds. Just -Pfast after the fix. mvn -B -e install -Pfastest,staging -T4C ./gfbuild.sh archive_bundles diff --git a/appserver/tests/admin/ssh-cluster/src/test/java/org/glassfish/main/test/clusterssh/SshClusterWindowsITest.java b/appserver/tests/admin/ssh-cluster/src/test/java/org/glassfish/main/test/clusterssh/SshClusterWindowsITest.java new file mode 100644 index 00000000000..60ff61c9f8f --- /dev/null +++ b/appserver/tests/admin/ssh-cluster/src/test/java/org/glassfish/main/test/clusterssh/SshClusterWindowsITest.java @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.main.test.clusterssh; + +import java.io.File; +import java.lang.System.Logger; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.nio.file.Path; +import java.time.Duration; + +import org.glassfish.main.test.clusterssh.docker.GlassFishContainer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.io.TempDir; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.Container.ExecResult; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.MountableFile; + +import static java.lang.System.Logger.Level.INFO; +import static java.lang.System.Logger.Level.TRACE; +import static java.lang.System.Logger.Level.WARNING; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.testcontainers.containers.BindMode.READ_ONLY; +import static org.testcontainers.utility.MountableFile.forHostPath; + +@EnabledOnOs(OS.WINDOWS) +@TestMethodOrder(OrderAnnotation.class) +public class SshClusterWindowsITest { + + private static final Logger LOG = System.getLogger(SshClusterWindowsITest.class.getName()); + + private static final String MSG_NODE_STARTED = "NODE STARTED!"; + + private static final String DOMAIN_NAME = "domain1"; + + private static final Path PATH_DOCKER_GF_ROOT = new File("C:\\glassfish7").toPath(); + private static final Path PATH_DOCKER_GF_DOMAINS = PATH_DOCKER_GF_ROOT.resolve(Path.of("glassfish", "domains")); + private static final Path PATH_DOCKER_GF_NODES = PATH_DOCKER_GF_ROOT.resolve(Path.of("glassfish", "nodes")); + private static final Path PATH_DOCKER_GF_DOMAIN1_SERVER_LOG = PATH_DOCKER_GF_DOMAINS + .resolve(Path.of(DOMAIN_NAME, "logs", "server.log")); + private static final Path PATH_DOCKER_GF_NODE1_SERVER_LOG = PATH_DOCKER_GF_NODES + .resolve(Path.of("node1", "agent", "logs", "server.log")); + private static final String PATH_DOCKER_ASADMIN = PATH_DOCKER_GF_ROOT.resolve(Path.of("bin", "asadmin.bat")) + .toString(); + + private static final String PATH_SSH_USERDIR = "/c/Users/root/.ssh"; + private static final String PATH_PRIVATE_KEY = PATH_SSH_USERDIR + "/id_rsa"; + private static final String PATH_SSHD_CFG = "/etc/ssh/sshd_config"; + private static final String PATH_SSHD_LOG = "/var/log/sshd.log"; + + @TempDir + private static File tmpDir; + + /** Docker network */ + private static Network network = Network.newNetwork(); + + @SuppressWarnings("resource") + private static final GlassFishContainer AS_DOMAIN = new GlassFishContainer(network, "admin", "A", getCommandAdmin()) + .withCopyFileToContainer(MountableFile.forClasspathResource("/glassfish.zip"), "/glassfish.zip") + .withClasspathResourceMapping("password_update.txt", "/password_update.txt", READ_ONLY) + .withClasspathResourceMapping("password.txt", "/password.txt", READ_ONLY) + .withExposedPorts(4848) + .withAsTrace(false) + .waitingFor( + Wait.forLogMessage(".*Total startup time including CLI.*", 1).withStartupTimeout(Duration.ofSeconds(60L))); + + @SuppressWarnings("resource") + private static final GlassFishContainer AS_NODE_1 = new GlassFishContainer(network, "node1", "N1", getCommandNode()) + .withAsTrace(false) + .withExposedPorts(22, 4848, 8080) + .waitingFor( + Wait.forLogMessage(".*" + MSG_NODE_STARTED + ".*", 1).withStartupTimeout(Duration.ofSeconds(60L))); + + @BeforeAll + public static void start() throws Exception { + assumeTrue(DockerClientFactory.instance().isDockerAvailable(), "Docker is not available on this environment"); + AS_DOMAIN.start(); + ExecResult keygenResult = AS_DOMAIN.execInContainer(UTF_8, "ssh-keygen", "-b", "4096", + "-t", "rsa", "-f", PATH_PRIVATE_KEY, "-q", "-N", ""); + assertEquals(0, keygenResult.getExitCode(), keygenResult.getStdout() + keygenResult.getStderr()); + File pubKey = new File(tmpDir, "adminkey.pub"); + AS_DOMAIN.copyFileFromContainer(PATH_SSH_USERDIR + "/id_rsa.pub", pubKey.getAbsolutePath()); + AS_NODE_1.withCopyFileToContainer(forHostPath(pubKey.getAbsolutePath()), "/" + pubKey.getName()); + AS_NODE_1.start(); + } + + @AfterAll + public static void stop() throws Exception { + LOG.log(INFO, "Closing docker containers ..."); + if (AS_NODE_1.isRunning()) { + ExecResult result = AS_DOMAIN.execInContainer(PATH_DOCKER_ASADMIN, "stop-node", "--kill", + "node1"); + LOG.log(INFO, "Result: {0}", result.getStdout()); + closeSilently(AS_NODE_1); + } + if (AS_DOMAIN.isRunning()) { + ExecResult result = AS_DOMAIN.execInContainer(PATH_DOCKER_ASADMIN, "stop-domain", "--kill"); + LOG.log(INFO, "Result: {0}", result.getStdout()); + closeSilently(AS_DOMAIN); + } + closeSilently(network); + } + + + /** + * First verify the we can connect using command line ssh and just execute something. + * This is to prove that the server setting is all right for ssh and that it is possible to + * connect with standard SSH client. + * + * @throws Exception + */ + @Test + @Order(1) + public void ssh() throws Exception { + ExecResult sshResult = AS_DOMAIN.execInContainer(UTF_8, + // Under some circumstances it can stuck -> now it has 5 seconds. + "timeout", "5", + // It always asks for a passphrase so we have to specify it even if it is empty. + "sshpass", /*"-v",*/ "-P", "passphrase", "-p", "", + "ssh", /*"-vvvvv",*/ + // Not recommended on production but useful here. Accept server's pub key as trusted. + "-o", "StrictHostKeyChecking=accept-new", + "-o", "KbdInteractiveAuthentication=no", + "-o", "PasswordAuthentication=no", + "-i", PATH_PRIVATE_KEY, "root@node1", + // Execute some command on the server which will create a log visible in the output. + "echo 'I am there!' >> " + PATH_SSHD_LOG); + assertEquals(0, sshResult.getExitCode(), () -> sshResult.getStdout() + sshResult.getStderr()); + } + + + @Test + @Order(2) + public void createNode1() throws Exception { + ExecResult result = AS_DOMAIN.execInContainer(UTF_8, PATH_DOCKER_ASADMIN, "--user", "admin", + "--passwordfile", "/password.txt", "create-node-ssh", "--nodehost", "node1", "--install", "true", + "--sshkeyfile", PATH_PRIVATE_KEY, "--sshuser", "root", + "node1"); + assertEquals(0, result.getExitCode(), result.getStdout() + result.getStderr()); + } + + + @Test + @Order(10) + @Disabled("Not finished yet") + void getRootOfNode1() throws Exception { + URL url = URI.create("http://localhost:" + AS_DOMAIN.getMappedPort(4848) + "/").toURL(); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + try { + connection.setRequestMethod("GET"); + assertEquals(200, connection.getResponseCode(), "Response code"); + } finally { + connection.disconnect(); + } + } + + private static String getCommandAdmin() { + final StringBuilder command = new StringBuilder(); + command.append("echo \"***************** Useful informations about admin domain *****************\""); + command.append(" && set -x && set -e"); + command.append(" && export LANG=\"en_US.UTF-8\"").append(" && export LANGUAGE=\"en_US.UTF-8\""); + command.append(" && (env | sort) && locale"); + command.append(" && ulimit -a"); + command.append(" && cat /etc/hosts && cat /etc/resolv.conf"); + command.append(" && hostname"); + command.append(" && java -version"); + command.append(" && apt-get update && apt-get install -y unzip openssh-client sshpass"); + command.append(" && unzip -q /glassfish.zip -d /opt"); + command.append(" && mkdir -p " + PATH_SSH_USERDIR); + command.append(" && touch " + PATH_SSH_USERDIR + "/known_hosts"); + command.append(getCommandCreatePrivateDir(PATH_SSH_USERDIR)); + command.append(" && ls -la ").append(PATH_DOCKER_GF_ROOT); + command.append(" && ").append(PATH_DOCKER_ASADMIN).append(" start-domain ").append("domain1"); + command.append(" && ").append(PATH_DOCKER_ASADMIN) + .append(" --user admin --passwordfile /password_update.txt change-admin-password"); + command.append(" && ").append(PATH_DOCKER_ASADMIN) + .append(" --user admin --passwordfile /password.txt enable-secure-admin"); + command.append(" && ").append(PATH_DOCKER_ASADMIN).append(" restart-domain"); + command.append(" && tail -n 10000 -F ").append(PATH_DOCKER_GF_DOMAIN1_SERVER_LOG); + return command.toString(); + } + + private static String getCommandNode() { + final StringBuilder command = new StringBuilder(); + command.append("echo \"***************** Useful informations about node1 *****************\""); + command.append(" && set -x && set -e"); + command.append(" && apt-get update && apt-get install -y unzip openssh-server"); + command.append(" && echo 'PermitRootLogin prohibit-password' > " + PATH_SSHD_CFG); + command.append(" && echo 'PasswordAuthentication no' >> " + PATH_SSHD_CFG); + command.append(" && echo 'PubkeyAuthentication yes' >> " + PATH_SSHD_CFG); + command.append(" && echo 'ChallengeResponseAuthentication no' >> " + PATH_SSHD_CFG); + command.append(" && echo 'UsePAM no' >> " + PATH_SSHD_CFG); + command.append(" && echo 'AllowUsers root' >> " + PATH_SSHD_CFG); + command.append(" && echo 'LogLevel INFO' >> " + PATH_SSHD_CFG); + command.append(" && echo 'Subsystem sftp /usr/lib/openssh/sftp-server' >> " + PATH_SSHD_CFG); + + command.append(" && cat " + PATH_SSHD_CFG); + // Bug in sshd - doesn't create it automatically + command.append(getCommandCreatePrivateDir("/var/run/sshd")); + // The directory must exist to create the file. The content must be private. + command.append(" && mkdir -p /root/.ssh"); + command.append(" && cat /adminkey.pub >> /root/.ssh/authorized_keys"); + command.append(getCommandCreatePrivateDir("/root/.ssh")); + command.append(" && sshd -E " + PATH_SSHD_LOG); + command.append(" && sleep 1"); + command.append(" && ps -lAf"); + command.append(" && echo \"" + MSG_NODE_STARTED + "\""); + command.append(" && tail -n 10000 -F ").append(PATH_SSHD_LOG).append(' ') + .append(PATH_DOCKER_GF_NODE1_SERVER_LOG); + return command.toString(); + } + + private static StringBuilder getCommandCreatePrivateDir(String path) { + StringBuilder command = new StringBuilder(); + command.append(" && mkdir -p ").append(path); + command.append(" && chmod -R go-rwx ").append(path); + command.append(" && chown -R root:root ").append(path); + return command; + } + + private static void closeSilently(final AutoCloseable closeable) { + LOG.log(TRACE, "closeSilently(closeable={0})", closeable); + if (closeable == null) { + return; + } + try { + closeable.close(); + } catch (final Exception e) { + LOG.log(WARNING, "Close method caused an exception.", e); + } + } +} diff --git a/appserver/tests/admin/ssh-cluster/src/test/java/org/glassfish/main/test/clusterssh/docker/GlassFishContainer.java b/appserver/tests/admin/ssh-cluster/src/test/java/org/glassfish/main/test/clusterssh/docker/GlassFishContainer.java index d96033d486f..a5423733a55 100644 --- a/appserver/tests/admin/ssh-cluster/src/test/java/org/glassfish/main/test/clusterssh/docker/GlassFishContainer.java +++ b/appserver/tests/admin/ssh-cluster/src/test/java/org/glassfish/main/test/clusterssh/docker/GlassFishContainer.java @@ -27,7 +27,8 @@ public class GlassFishContainer extends GenericContainer { public GlassFishContainer(Network network, String hostname, String logPrefix, String command) { - super("eclipse-temurin:17.0.13_11-jdk"); + super("mcr.microsoft.com/windows/nanoserver:ltsc2025"); +// super("eclipse-temurin:17.0.13_11-jdk"); withNetwork(network) .withEnv("TZ", "UTC").withEnv("LC_ALL", "en_US.UTF-8") .withStartupAttempts(1)