diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml new file mode 100644 index 00000000000..b2c6685727d --- /dev/null +++ b/.github/workflows/build-linux.yml @@ -0,0 +1,30 @@ + +name: Build on Ubuntu + +on: + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + - name: Install Maven + run: | + curl -s -S -o ./apache-maven-3.9.9-bin.zip https://dlcdn.apache.org/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.zip + unzip -q ./apache-maven-3.9.9-bin.zip + - 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' + env: + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_API_TOKEN }} diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 7579d9034f5..2442e63bc6b 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -1,5 +1,5 @@ -name: Build on Windows 2022 +name: Build on Windows on: pull_request: @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 21 + - name: Set up JDK uses: actions/setup-java@v4 with: java-version: '21' @@ -20,10 +20,12 @@ jobs: cache: maven - name: Install Maven run: | - curl.exe -o ./apache-maven-3.9.9-bin.zip https://dlcdn.apache.org/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.zip + curl.exe -s -S -o ./apache-maven-3.9.9-bin.zip https://dlcdn.apache.org/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.zip tar -xf ./apache-maven-3.9.9-bin.zip - 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 clean install -Pstaging -Pqa '-Dcheckstyle.skip=true' + run: ./apache-maven-3.9.9/bin/mvn -B -e -ntp clean install -Pstaging -Pqa '-Dcheckstyle.skip=true' + env: + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_API_TOKEN }} diff --git a/appserver/admin/template/src/main/resources/config/domain.xml b/appserver/admin/template/src/main/resources/config/domain.xml index b29c5e4500e..dcc7cdadcfd 100644 --- a/appserver/admin/template/src/main/resources/config/domain.xml +++ b/appserver/admin/template/src/main/resources/config/domain.xml @@ -220,7 +220,7 @@ --add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-exports=java.base/jdk.internal.vm.annotation=ALL-UNNAMED --add-opens=java.base/jdk.internal.vm.annotation=ALL-UNNAMED - + -Djdk.attach.allowAttachSelf=true @@ -383,7 +383,7 @@ -Dosgi.shell.telnet.ip=127.0.0.1 - -Dgosh.args=--noshutdown -c noop=true + -Dgosh.args=--nointeractive -Dfelix.fileinstall.dir=${com.sun.aas.installRoot}/modules/autostart/ diff --git a/appserver/admingui/cluster/src/main/resources/org/glassfish/cluster/admingui/Strings.properties b/appserver/admingui/cluster/src/main/resources/org/glassfish/cluster/admingui/Strings.properties index c1bd0be9cd6..72370a2d49e 100644 --- a/appserver/admingui/cluster/src/main/resources/org/glassfish/cluster/admingui/Strings.properties +++ b/appserver/admingui/cluster/src/main/resources/org/glassfish/cluster/admingui/Strings.properties @@ -190,7 +190,7 @@ node.KeyfileHelp=The absolute path to the SSH User's private key file. The defau node.EditPageTitle=Edit Node node.EditPageTitleHelp=Edit properties for the selected node. node.InstallDir=Installation Directory: -node.InstallDirHelp=The full path to the parent of the base installation directory of the GlassFish Server software on the host, for example, /export/glassfish7. +node.InstallDirHelp=The full path to the parent of the base installation directory of the GlassFish Server software on the host, for example, /export/glassfish7. Paths are always relative to the configured SFTP server root. node.force=Force: node.forceHelp=Specifies whether the node is created even if validation of the node's parameters fails. node.type=Type: diff --git a/appserver/itest-tools/src/main/java/org/glassfish/main/itest/tools/GlassFishTestEnvironment.java b/appserver/itest-tools/src/main/java/org/glassfish/main/itest/tools/GlassFishTestEnvironment.java index 001892153e4..8c0413a04eb 100644 --- a/appserver/itest-tools/src/main/java/org/glassfish/main/itest/tools/GlassFishTestEnvironment.java +++ b/appserver/itest-tools/src/main/java/org/glassfish/main/itest/tools/GlassFishTestEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024 Eclipse Foundation and/or its affiliates. + * Copyright (c) 2022, 2025 Eclipse Foundation and/or its affiliates. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -363,7 +363,7 @@ private static File findPasswordFile(final String filename) { private static void changePassword() { final Asadmin asadmin = new Asadmin(ASADMIN, ADMIN_USER, PASSWORD_FILE_FOR_UPDATE); - final AsadminResult result = asadmin.exec(5_000, "change-admin-password"); + final AsadminResult result = asadmin.exec(20_000, "change-admin-password"); if (result.isError()) { // probably changed by previous execution without maven clean System.out.println("Admin password NOT changed."); diff --git a/appserver/tests/admin/pom.xml b/appserver/tests/admin/pom.xml index 5fc4aad4190..1936afd9f80 100755 --- a/appserver/tests/admin/pom.xml +++ b/appserver/tests/admin/pom.xml @@ -34,5 +34,6 @@ admin-extension tests + ssh-cluster diff --git a/appserver/tests/admin/ssh-cluster/pom.xml b/appserver/tests/admin/ssh-cluster/pom.xml new file mode 100644 index 00000000000..414324346cd --- /dev/null +++ b/appserver/tests/admin/ssh-cluster/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + + org.glassfish.main.tests + admin-tests-parent + 7.0.22-SNAPSHOT + + + ssh-cluster-tests + + + ${project.version} + + + + + org.slf4j + slf4j-jdk14 + 2.0.16 + test + + + org.junit.jupiter + junit-jupiter-engine + + + org.hamcrest + hamcrest + + + org.testcontainers + testcontainers + + + org.testcontainers + junit-jupiter + + + org.glassfish.main.distributions + glassfish + ${glassfish.version} + zip + test + + + + + + + maven-dependency-plugin + + + prepare-glassfish.zip + + copy-dependencies + + generate-resources + + ${project.build.testOutputDirectory} + glassfish + zip + true + + + + + + + \ No newline at end of file diff --git a/appserver/tests/admin/ssh-cluster/src/test/java/org/glassfish/main/test/clusterssh/SshClusterITest.java b/appserver/tests/admin/ssh-cluster/src/test/java/org/glassfish/main/test/clusterssh/SshClusterITest.java new file mode 100644 index 00000000000..e9990954cc7 --- /dev/null +++ b/appserver/tests/admin/ssh-cluster/src/test/java/org/glassfish/main/test/clusterssh/SshClusterITest.java @@ -0,0 +1,265 @@ +/* + * 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.DisabledOnOs; +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; + +@DisabledOnOs(OS.WINDOWS) +@TestMethodOrder(OrderAnnotation.class) +public class SshClusterITest { + + private static final Logger LOG = System.getLogger(SshClusterITest.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 = Path.of("/opt", "glassfish7"); + 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")).toString(); + + private static final String PATH_ETC_ENVIRONMENT = "/etc/environment"; + private static final String PATH_SSH_USERDIR = "/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"); + + // Replace the original which did not mention java -> affects ssh clients + command.append(" && echo \"JAVA_HOME=${JAVA_HOME}\" > " + PATH_ETC_ENVIRONMENT); + command.append(" && echo \"PATH=${PATH}\" >> " + PATH_ETC_ENVIRONMENT); + + 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); + // UsePAM no would mean that the /etc/environment file would not be used. + command.append(" && echo 'UsePAM yes' >> " + 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(PATH_SSH_USERDIR)); + 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 new file mode 100644 index 00000000000..d96033d486f --- /dev/null +++ b/appserver/tests/admin/ssh-cluster/src/test/java/org/glassfish/main/test/clusterssh/docker/GlassFishContainer.java @@ -0,0 +1,61 @@ +/* + * 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.docker; + +import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.Ulimit; + +import java.time.Duration; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; + +public class GlassFishContainer extends GenericContainer { + + public GlassFishContainer(Network network, String hostname, String logPrefix, String command) { + super("eclipse-temurin:17.0.13_11-jdk"); + withNetwork(network) + .withEnv("TZ", "UTC").withEnv("LC_ALL", "en_US.UTF-8") + .withStartupAttempts(1) + .withStartupTimeout(Duration.ofSeconds(30L)) + .withCreateContainerCmdModifier(cmd -> { + cmd.withEntrypoint("/bin/sh", "-c"); + cmd.withCmd(command); + cmd.withHostName(hostname); + cmd.withAttachStderr(true); + cmd.withAttachStdout(true); + final HostConfig hostConfig = cmd.getHostConfig(); + hostConfig.withMemory(2 * 1024 * 1024 * 1024L); + hostConfig.withMemorySwappiness(0L); + hostConfig.withUlimits(new Ulimit[] {new Ulimit("nofile", 4096L, 8192L)}); + }) + .withLogConsumer(o -> { + System.err.println(logPrefix + ": " + o.getUtf8StringWithoutLineEnding()); + System.err.flush(); + } + ); + } + + /** + * @param enable true to enable verbose logging for the asadmin command + * @return this + */ + public GlassFishContainer withAsTrace(final boolean enable) { + withEnv("AS_TRACE", Boolean.toString(enable)); + return this; + } +} diff --git a/appserver/tests/admin/ssh-cluster/src/test/resources/password.txt b/appserver/tests/admin/ssh-cluster/src/test/resources/password.txt new file mode 100644 index 00000000000..77eead52ee0 --- /dev/null +++ b/appserver/tests/admin/ssh-cluster/src/test/resources/password.txt @@ -0,0 +1,2 @@ +AS_ADMIN_PASSWORD=admintest +AS_ADMIN_SSHKEYPASSPHRASE= diff --git a/appserver/tests/admin/ssh-cluster/src/test/resources/password_update.txt b/appserver/tests/admin/ssh-cluster/src/test/resources/password_update.txt new file mode 100644 index 00000000000..73f0e0424ae --- /dev/null +++ b/appserver/tests/admin/ssh-cluster/src/test/resources/password_update.txt @@ -0,0 +1,2 @@ +AS_ADMIN_PASSWORD= +AS_ADMIN_NEWPASSWORD=admintest diff --git a/appserver/tests/appserv-tests/devtests/ejb/pom.xml b/appserver/tests/appserv-tests/devtests/ejb/pom.xml index 677b963f342..8b240d692ce 100644 --- a/appserver/tests/appserv-tests/devtests/ejb/pom.xml +++ b/appserver/tests/appserv-tests/devtests/ejb/pom.xml @@ -17,7 +17,8 @@ --> - + 4.0.0 org.glassfish.main @@ -25,63 +26,52 @@ 7.0.22-SNAPSHOT org.glassfish.main.tests - tests + ant-tests-ejb pom GlassFish devtests ejb - - - - - ant-contrib - ant-contrib - 1.0b3 - + + ant-contrib + ant-contrib + 1.0b3 + - - - maven-antrun-plugin - - - com.sun - tools - 1.8.0 - system - ${env.JAVA_HOME}/lib/tools.jar - - - - - id.test - test - - - - - - - - - run - - - - id.clean - clean - - - - - - - - run - - - - - - + + + maven-antrun-plugin + + + id.test + test + + + + + + + + + run + + + + id.clean + clean + + + + + + + + run + + + + + diff --git a/appserver/tests/pom.xml b/appserver/tests/pom.xml index 15d5920f0a8..da783771c1c 100755 --- a/appserver/tests/pom.xml +++ b/appserver/tests/pom.xml @@ -34,6 +34,23 @@ GlassFish Tests + + + + org.testcontainers + testcontainers + 1.20.3 + test + + + org.testcontainers + junit-jupiter + 1.20.3 + test + + + + diff --git a/appserver/tests/tck/platform-tck-runner/pom.xml b/appserver/tests/tck/platform-tck-runner/pom.xml index 0bf9590afd5..b0658e35be7 100644 --- a/appserver/tests/tck/platform-tck-runner/pom.xml +++ b/appserver/tests/tck/platform-tck-runner/pom.xml @@ -87,7 +87,6 @@ org.testcontainers testcontainers - 1.17.3 diff --git a/nucleus/admin/launcher/src/main/java/com/sun/enterprise/admin/launcher/GFLauncher.java b/nucleus/admin/launcher/src/main/java/com/sun/enterprise/admin/launcher/GFLauncher.java index d57a9da323c..241166cd007 100644 --- a/nucleus/admin/launcher/src/main/java/com/sun/enterprise/admin/launcher/GFLauncher.java +++ b/nucleus/admin/launcher/src/main/java/com/sun/enterprise/admin/launcher/GFLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 2008, 2020 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -24,7 +24,6 @@ import com.sun.enterprise.universal.process.ProcessStreamDrainer; import com.sun.enterprise.universal.xml.MiniXmlParser; import com.sun.enterprise.universal.xml.MiniXmlParserException; -import com.sun.enterprise.util.OS; import com.sun.enterprise.util.io.FileUtils; import java.io.BufferedWriter; @@ -40,11 +39,12 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; +import java.util.ListIterator; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import static com.sun.enterprise.admin.launcher.GFLauncher.LaunchType.fake; import static com.sun.enterprise.admin.launcher.GFLauncherConstants.DEFAULT_LOGFILE; @@ -66,6 +66,9 @@ import static com.sun.enterprise.util.SystemPropertyConstants.JAVA_ROOT_PROPERTY; import static java.lang.Boolean.TRUE; import static java.lang.System.Logger.Level.INFO; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toList; @@ -176,7 +179,6 @@ public abstract class GFLauncher { * The process which is running GlassFish */ private Process glassFishProcess; - private ProcessWhacker processWhacker; private ProcessStreamDrainer processStreamDrainer; /** @@ -192,11 +194,11 @@ public abstract class GFLauncher { ////// PUBLIC api area starts here //////////////////////// /////////////////////////////////////////////////////////////////////////// + /** - * Launches the server. Any fatal error results in a GFLauncherException No unchecked Throwables of any kind will be - * thrown. + * Launches the server. * - * @throws com.sun.enterprise.admin.launcher.GFLauncherException + * @throws GFLauncherException if launch failed. */ public final void launch() throws GFLauncherException { if (isDebugSuspend()) { @@ -210,12 +212,10 @@ public final void launch() throws GFLauncherException { if (!setupCalledByClients) { setup(); } - // Typically invokes launchInstance() internalLaunch(); } catch (GFLauncherException gfe) { throw gfe; - } catch (Throwable t) { - // hk2 might throw a java.lang.Error + } catch (Exception t) { throw new GFLauncherException(strings.get("unknownError", t.getMessage()), t); } finally { GFLauncherLogger.removeLogFileHandler(); @@ -232,28 +232,6 @@ public final void relaunch() throws GFLauncherException { launch(); } - public final void launchJVM(List cmdsIn) throws GFLauncherException { - try { - setup(); // we only use one thing -- the java executable - List commands = new LinkedList<>(); - commands.add(javaExe); - - for (String cmd : cmdsIn) { - commands.add(cmd); - } - - ProcessBuilder processBuilder = new ProcessBuilder(commands); - Process process = processBuilder.start(); - ProcessStreamDrainer.drain("launchJVM", process); // just to be safe - } catch (GFLauncherException gfe) { - throw gfe; - } catch (Throwable t) { - // hk2 might throw a java.lang.Error - throw new GFLauncherException(strings.get("unknownError", t.getMessage()), t); - } finally { - GFLauncherLogger.removeLogFileHandler(); - } - } public void setup() throws GFLauncherException, MiniXmlParserException { asenvProps = getAsEnvConfReader().getProps(); @@ -444,7 +422,7 @@ void launchInstance() throws GFLauncherException { return; } - List cmds = null; + final List cmds; // Use launchctl bsexec on MacOS versions before 10.10 // otherwise use regular startup. @@ -464,23 +442,31 @@ void launchInstance() throws GFLauncherException { cmds.add("bsexec"); cmds.add("/"); cmds.addAll(getCommandLine()); - } else { + } else if (isWindows()) { + boolean preloadStdin = !getInfo().isVerbose() && isSSHSession(); + cmds = prepareWindowsEnvironment(getCommandLine(), getInfo().getConfigDir().toPath(), preloadStdin); + } else if (getInfo().isVerboseOrWatchdog()) { cmds = getCommandLine(); + } else { + // Usual usage on Linux based systems + cmds = new ArrayList<>(); + cmds.add("nohup"); + cmds.addAll(getCommandLine()); } + System.err.println("Executing: " + cmds.stream().collect(Collectors.joining(" "))); + System.err.println("Please look at the server log for more details..."); ProcessBuilder processBuilder = new ProcessBuilder(cmds); - // Change the directory if there is one specified, o/w stick with the - // default. + // Change the directory if there is one specified, o/w stick with the default. try { processBuilder.directory(getInfo().getConfigDir()); } catch (Exception e) { + throw new IllegalStateException(e); } // Run the glassFishProcess and attach Stream Drainers try { - closeStandardStreamsMaybe(); - // We have to abandon server.log file to avoid file locking issues on Windows. // From now on the server.log file is owned by the server, not by launcher. GFLauncherLogger.removeLogFileHandler(); @@ -634,7 +620,7 @@ private void parseJavaConfigDebugOptions() { } } - private void setLogFilename(MiniXmlParser domainXML) throws GFLauncherException { + private void setLogFilename(MiniXmlParser domainXML) { logFilename = domainXML.getLogFilename(); if (logFilename == null) { @@ -803,7 +789,7 @@ void setCommandLine() throws GFLauncherException { } } - void setJvmOptions() throws GFLauncherException { + void setJvmOptions() { domainXMLJvmOptionsAsList.clear(); if (domainXMLjvmOptions != null) { @@ -844,7 +830,6 @@ private void addIgnoreNull(List list, Collection ss) { private void wait(final Process p) throws GFLauncherException { try { - setShutdownHook(p); p.waitFor(); exitValue = p.exitValue(); } catch (InterruptedException ex) { @@ -852,30 +837,6 @@ private void wait(final Process p) throws GFLauncherException { } } - private void setShutdownHook(final Process p) { - // ON UNIX a ^C on the console will also kill DAS - // On Windows a ^C on the console will not kill DAS - // We want UNIX behavior on Windows - // note that the hook thread will run in both cases: - // 1. the server died on its own, e.g. with a stop-domain - // 2. a ^C (or equivalent signal) was received by the console - // note that exitValue is still set to -1 - - // if we are restarting we may get many many processes. - // Each time this method is called we reset the Process reference inside - // the processWhacker - - if (processWhacker == null) { - Runtime runtime = Runtime.getRuntime(); - final String msg = strings.get("serverStopped", callerParameters.getType()); - processWhacker = new ProcessWhacker(p, msg); - - runtime.addShutdownHook(new Thread(processWhacker, "GlassFish Process Whacker Shutdown Hook")); - } else { - processWhacker.setProcess(p); - } - } - private void setupProfilerAndJvmOptions(MiniXmlParser domainXML) throws MiniXmlParserException, GFLauncherException { // Add JVM options from Profiler *last* so they override config's JVM options domainXMLJavaConfigProfiler = new Profiler(domainXML.getProfilerConfig(), domainXML.getProfilerJvmOptions(), @@ -936,18 +897,18 @@ private void handleDeadProcess() throws GFLauncherException { } } - private String getDeadProcessTrace(Process sp) throws GFLauncherException { - // returns null in case the glassFishProcess is NOT dead - try { - int ev = sp.exitValue(); - ProcessStreamDrainer psd1 = getProcessStreamDrainer(); - String output = psd1.getOutErrString(); - String trace = strings.get("server_process_died", ev, output); - return trace; - } catch (IllegalThreadStateException e) { - // the glassFishProcess is still running and we are ok + /** @returns null in case the process is NOT dead or succeeded */ + private String getDeadProcessTrace(Process process) throws GFLauncherException { + if (process.isAlive()) { + return null; + } + int ev = process.exitValue(); + if (ev == 0) { return null; } + ProcessStreamDrainer psd1 = getProcessStreamDrainer(); + String output = psd1.getOutErrString(); + return strings.get("server_process_died", ev, output); } private void setupUpgradeSecurity() throws GFLauncherException { @@ -1027,11 +988,10 @@ private String getMonitoringAgentJvmOptionString() throws GFLauncherException { if (flashlightJarFile.isFile()) { return "javaagent:" + getCleanPath(flashlightJarFile); - } else { - String msg = strings.get("no_flashlight_agent", flashlightJarFile); - GFLauncherLogger.warning(GFLauncherLogger.NO_FLASHLIGHT_AGENT, flashlightJarFile); - throw new GFLauncherException(msg); } + String msg = strings.get("no_flashlight_agent", flashlightJarFile); + GFLauncherLogger.warning(GFLauncherLogger.NO_FLASHLIGHT_AGENT, flashlightJarFile); + throw new GFLauncherException(msg); } private static String getCleanPath(File f) { @@ -1081,69 +1041,112 @@ private void setupLogLevels() { } } - private void closeStandardStreamsMaybe() { - // see issue 12832 - // Windows bug/feature --> - // Say glassFishProcess A (ssh) creates Process B (asadmin start-instance ) - // which then fires up Process C (the instance). - // Process B exits but Process A does NOT. Process A is waiting for - // Process C to exit. - // The solution is to close down the standard streams BEFORE creating - // Process C. Then Process A becomes convinced that the glassFishProcess it created - // has finished. - // If there is a console that means the user is sitting at the terminal - // directly and we don't have to worry about it. - // Note that the issue is inside SSH -- not inside GF code per se. I.e. - // Process B absolutely positively does exit whether or not this code runs... - // don't run this unless we have to because our "..." messages disappear. - - if (System.console() == null && OS.isWindows() && !callerParameters.isVerboseOrWatchdog()) { - String sname; - - if (callerParameters.isDomain()) { - sname = callerParameters.getDomainName(); - } else { - sname = callerParameters.getInstanceName(); - } - System.out.println(strings.get("ssh", sname)); - try { - System.in.close(); - } catch (Exception e) { // ignore - } - try { - System.err.close(); - } catch (Exception e) { // ignore - } - try { - System.out.close(); - } catch (Exception e) { // ignore - } - } + private static boolean isWindows() { + return System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("win"); } - /////////////////////////////////////////////////////////////////////////// - private static class ProcessWhacker implements Runnable { - private final String message; - private Process process; + private static boolean isSSHSession() { + return System.getenv("SSH_CLIENT") != null || System.getenv("SSH_CONNECTION") != null + || System.getenv("SSH_TTY") != null; + } - ProcessWhacker(Process p, String msg) { - message = msg; - process = p; - } - void setProcess(Process p) { - process = p; + private static List prepareWindowsEnvironment(final List command, final Path configDir, + final boolean stdinPreloaded) throws GFLauncherException { + Path psFile; + Path batFile; + try { + batFile = configDir.resolve("gfstart.bat"); + psFile = configDir.resolve("gfstart.ps1"); + StringBuilder batContent = new StringBuilder(8192); + batContent.append("@echo off\n"); + ListIterator itemsOfCommand = command.listIterator(); + while (itemsOfCommand.hasNext()) { + String line = itemsOfCommand.next(); + if (!itemsOfCommand.hasPrevious()) { + batContent.append(" "); + } + // Java System options wrap just values and it should have been already done. + boolean wrap = line.contains(" ") && !line.startsWith("-D"); + if (wrap) { + batContent.append('"'); + } + batContent.append(line); + if (wrap) { + batContent.append('"'); + } + if (itemsOfCommand.hasNext()) { + batContent.append(" ^"); + } + batContent.append('\n'); + } + if (stdinPreloaded) { + batContent.append("< %1\n"); + } + Files.writeString(batFile, batContent, UTF_8, TRUNCATE_EXISTING, CREATE); + + StringBuilder psContent = new StringBuilder(8192); + psContent.append("param(\n"); + psContent.append(" [Parameter(Mandatory=$true)]\n"); + psContent.append(" [string]$BatchFilePath\n"); + psContent.append(")\n"); + + psContent.append("$pidFile = \"").append(new File(configDir.toFile(), "pid").getAbsolutePath()).append("\"\n"); + psContent.append("if (Test-Path $pidFile) {\n"); + psContent.append(" Remove-Item $pidFile -Force\n"); + psContent.append("}\n"); + if (stdinPreloaded) { + psContent.append("$stdin = [System.IO.StreamReader]::new([Console]::OpenStandardInput()).ReadToEnd()\n"); + psContent.append("$tempFile = [System.IO.Path]::GetTempFileName()\n"); + psContent.append("[System.IO.File]::WriteAllText($tempFile, $stdin)\n"); + } + if (!isSSHSession()) { + psContent.append("Start-Process -FilePath \"$BatchFilePath\" -NoNewWindow -PassThru"); + if (stdinPreloaded) { + psContent.append(" < $tempFile"); + } + psContent.append('\n'); + } else { + psContent.append("$action = New-ScheduledTaskAction -Execute \"cmd.exe\" -Argument \"/c $BatchFilePath"); + if (stdinPreloaded) { + psContent.append(" < $tempFile"); + } + psContent.append("\"\n"); + psContent.append("$taskName = \"GlassFishInstance_\" + [System.Guid]::NewGuid().ToString()\n"); + psContent.append("$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(1)\n"); + psContent.append("$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType S4U\n"); + psContent.append("$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Hours 0)\n"); + psContent.append("Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings\n"); + + psContent.append("Start-ScheduledTask -TaskName $taskName\n"); + psContent.append("Start-Sleep -Seconds 5\n"); + psContent.append("Unregister-ScheduledTask -TaskName $taskName -Confirm:$false\n"); + } + psContent.append("$start = Get-Date\n"); + psContent.append("$timeout = 60\n"); + psContent.append("while (-not (Test-Path $pidFile)) {\n"); + psContent.append(" if ((New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds -gt $timeout) {\n"); + psContent.append(" Write-Error \"Timeout waiting for GlassFish to start (pid file not created within $timeout seconds)\"\n"); + psContent.append(" exit 1\n"); + psContent.append(" }\n"); + psContent.append(" Start-Sleep -Seconds 1\n"); + psContent.append("}\n"); + + Files.writeString(psFile, psContent, UTF_8, TRUNCATE_EXISTING, CREATE); + } catch (IOException e) { + throw new GFLauncherException(e); } - - @Override - public void run() { - // we are in a shutdown hook -- most of the JVM is gone. - // logger won't work anymore... - System.out.println(message); - process.destroy(); + final List cmds = new ArrayList<>(); + cmds.add("powershell.exe"); + if (stdinPreloaded) { + cmds.add("-noninteractive"); } - + cmds.add("-File"); + cmds.add("\"" + psFile.toFile().getAbsolutePath() + "\""); + cmds.add("-BatchFilePath"); + cmds.add("\"" + batFile.toFile().getAbsolutePath() + "\""); + return cmds; } } diff --git a/nucleus/admin/launcher/src/main/java/com/sun/enterprise/admin/launcher/GFLauncherNativeHelper.java b/nucleus/admin/launcher/src/main/java/com/sun/enterprise/admin/launcher/GFLauncherNativeHelper.java index 559ad70f3f2..6ced15800d1 100644 --- a/nucleus/admin/launcher/src/main/java/com/sun/enterprise/admin/launcher/GFLauncherNativeHelper.java +++ b/nucleus/admin/launcher/src/main/java/com/sun/enterprise/admin/launcher/GFLauncherNativeHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * Copyright (c) 2024, 2025 Contributors to the Eclipse Foundation. * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -86,7 +86,7 @@ List getCommands() { // * junk is removed, e.g. ":xxx::yy::::::" goes to "xxx:yy" String finalPathString = GFLauncherUtils.fileListToPathString(GFLauncherUtils.stringToFiles(sb.toString())); - String nativeCommand = "-D" + JAVA_NATIVE_SYSPROP_NAME + "=" + finalPathString; + String nativeCommand = "-D" + JAVA_NATIVE_SYSPROP_NAME + "=\"" + finalPathString + "\""; list.add(nativeCommand); return list; } diff --git a/nucleus/admin/launcher/src/main/resources/com/sun/enterprise/admin/launcher/LocalStrings.properties b/nucleus/admin/launcher/src/main/resources/com/sun/enterprise/admin/launcher/LocalStrings.properties index d8e954e8734..4aa70990def 100644 --- a/nucleus/admin/launcher/src/main/resources/com/sun/enterprise/admin/launcher/LocalStrings.properties +++ b/nucleus/admin/launcher/src/main/resources/com/sun/enterprise/admin/launcher/LocalStrings.properties @@ -37,7 +37,6 @@ jvmfailure=JVM failed to start: {0} verboseInterruption=Got an InterruptedException while waiting for the Domain to stop in verbose or watchdog mode: {0} nobootjar=Can''t find required files: {0} launchTime=Successfully launched in {0} msec. -serverStopped=The {0} was stopped. UnknownJvmOptionFormat=I don''t understand the format of this jvm-option: {0} no_gfe_jar=You must specify the location of the Embedded GlassFish jar with an env. variable: set GFE_JAR=xxxx.jar no_quotes_allowed=Single and double quote characters are not allowed in the CLASSPATH environmental variable. \ @@ -50,9 +49,6 @@ invalid_process=Invalid call to getProcess before the process has been created. Wait a moment longer. Always call launch before calling this method. server_process_died=The server exited prematurely with exit code {0}.\nBefore it died, it produced the following output:\n\n{1} # -#'ssh' message from Sathyan Catari see IT 13862 October 19, 2010 -ssh=Attempting to start {0}.... Please look at the server log for more details..... - # issue 11665 copy_server_policy_error=Could not copy server.policy to domain. You may need to turn off the \ security manager before upgrading.\nCause: {0} diff --git a/nucleus/admin/server-mgmt/src/main/java/com/sun/enterprise/admin/servermgmt/V2ToV3ConfigUpgrade.java b/nucleus/admin/server-mgmt/src/main/java/com/sun/enterprise/admin/servermgmt/V2ToV3ConfigUpgrade.java index 22cb841c387..a1611839285 100644 --- a/nucleus/admin/server-mgmt/src/main/java/com/sun/enterprise/admin/servermgmt/V2ToV3ConfigUpgrade.java +++ b/nucleus/admin/server-mgmt/src/main/java/com/sun/enterprise/admin/servermgmt/V2ToV3ConfigUpgrade.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025 Contributors to the Eclipse Foundation * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -144,7 +145,7 @@ private boolean ok(String s) { // these are added to all configs private static final String[] ADD_LIST = new String[] { "-XX:+UnlockDiagnosticVMOptions", "-XX:+LogVMOutput", "-XX:LogFile=${com.sun.aas.instanceRoot}/logs/jvm.log", "-Djava.awt.headless=true", "-DANTLR_USE_DIRECT_CLASS_LOADING=true", - "-Dosgi.shell.telnet.maxconn=1", "-Dosgi.shell.telnet.ip=127.0.0.1", "-Dgosh.args=--noshutdown -c noop=true", + "-Dosgi.shell.telnet.maxconn=1", "-Dosgi.shell.telnet.ip=127.0.0.1", "-Dgosh.args=--nointeractive", "-Dfelix.fileinstall.dir=${com.sun.aas.installRoot}/modules/autostart/", "-Dfelix.fileinstall.poll=5000", "-Dfelix.fileinstall.debug=3", "-Dfelix.fileinstall.bundles.new.start=true", "-Dfelix.fileinstall.bundles.startTransient=true", "-Dfelix.fileinstall.disableConfigSave=false", "-Dfelix.fileinstall.log.level=2", diff --git a/nucleus/admin/server-mgmt/src/main/java/com/sun/enterprise/admin/servermgmt/cli/StartServerHelper.java b/nucleus/admin/server-mgmt/src/main/java/com/sun/enterprise/admin/servermgmt/cli/StartServerHelper.java index 13c39684b87..97d55b91f6a 100644 --- a/nucleus/admin/server-mgmt/src/main/java/com/sun/enterprise/admin/servermgmt/cli/StartServerHelper.java +++ b/nucleus/admin/server-mgmt/src/main/java/com/sun/enterprise/admin/servermgmt/cli/StartServerHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -44,6 +44,7 @@ import static com.sun.enterprise.admin.cli.CLIConstants.WAIT_FOR_DAS_TIME_MS; import static com.sun.enterprise.util.StringUtils.ok; import static java.lang.System.Logger.Level.DEBUG; +import static java.lang.System.Logger.Level.INFO; import static java.lang.System.Logger.Level.WARNING; /** @@ -150,6 +151,12 @@ public void waitForServerStart() throws CommandException { final String serverName = info.isDomain() ? "domain " + info.getDomainName() : "instance " + info.getInstanceName(); + if (exitCode == 0) { + LOG.log(INFO, + "Server {0} started successfuly. The startup command produced following output before it finished: \n{1}", + serverName, output); + return; + } if (ok(output)) { throw new CommandException(I18N.get("serverDiedOutput", serverName, exitCode, output)); } @@ -186,7 +193,7 @@ public void report() { } else { adminPort = addresses.get(0).getPort(); } - LOG.log(Level.INFO, "ServerStart.SuccessMessage", info.isDomain() ? "domain " : "instance", + LOG.log(INFO, "ServerStart.SuccessMessage", info.isDomain() ? "domain " : "instance", serverDirs.getServerName(), serverDirs.getServerDir(), logfile, adminPort); } diff --git a/nucleus/admin/template/src/main/resources/config/domain.xml b/nucleus/admin/template/src/main/resources/config/domain.xml index 7383a811599..ee5361b107d 100644 --- a/nucleus/admin/template/src/main/resources/config/domain.xml +++ b/nucleus/admin/template/src/main/resources/config/domain.xml @@ -179,7 +179,7 @@ --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED --add-opens=jdk.management/com.sun.management.internal=ALL-UNNAMED --add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED - + -Djdk.attach.allowAttachSelf=true @@ -306,7 +306,7 @@ -Dosgi.shell.telnet.ip=127.0.0.1 - -Dgosh.args=--noshutdown -c noop=true + -Dgosh.args=--nointeractive -Dfelix.fileinstall.dir=${com.sun.aas.installRoot}/modules/autostart/ diff --git a/nucleus/admin/template/src/main/resources/config/logging.properties b/nucleus/admin/template/src/main/resources/config/logging.properties index fbc3d7fe202..38d265e4717 100644 --- a/nucleus/admin/template/src/main/resources/config/logging.properties +++ b/nucleus/admin/template/src/main/resources/config/logging.properties @@ -60,6 +60,8 @@ org.glassfish.main.jul.handler.SyslogHandler.port=514 .level=INFO systemRootLogger.level=INFO +com.jcraft.level=INFO + com.sun.enterprise.admin.level=INFO com.sun.enterprise.connectors.level=INFO com.sun.enterprise.container.level=INFO @@ -198,6 +200,7 @@ org.glassfish.admingui.level=INFO org.glassfish.apf.level=INFO org.glassfish.api.level=INFO org.glassfish.api.invocation.level=INFO +org.glassfish.cluster.level=INFO org.glassfish.concurrent.level=INFO org.glassfish.concurro.level=INFO org.glassfish.exousia.level=INFO diff --git a/nucleus/admin/util/src/main/java/com/sun/enterprise/admin/util/JvmOptionsHelper.java b/nucleus/admin/util/src/main/java/com/sun/enterprise/admin/util/JvmOptionsHelper.java index 89927e1e014..bf5bc28f557 100644 --- a/nucleus/admin/util/src/main/java/com/sun/enterprise/admin/util/JvmOptionsHelper.java +++ b/nucleus/admin/util/src/main/java/com/sun/enterprise/admin/util/JvmOptionsHelper.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025 Contributors to the Eclipse Foundation * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -77,13 +78,13 @@ public String[] addJvmOptions(String[] options) throws InvalidJvmOptionException } final Set alreadyExist = new HashSet(); JvmOptionsElement last = last(); - for (int i = 0; i < options.length; i++) { - if (!head.hasOption(options[i])) { - JvmOptionsElement x = new JvmOptionsElement(options[i]); + for (String option : options) { + if (!head.hasOption(option)) { + JvmOptionsElement x = new JvmOptionsElement(option); last.setNext(x); last = x; } else { - alreadyExist.add(options[i]); + alreadyExist.add(option); } } return toStringArray(alreadyExist); @@ -114,9 +115,9 @@ public String[] deleteJvmOptions(String[] options) { } final Set donotExist = new HashSet(); - for (int i = 0; i < options.length; i++) { - if (!head.deleteJvmOption(options[i])) { - donotExist.add(options[i]); + for (String option : options) { + if (!head.deleteJvmOption(option)) { + donotExist.add(option); } } return toStringArray(donotExist); @@ -208,6 +209,7 @@ void setNext(JvmOptionsElement element) { throw new UnsupportedOperationException(); } }; + private final Set jvmOptions = new LinkedHashSet(); private JvmOptionsElement next; @@ -232,21 +234,11 @@ private JvmOptionsElement() { if (null == options) { throw new IllegalArgumentException(); } - //Need to exclude the gogo shell args that was added for issue 14173. - //Otherwise performance tuner breaks saying that noop=true is not valid - //because it does not begin with a '-'. - String gogoArgs = "-Dgosh.args=--noshutdown -c noop=true"; - if (!options.equals(gogoArgs)) { - //4923404 - QuotedStringTokenizer strTok = new QuotedStringTokenizer(options, " \t"); - //4923404 - while (strTok.hasMoreTokens()) { - String option = strTok.nextToken(); - checkValidOption(option); - jvmOptions.add(option); - } - } else { - jvmOptions.add(gogoArgs); + QuotedStringTokenizer strTok = new QuotedStringTokenizer(options, " \t"); + while (strTok.hasMoreTokens()) { + String option = strTok.nextToken(); + checkValidOption(option); + jvmOptions.add(option); } next = DEFAULT; } @@ -297,7 +289,7 @@ String getJvmOptionsAsStoredInXml() { if (jvmOptions.isEmpty()) { return ""; } - final StringBuffer sb = new StringBuffer(); + final StringBuilder sb = new StringBuilder(); final Iterator it = jvmOptions.iterator(); while (it.hasNext()) { sb.append(it.next()); diff --git a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/CreateInstanceCommand.java b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/CreateInstanceCommand.java index c2fb8df5cd9..de1732cbd98 100644 --- a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/CreateInstanceCommand.java +++ b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/CreateInstanceCommand.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025 Contributors to the Eclipse Foundation * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -13,7 +14,6 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 */ - package com.sun.enterprise.v3.admin.cluster; import com.sun.enterprise.config.serverbeans.Domain; @@ -22,15 +22,18 @@ import com.sun.enterprise.config.serverbeans.Server; import com.sun.enterprise.config.serverbeans.Servers; import com.sun.enterprise.universal.glassfish.ASenvPropertyReader; -import com.sun.enterprise.util.ExceptionUtil; import com.sun.enterprise.util.StringUtils; import com.sun.enterprise.util.SystemPropertyConstants; import com.sun.enterprise.util.io.InstanceDirs; +import com.sun.enterprise.v3.admin.cluster.SecureAdminBootstrapHelper.BootstrapException; import jakarta.inject.Inject; import java.io.File; import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -53,10 +56,10 @@ import org.glassfish.api.admin.RestEndpoints; import org.glassfish.api.admin.RuntimeType; import org.glassfish.api.admin.ServerEnvironment; +import org.glassfish.cluster.ssh.launcher.SSHLauncher; import org.glassfish.hk2.api.IterableProvider; import org.glassfish.hk2.api.PerLookup; import org.glassfish.hk2.api.ServiceLocator; -import org.glassfish.internal.api.ServerContext; import org.jvnet.hk2.annotations.Service; /** @@ -91,8 +94,6 @@ public class CreateInstanceCommand implements AdminCommand { private Servers servers; @Inject private ServerEnvironment env; - @Inject - private ServerContext serverContext; @Param(name = "node", alias = "nodeagent") String node; @Param(name = "config", optional = true) @@ -231,12 +232,11 @@ private void validateInstanceDirUnique(ActionReport report, AdminCommandContext Properties pro = listInstances.report().getExtraProperties(); if (pro != null){ List instanceList = (List) pro.get("instanceList"); - if (instanceList == null) + if (instanceList == null) { return; + } for (HashMap instanceMap : instanceList) { - final File nodeDirFile = (nodeDir != null - ? new File(nodeDir) - : defaultLocalNodeDirFile()); + final File nodeDirFile = nodeDir == null ? defaultLocalNodeDirFile() : new File(nodeDir); File instanceDir = new File(new File(nodeDirFile.toString(), theNode.getName()), instance); String instanceName = (String)instanceMap.get("name"); File instanceListDir = new File(new File(nodeDirFile.toString(), theNode.getName()), instance); @@ -251,30 +251,6 @@ private void validateInstanceDirUnique(ActionReport report, AdminCommandContext } } - /** - * Returns the directory for the selected instance that is on the local - * system. - * @param instanceName name of the instance - * @return File for the local file system location of the instance directory - * @throws IOException - */ - private File getLocalInstanceDir() throws IOException { - /* - * Pass the node directory parent and the node directory name explicitly - * or else InstanceDirs will not work as we want if there are multiple - * nodes registered on this node. - * - * If the configuration recorded an explicit directory for the node, - * then use it. Otherwise, use the default node directory of - * ${installDir}/glassfish/nodes/${nodeName}. - */ - final File nodeDirFile = (nodeDir != null - ? new File(nodeDir) - : defaultLocalNodeDirFile()); - InstanceDirs instanceDirs = new InstanceDirs(nodeDirFile.toString(), theNode.getName(), instance); - return instanceDirs.getInstanceDir(); - } - private File defaultLocalNodeDirFile() { final Map systemProps = Collections.unmodifiableMap(new ASenvPropertyReader().getProps()); @@ -293,119 +269,71 @@ private File getDomainInstanceDir() { return env.getInstanceRoot(); } - /** - * - * Delivers bootstrap files for secure admin locally, because the instance - * is on the same system as the DAS (and therefore on the same system where - * this command is running). - * - * @return 0 if successful, 1 otherwise - */ - private int bootstrapSecureAdminLocally() { - final ActionReport report = ctx.getActionReport(); - - try { - final SecureAdminBootstrapHelper bootHelper = - SecureAdminBootstrapHelper.getLocalHelper( - env.getInstanceRoot(), - getLocalInstanceDir()); - bootHelper.bootstrapInstance(); - bootHelper.close(); - return 0; - } - catch (IOException ex) { - return reportFailure(ex, report); - } - catch (SecureAdminBootstrapHelper.BootstrapException ex) { - return reportFailure(ex, report); - } - } - - private int reportFailure(final Exception ex, final ActionReport report) { - String msg = Strings.get("create.instance.local.boot.failed", instance, node, nodeHost); - logger.log(Level.SEVERE, msg, ex); - report.setActionExitCode(ActionReport.ExitCode.FAILURE); - report.setMessage(msg); - return 1; - } - - /** - * Delivers bootstrap files for secure admin remotely, because the instance - * is NOT on the same system as the DAS. - * - * @return 0 if successful; 1 otherwise - */ - private int bootstrapSecureAdminRemotely() { + private void createInstanceFilesystem(AdminCommandContext context) { ActionReport report = ctx.getActionReport(); - // nodedir is the root of where all the node dirs will be created. - // add the name of the node as that is where the instance files should be created - String thisNodeDir = null; - if (nodeDir != null) - thisNodeDir = nodeDir + "/" + node; + report.setActionExitCode(ActionReport.ExitCode.SUCCESS); + try { - final SecureAdminBootstrapHelper bootHelper = - SecureAdminBootstrapHelper.getRemoteHelper( - habitat, - getDomainInstanceDir(), - thisNodeDir, - instance, - theNode, logger); - bootHelper.bootstrapInstance(); - bootHelper.close(); - return 0; - } - catch (Exception ex) { - String exmsg = ex.getMessage(); - if (exmsg == null) { - // The root cause message is better than no message at all - exmsg = ExceptionUtil.getRootCause(ex).toString(); + Server dasServer = servers.getServer(SystemPropertyConstants.DAS_SERVER_NAME); + final SSHLauncher sshL = theNode.isLocal() ? null : new SSHLauncher(theNode); + List command = generateCommand(dasServer, sshL); + String humanCommand = makeCommandHuman(command); + if (userManagedNodeType()) { + String msg = Strings.get("create.instance.config", instance, humanCommand); + msg = StringUtils.cat(NL, registerInstanceMessage, msg); + report.setMessage(msg); + return; } - String msg = Strings.get( - "create.instance.remote.boot.failed", - instance, - + // First error message displayed if we fail + String firstErrorMessage = Strings.get("create.instance.filesystem.failed", instance, node, nodeHost); - // DCOMFIX - (ex instanceof SecureAdminBootstrapHelper.BootstrapException - ? ((SecureAdminBootstrapHelper.BootstrapException) ex).sshSettings() : null), - exmsg, - + // Run the command on the node and handle errors. + NodeUtils nodeUtils = new NodeUtils(habitat); + StringBuilder output = new StringBuilder(); + nodeUtils.runAdminCommandOnNode(theNode, command, ctx, firstErrorMessage, humanCommand, output); + if (report.getActionExitCode() != ActionReport.ExitCode.SUCCESS) { + // something went wrong with the nonlocal command don't continue but set status to + // warning because config was updated correctly or we would not be here. + report.setActionExitCode(ActionReport.ExitCode.WARNING); + return; + } + // If it was successful say so and display the command output + String msg = Strings.get("create.instance.success", instance, nodeHost); + if (!terse) { + msg = StringUtils.cat(NL, output.toString().trim(), registerInstanceMessage, msg); + } + report.setMessage(msg); - nodeHost); - logger.log(Level.SEVERE, msg, ex); + try (SecureAdminBootstrapHelper bootstrapHelper = createBootstrapHelper(sshL)) { + bootstrapHelper.bootstrapInstance(); + } + } catch (IOException | BootstrapException e) { + String message = Strings.get("create.instance.boot.failed", instance, node, e.getMessage()); + logger.log(Level.SEVERE, message, e); report.setActionExitCode(ActionReport.ExitCode.FAILURE); - report.setMessage(msg); - return 1; + report.setMessage(message); + } + if (report.getActionExitCode() != ActionReport.ExitCode.SUCCESS) { + // something went wrong with the nonlocal command don't continue but set status to + // warning because config was updated correctly or we would not be here. + report.setActionExitCode(ActionReport.ExitCode.WARNING); } } - private void createInstanceFilesystem(AdminCommandContext context) { - ActionReport report = ctx.getActionReport(); - report.setActionExitCode(ActionReport.ExitCode.SUCCESS); - - NodeUtils nodeUtils = new NodeUtils(habitat, logger); - Server dasServer = - servers.getServer(SystemPropertyConstants.DAS_SERVER_NAME); - String dasHost = dasServer.getAdminHost(); - String dasPort = Integer.toString(dasServer.getAdminPort()); - - ArrayList command = new ArrayList(); - String humanCommand = null; + private List generateCommand(Server dasServer, SSHLauncher sshL) throws BootstrapException { + List command = new ArrayList<>(); if (!theNode.isLocal()) { - // Only specify the DAS host if the node is remote. See issue 13993 command.add("--host"); - command.add(dasHost); + command.add(resolveAdminHost(sshL)); } - command.add("--port"); - command.add(dasPort); + command.add(Integer.toString(dasServer.getAdminPort())); command.add("_create-instance-filesystem"); - if (nodeDir != null) { command.add("--nodedir"); command.add(StringUtils.quotePathIfNecessary(nodeDir)); @@ -413,57 +341,31 @@ private void createInstanceFilesystem(AdminCommandContext context) { command.add("--node"); command.add(node); - command.add(instance); + return command; + } - humanCommand = makeCommandHuman(command); - if (userManagedNodeType()) { - String msg = Strings.get("create.instance.config", - instance, humanCommand); - msg = StringUtils.cat(NL, registerInstanceMessage, msg); - report.setMessage(msg); - return; - } - - // First error message displayed if we fail - String firstErrorMessage = Strings.get("create.instance.filesystem.failed", - instance, node, nodeHost); - - StringBuilder output = new StringBuilder(); - - // Run the command on the node and handle errors. - nodeUtils.runAdminCommandOnNode(theNode, command, ctx, firstErrorMessage, - humanCommand, output); - - if (report.getActionExitCode() != ActionReport.ExitCode.SUCCESS) { - // something went wrong with the nonlocal command don't continue but set status to warning - // because config was updated correctly or we would not be here. - report.setActionExitCode(ActionReport.ExitCode.WARNING); - return; - } - - // If it was successful say so and display the command output - String msg = Strings.get("create.instance.success", - instance, nodeHost); - if (!terse) { - msg = StringUtils.cat(NL, - output.toString().trim(), registerInstanceMessage, msg); - } - report.setMessage(msg); - // Bootstrap secure admin files + private SecureAdminBootstrapHelper createBootstrapHelper(SSHLauncher sshL) throws IOException, BootstrapException { if (theNode.isLocal()) { - bootstrapSecureAdminLocally(); - } - else { - bootstrapSecureAdminRemotely(); - } - if (report.getActionExitCode() != ActionReport.ExitCode.SUCCESS) { - - // something went wrong with the nonlocal command don't continue but set status to warning - // because config was updated correctly or we would not be here. - report.setActionExitCode(ActionReport.ExitCode.WARNING); + /* + * Pass the node directory parent and the node directory name explicitly + * or else InstanceDirs will not work as we want if there are multiple + * nodes registered on this node. + * + * If the configuration recorded an explicit directory for the node, + * then use it. Otherwise, use the default node directory of + * ${installDir}/glassfish/nodes/${nodeName}. + */ + final File nodeDirFile = nodeDir == null ? defaultLocalNodeDirFile() : new File(nodeDir); + final File localInstanceDir = new InstanceDirs(nodeDirFile.toString(), theNode.getName(), instance) + .getInstanceDir(); + return SecureAdminBootstrapHelper.getLocalHelper(env.getInstanceRoot(), localInstanceDir); } + // nodedir is the root of where all the node dirs will be created. + // add the name of the node as that is where the instance files should be created + String thisNodeDir = nodeDir == null ? null : (nodeDir + "/" + node); + return SecureAdminBootstrapHelper.getRemoteHelper(sshL, getDomainInstanceDir(), thisNodeDir, instance, theNode); } /** @@ -476,9 +378,8 @@ private boolean validateDasOptions(AdminCommandContext context) { ActionReport report = ctx.getActionReport(); report.setActionExitCode(ActionReport.ExitCode.SUCCESS); - NodeUtils nodeUtils = new NodeUtils(habitat, logger); - Server dasServer = - servers.getServer(SystemPropertyConstants.DAS_SERVER_NAME); + NodeUtils nodeUtils = new NodeUtils(habitat); + Server dasServer = servers.getServer(SystemPropertyConstants.DAS_SERVER_NAME); String dasHost = dasServer.getAdminHost(); String dasPort = Integer.toString(dasServer.getAdminPort()); @@ -541,12 +442,24 @@ private String makeCommandHuman(List command) { // verbose but very readable... private boolean userManagedNodeType() { - if(theNode.isLocal()) + if (theNode.isLocal()) { return false; + } - if(theNode.getType().equals("SSH")) + if (theNode.getType().equals("SSH")) { return false; + } return true; } + + + private String resolveAdminHost(SSHLauncher sshLauncher) throws BootstrapException { + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress(InetAddress.getByName(sshLauncher.getHost()), sshLauncher.getPort())); + return socket.getLocalAddress().getHostName(); + } catch (IOException e) { + throw new BootstrapException("Failed to resolve the admin host visible from the remote node " + node, e); + } + } } diff --git a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/CreateNodeConfigCommand.java b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/CreateNodeConfigCommand.java index bceadb2c04e..e519d9df982 100644 --- a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/CreateNodeConfigCommand.java +++ b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/CreateNodeConfigCommand.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025 Contributors to the Eclipse Foundation * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -24,6 +25,7 @@ import jakarta.inject.Inject; import java.io.File; +import java.nio.file.Path; import java.util.HashMap; import java.util.Map; @@ -86,15 +88,12 @@ public void execute(AdminCommandContext context) { TokenResolver resolver = null; // Create a resolver that can replace system properties in strings - Map systemPropsMap = - new HashMap((Map)(System.getProperties())); + Map systemPropsMap = new HashMap((Map) (System.getProperties())); resolver = new TokenResolver(systemPropsMap); - String resolvedInstallDir = resolver.resolve(installdir); - File actualInstallDir = new File( resolvedInstallDir+"/" + NodeUtils.LANDMARK_FILE); - - - if (!actualInstallDir.exists()){ - report.setMessage(Strings.get("invalid.installdir",installdir)); + Path resolvedInstallDir = new File(resolver.resolve(installdir)).toPath(); + Path actualInstallDir = resolvedInstallDir.resolve(NodeUtils.LANDMARK_FILE); + if (!actualInstallDir.toFile().exists()) { + report.setMessage(Strings.get("invalid.installdir", installdir)); report.setActionExitCode(ActionReport.ExitCode.FAILURE); return; } @@ -103,12 +102,15 @@ public void execute(AdminCommandContext context) { CommandInvocation ci = cr.getCommandInvocation("_create-node", report, context.getSubject()); ParameterMap map = new ParameterMap(); map.add("DEFAULT", name); - if (StringUtils.ok(nodedir)) + if (StringUtils.ok(nodedir)) { map.add(NodeUtils.PARAM_NODEDIR, nodedir); - if (StringUtils.ok(installdir)) + } + if (StringUtils.ok(installdir)) { map.add(NodeUtils.PARAM_INSTALLDIR, installdir); - if (StringUtils.ok(nodehost)) + } + if (StringUtils.ok(nodehost)) { map.add(NodeUtils.PARAM_NODEHOST, nodehost); + } map.add(NodeUtils.PARAM_TYPE,"CONFIG"); ci.parameters(map); ci.execute(); diff --git a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/CreateNodeSshCommand.java b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/CreateNodeSshCommand.java index f25e5cef5dd..0a8bcdf3034 100644 --- a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/CreateNodeSshCommand.java +++ b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/CreateNodeSshCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -34,6 +34,7 @@ import org.glassfish.api.admin.RestEndpoint; import org.glassfish.api.admin.RestEndpoints; import org.glassfish.api.admin.RuntimeType; +import org.glassfish.cluster.ssh.launcher.SSHLauncher; import org.glassfish.cluster.ssh.util.SSHUtil; import org.glassfish.hk2.api.PerLookup; import org.jvnet.hk2.annotations.Service; @@ -145,11 +146,10 @@ protected final void populateCommandArgs(List args) { @Override protected List getPasswords() { List list = new ArrayList<>(); - NodeUtils nUtils = new NodeUtils(habitat, logger); - list.add("AS_ADMIN_SSHPASSWORD=" + nUtils.sshL.expandPasswordAlias(remotePassword)); + list.add("AS_ADMIN_SSHPASSWORD=" + SSHLauncher.expandPasswordAlias(remotePassword)); if (sshkeypassphrase != null) { - list.add("AS_ADMIN_SSHKEYPASSPHRASE=" + nUtils.sshL.expandPasswordAlias(sshkeypassphrase)); + list.add("AS_ADMIN_SSHKEYPASSPHRASE=" + SSHLauncher.expandPasswordAlias(sshkeypassphrase)); } return list; } diff --git a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/CreateRemoteNodeCommand.java b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/CreateRemoteNodeCommand.java index 9a43f9ab5a1..7f883b2cd20 100644 --- a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/CreateRemoteNodeCommand.java +++ b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/CreateRemoteNodeCommand.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025 Contributors to the Eclipse Foundation * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -13,7 +14,6 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 */ - package com.sun.enterprise.v3.admin.cluster; import com.sun.enterprise.config.serverbeans.Nodes; @@ -53,7 +53,7 @@ public abstract class CreateRemoteNodeCommand implements AdminCommand { @Inject private CommandRunner cr; @Inject - ServiceLocator habitat; + ServiceLocator locator; @Inject Nodes nodes; @Param(name = "name", primary = true) @@ -112,7 +112,7 @@ public final void executeInternal(AdminCommandContext context) { } try { - nodeUtils = new NodeUtils(habitat, logger); + nodeUtils = new NodeUtils(locator); nodeUtils.validate(map); if (install) { boolean s = installNode(context); @@ -261,18 +261,20 @@ final int execCommand(List cmdLine, StringBuilder output) { fullcommand.addAll(cmdLine); ProcessManager pm = new ProcessManager(fullcommand); - if (!pass.isEmpty()) + if (!pass.isEmpty()) { pm.setStdinLines(pass); + } if (logger.isLoggable(Level.INFO)) { logger.info("Running command on DAS: " + commandListToString(fullcommand)); } pm.setTimeoutMsec(DEFAULT_TIMEOUT_MSEC); - if (logger.isLoggable(Level.FINER)) + if (logger.isLoggable(Level.FINER)) { pm.setEcho(true); - else + } else { pm.setEcho(false); + } try { exit = pm.execute(); diff --git a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/DeleteInstanceCommand.java b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/DeleteInstanceCommand.java index c98d8027dbf..4d2a70a59c1 100644 --- a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/DeleteInstanceCommand.java +++ b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/DeleteInstanceCommand.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025 Contributors to the Eclipse Foundation * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -13,7 +14,6 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 */ - package com.sun.enterprise.v3.admin.cluster; import com.sun.enterprise.config.serverbeans.Node; @@ -71,7 +71,7 @@ public class DeleteInstanceCommand implements AdminCommand { private CommandRunner cr; @Inject - ServiceLocator habitat; + ServiceLocator locator; @Inject private Servers servers; @@ -187,7 +187,7 @@ public void execute(AdminCommandContext ctx) { private void deleteInstanceFilesystem(AdminCommandContext ctx) { - NodeUtils nodeUtils = new NodeUtils(habitat, logger); + NodeUtils nodeUtils = new NodeUtils(locator); ArrayList command = new ArrayList(); String humanCommand = null; diff --git a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/DeleteNodeSshCommand.java b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/DeleteNodeSshCommand.java index 535c66c4a0b..a44893bd7ab 100644 --- a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/DeleteNodeSshCommand.java +++ b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/DeleteNodeSshCommand.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025 Contributors to the Eclipse Foundation * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -13,7 +14,6 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 */ - package com.sun.enterprise.v3.admin.cluster; import com.sun.enterprise.config.serverbeans.Nodes; @@ -28,13 +28,14 @@ import org.glassfish.api.admin.RestEndpoint; import org.glassfish.api.admin.RestEndpoints; import org.glassfish.api.admin.RuntimeType; +import org.glassfish.cluster.ssh.launcher.SSHLauncher; import org.glassfish.hk2.api.PerLookup; import org.jvnet.hk2.annotations.Service; /** * Remote AdminCommand to create a config node. This command is run only on DAS. - * Register the config node on DAS + * Register the config node on DAS * * @author Carla Mott */ @@ -59,12 +60,10 @@ public final void execute(AdminCommandContext context) { */ @Override protected final List getPasswords() { - List list = new ArrayList(); - NodeUtils nodeUtils = new NodeUtils(habitat, logger); - list.add("AS_ADMIN_SSHPASSWORD=" + nodeUtils.sshL.expandPasswordAlias(remotepassword)); - + List list = new ArrayList<>(); + list.add("AS_ADMIN_SSHPASSWORD=" + SSHLauncher.expandPasswordAlias(remotepassword)); if (sshkeypassphrase != null) { - list.add("AS_ADMIN_SSHKEYPASSPHRASE=" + nodeUtils.sshL.expandPasswordAlias(sshkeypassphrase)); + list.add("AS_ADMIN_SSHKEYPASSPHRASE=" + SSHLauncher.expandPasswordAlias(sshkeypassphrase)); } return list; } diff --git a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/LocalStrings.properties b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/LocalStrings.properties index 152f63b41cb..cd771ade30a 100644 --- a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/LocalStrings.properties +++ b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/LocalStrings.properties @@ -133,8 +133,7 @@ cluster.command.noInstances=The cluster {0} contains no instances. cluster.command.interrupted=Command for cluster {0} was interrupted after \ getting responses from {1} of {2} instances for command {3}. cluster.command.executing=Executing {0} on {1} instances. -create.instance.local.boot.failed=Successfully created instance {0} in the DAS configuration, but failed to retrieve configuration files during bootstrap. -create.instance.remote.boot.failed=Successfully created instance {0} in the DAS configuration, but failed to install configuration files for the instance on node {3} during bootstrap.\n\nSSH configuration information \n{1}\n Additional failure info: {2}. +create.instance.boot.failed=Successfully created instance {0} on node {1} in the DAS configuration, but failed to finish the instance bootstrap. Additional details: {2} deleting.instance=Deleting instance {0} on {1} instance.shutdown=Instance {0} must be stopped before it can be deleted. @@ -206,7 +205,7 @@ update.node.ssh.not.updated=Node not updated. To force an update of the \ update.node.config.missing.attribute={1} attribute is required to update node {0}. update.node.config.defaultnode=Cannot update node {0}. It is the built-in localhost node. node.ssh.invalid.params=Warning: some parameters appear to be invalid. -ssh.bad.connect=Could not connect to host {0} using {1}. +ssh.bad.connect=Could not connect to host {0} using {1}. Cause: {2} ssh.invalid.port=Invalid port number {0}. key.path.not.absolute=Key file path {0} must be an absolute path. key.path.not.found=Key file {0} not found. The key file path must be reachable by the DAS. diff --git a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/NodeUtils.java b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/NodeUtils.java index cbaf592102f..19a52e27d8e 100644 --- a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/NodeUtils.java +++ b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/NodeUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -28,15 +28,14 @@ import com.sun.enterprise.util.net.NetUtils; import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; +import java.lang.System.Logger; import java.net.InetAddress; import java.net.UnknownHostException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.logging.Logger; import org.glassfish.api.ActionReport; import org.glassfish.api.admin.AdminCommandContext; @@ -44,10 +43,15 @@ import org.glassfish.api.admin.ParameterMap; import org.glassfish.api.admin.SSHCommandExecutionException; import org.glassfish.cluster.ssh.connect.NodeRunner; +import org.glassfish.cluster.ssh.launcher.SSHException; import org.glassfish.cluster.ssh.launcher.SSHLauncher; +import org.glassfish.common.util.admin.AuthTokenManager; import org.glassfish.hk2.api.ServiceLocator; import org.glassfish.internal.api.RelativePathResolver; +import static java.lang.System.Logger.Level.INFO; +import static java.lang.System.Logger.Level.WARNING; + /** * Utility methods for operating on Nodes * @@ -55,6 +59,8 @@ * @author Byron Nevins */ public class NodeUtils { + private static final Logger LOG = System.getLogger(NodeUtils.class.getName()); + public static final String NODE_DEFAULT_SSH_PORT = "22"; public static final String NODE_DEFAULT_REMOTE_USER = "${user.name}"; static final String NODE_DEFAULT_INSTALLDIR = "${com.sun.aas.productRoot}"; @@ -71,22 +77,18 @@ public class NodeUtils { static final String PARAM_TYPE = "type"; static final String PARAM_INSTALL = "install"; public static final String PARAM_WINDOWS_DOMAIN = "windowsdomain"; - static final String LANDMARK_FILE = "glassfish/modules/admin-cli.jar"; + static final Path LANDMARK_FILE = Path.of("glassfish", "modules", "admin-cli.jar"); private static final String NL = System.lineSeparator(); private TokenResolver resolver = null; - private Logger logger = null; - private ServiceLocator habitat = null; - SSHLauncher sshL = null; + private ServiceLocator locator = null; - NodeUtils(ServiceLocator habitat, Logger logger) { - this.logger = logger; - this.habitat = habitat; + NodeUtils(ServiceLocator locator) { + this.locator = locator; // Create a resolver that can replace system properties in strings Map systemPropsMap = new HashMap((Map) (System.getProperties())); resolver = new TokenResolver(systemPropsMap); - sshL = habitat.getService(SSHLauncher.class); } static boolean isSSHNode(Node node) { @@ -110,7 +112,7 @@ String getGlassFishVersionOnNode(Node node, AdminCommandContext context) throws command.add("version"); command.add("--local"); command.add("--terse"); - NodeRunner nr = new NodeRunner(habitat, logger); + NodeRunner nr = new NodeRunner(locator.getService(AuthTokenManager.class)); StringBuilder output = new StringBuilder(); try { @@ -118,11 +120,9 @@ String getGlassFishVersionOnNode(Node node, AdminCommandContext context) throws if (commandStatus != 0) { return "unknown version: " + output.toString(); } - } - catch (Exception e) { - throw new CommandValidationException( - Strings.get("failed.to.run", command.toString(), - node.getNodeHost()), e); + } catch (Exception e) { + throw new CommandValidationException(Strings.get("failed.to.run", command.toString(), node.getNodeHost()), + e); } return output.toString().trim(); } @@ -184,11 +184,6 @@ private void validateRemote(ParameterMap map, String nodehost) throws return; } - // BN says: Shouldn't this be a fatal error?!? TODO - if (sshL == null) { - return; - } - validateRemoteConnection(map); } @@ -249,20 +244,14 @@ private void validatePassword(String p) throws CommandValidationException { if (StringUtils.ok(p)) { try { expandedPassword = RelativePathResolver.getRealPasswordFromAlias(p); - } - catch (IllegalArgumentException e) { - throw new CommandValidationException( - Strings.get("no.such.password.alias", p)); - } - catch (Exception e) { - throw new CommandValidationException( - Strings.get("no.such.password.alias", p), - e); + } catch (IllegalArgumentException e) { + throw new CommandValidationException(Strings.get("no.such.password.alias", p)); + } catch (Exception e) { + throw new CommandValidationException(Strings.get("no.such.password.alias", p), e); } if (expandedPassword == null) { - throw new CommandValidationException( - Strings.get("no.such.password.alias", p)); + throw new CommandValidationException(Strings.get("no.such.password.alias", p)); } } } @@ -292,24 +281,14 @@ void pingRemoteConnection(Node node) throws CommandValidationException { * @param node Node to connect to * @throws CommandValidationException */ - private void pingSSHConnection(Node node) throws - CommandValidationException { + private void pingSSHConnection(Node node) throws CommandValidationException { + SSHLauncher sshL = new SSHLauncher(node); try { - sshL.init(node, logger); sshL.pingConnection(); - } - catch (Exception e) { - String m1 = e.getMessage(); - String m2 = ""; - Throwable e2 = e.getCause(); - if (e2 != null) { - m2 = e2.getMessage(); - } - String msg = Strings.get("ssh.bad.connect", node.getNodeHost(), "SSH"); - logger.warning(StringUtils.cat(": ", msg, m1, m2, - sshL.toString())); - throw new CommandValidationException(StringUtils.cat(NL, - msg, m1, m2)); + } catch (SSHException e) { + String msg = Strings.get("ssh.bad.connect", node.getNodeHost(), "SSH", e.getMessage()); + LOG.log(WARNING, msg, e); + throw new CommandValidationException(msg, e); } } @@ -344,44 +323,23 @@ private void validateSSHConnection(ParameterMap map) throws int port = Integer.parseInt(resolver.resolve(sshport)); + // sshpassword and sshkeypassphrase may be password alias. + // Those aliases are handled by sshLauncher + Path resolvedInstallDir = new File(resolver.resolve(installdir)).toPath(); + String keyFile = resolver.resolve(sshkeyfile); + String host = resolver.resolve(nodehost); + SSHLauncher sshLauncher = new SSHLauncher(resolver.resolve(sshuser), resolver.resolve(nodehost), port, + sshpassword, keyFile == null ? null : new File(keyFile), sshkeypassphrase); try { - // sshpassword and sshkeypassphrase may be password alias. - // Those aliases are handled by sshLauncher - String resolvedInstallDir = resolver.resolve(installdir); - - String keyFile = resolver.resolve(sshkeyfile); - sshL.validate(resolver.resolve(nodehost), - port, - resolver.resolve(sshuser), - sshpassword, - keyFile == null ? null : new File(keyFile), - sshkeypassphrase, - resolvedInstallDir, - // Landmark file to ensure valid GF install - LANDMARK_FILE, - logger); - } - catch (IOException e) { - String m1 = e.getMessage(); - String m2 = ""; - Throwable e2 = e.getCause(); - if (e2 != null) { - m2 = e2.getMessage(); - } - if (e instanceof FileNotFoundException) { - if (!installFlag) { - logger.warning(StringUtils.cat(": ", m1, m2, sshL.toString())); - throw new CommandValidationException(StringUtils.cat(NL, - m1, m2)); - } - } - else { - String msg = Strings.get("ssh.bad.connect", nodehost, "SSH"); - logger.warning(StringUtils.cat(": ", msg, m1, m2, - sshL.toString())); - throw new CommandValidationException(StringUtils.cat(NL, - msg, m1, m2)); + Path pathToCheck = resolvedInstallDir.resolve(LANDMARK_FILE); + if (!installFlag && !sshLauncher.exists(pathToCheck)) { + throw new CommandValidationException( + "Invalid install directory: could not find " + pathToCheck + " on " + host); } + } catch (SSHException e) { + String msg = Strings.get("ssh.bad.connect", nodehost, "SSH", e.getMessage()); + LOG.log(WARNING, msg, e); + throw new CommandValidationException(msg, e); } } @@ -444,63 +402,48 @@ void runAdminCommandOnNode(Node node, List command, } if (StringUtils.ok(humanCommand)) { - msg3 = Strings.get("node.remote.tocomplete", - nodeHost, installDir, humanCommand); + msg3 = Strings.get("node.remote.tocomplete", nodeHost, installDir, humanCommand); } - NodeRunner nr = new NodeRunner(habitat, logger); + NodeRunner nr = new NodeRunner(locator.getService(AuthTokenManager.class)); try { int status = nr.runAdminCommandOnNode(node, output, command, context); - if (status != 0) { + if (status == 0) { + failure = false; + LOG.log(INFO, output.toString().trim()); + } else { // Command ran, but didn't succeed. Log full information - msg2 = Strings.get("node.command.failed", nodeName, - nodeHost, output.toString().trim(), nr.getLastCommandRun()); - logger.warning(StringUtils.cat(": ", msg1, msg2, msg3)); + msg2 = Strings.get("node.command.failed", nodeName, nodeHost, output.toString().trim(), + nr.getLastCommandRun()); + LOG.log(WARNING, StringUtils.cat(": ", msg1, msg2, msg3)); // Don't expose command name to user in case it is a hidden command - msg2 = Strings.get("node.command.failed.short", nodeName, - nodeHost, output.toString().trim()); - } - else { - failure = false; - logger.info(output.toString().trim()); + msg2 = Strings.get("node.command.failed.short", nodeName, nodeHost, output.toString().trim()); } - } - catch (SSHCommandExecutionException ec) { - msg2 = Strings.get("node.ssh.bad.connect", - nodeName, nodeHost, ec.getMessage()); + } catch (SSHCommandExecutionException e) { + msg2 = Strings.get("node.ssh.bad.connect", nodeName, nodeHost, e.getMessage()); // Log some extra info - String msg = Strings.get("node.command.failed.ssh.details", - nodeName, nodeHost, ec.getCommandRun(), ec.getMessage(), - ec.getSSHSettings()); - logger.warning(StringUtils.cat(": ", msg1, msg, msg3)); - } - catch (ProcessManagerException ex) { - msg2 = Strings.get("node.command.failed.local.details", - ex.getMessage(), nr.getLastCommandRun()); - logger.warning(StringUtils.cat(": ", msg1, msg2, msg3)); + String msg = Strings.get("node.command.failed.ssh.details", nodeName, nodeHost, e.getCommandRun(), + e.getMessage(), e.getSSHSettings()); + LOG.log(WARNING, StringUtils.cat(": ", msg1, msg, msg3), e); + } catch (ProcessManagerException e) { + msg2 = Strings.get("node.command.failed.local.details", e.getMessage(), nr.getLastCommandRun()); + LOG.log(WARNING, StringUtils.cat(": ", msg1, msg2, msg3), e); // User message doesn't have command that was run - msg2 = Strings.get("node.command.failed.local.exception", - ex.getMessage()); - } - catch (UnsupportedOperationException e) { + msg2 = Strings.get("node.command.failed.local.exception", e.getMessage()); + } catch (UnsupportedOperationException e) { msg2 = Strings.get("node.not.ssh", nodeName, nodeHost); - logger.warning(StringUtils.cat(": ", msg1, msg2, msg3)); - } - catch (IllegalArgumentException e) { + LOG.log(WARNING, StringUtils.cat(": ", msg1, msg2, msg3), e); + } catch (IllegalArgumentException e) { msg2 = e.getMessage(); - logger.warning(StringUtils.cat(": ", msg1, msg2, msg3)); + LOG.log(WARNING, StringUtils.cat(": ", msg1, msg2, msg3), e); } if (failure) { report.setMessage(StringUtils.cat(NL + NL, msg1, msg2, msg3)); report.setActionExitCode(ActionReport.ExitCode.FAILURE); - } - else { + } else { report.setActionExitCode(ActionReport.ExitCode.SUCCESS); } - - - return; } private RemoteType parseType(ParameterMap map) throws CommandValidationException { diff --git a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/PingNodeRemoteCommand.java b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/PingNodeRemoteCommand.java index 1c21b676aa2..0072e286408 100644 --- a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/PingNodeRemoteCommand.java +++ b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/PingNodeRemoteCommand.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025 Contributors to the Eclipse Foundation * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -13,7 +14,6 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 */ - package com.sun.enterprise.v3.admin.cluster; import com.sun.enterprise.config.serverbeans.Node; @@ -38,7 +38,7 @@ */ public abstract class PingNodeRemoteCommand implements AdminCommand { @Inject - ServiceLocator habitat; + ServiceLocator locator; @Inject private Nodes nodes; @Param(name = "name", primary = true) @@ -55,7 +55,7 @@ protected final void executeInternal(AdminCommandContext context) { Node theNode = null; logger = context.getLogger(); - NodeUtils nodeUtils = new NodeUtils(habitat, logger); + NodeUtils nodeUtils = new NodeUtils(locator); // Make sure Node is valid theNode = nodes.getNode(name); diff --git a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/SecureAdminBootstrapHelper.java b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/SecureAdminBootstrapHelper.java index e2a6765b933..baa49ae0a53 100644 --- a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/SecureAdminBootstrapHelper.java +++ b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/SecureAdminBootstrapHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -17,25 +17,23 @@ package com.sun.enterprise.v3.admin.cluster; -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.SftpException; +import com.jcraft.jsch.SftpATTRS; import com.sun.enterprise.config.serverbeans.Node; import com.sun.enterprise.util.cluster.RemoteType; import com.sun.enterprise.util.io.FileUtils; -import java.io.BufferedInputStream; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.net.URI; -import java.util.logging.Level; -import java.util.logging.Logger; +import java.nio.file.Path; +import org.glassfish.cluster.ssh.launcher.SSHException; import org.glassfish.cluster.ssh.launcher.SSHLauncher; +import org.glassfish.cluster.ssh.launcher.SSHSession; import org.glassfish.cluster.ssh.sftp.SFTPClient; -import org.glassfish.hk2.api.ServiceLocator; /** * Bootstraps the secure admin-related files, either over ssh (copying files from the @@ -44,58 +42,50 @@ * * @author Tim Quinn */ -public abstract class SecureAdminBootstrapHelper { - private static final String DOMAIN_XML_PATH = "config/domain.xml"; - private static final String[] SECURE_ADMIN_FILE_REL_URIS_TO_COPY = new String[]{ +public abstract class SecureAdminBootstrapHelper implements AutoCloseable { + private static final Logger LOG = System.getLogger(SecureAdminBootstrapHelper.class.getName()); + private static final Path DOMAIN_XML_PATH = Path.of("config", "domain.xml"); + private static final Path[] SECURE_ADMIN_FILE_REL_URIS_TO_COPY = new Path[] { DOMAIN_XML_PATH, - "config/keystore.jks", - "config/cacerts.jks" - }; - private static final String[] SECURE_ADMIN_FILE_DIRS_TO_CREATE = new String[]{ - "config" + Path.of("config", "keystore.jks"), + Path.of("config", "cacerts.jks") }; + private static final Path[] SECURE_ADMIN_FILE_DIRS_TO_CREATE = new Path[] {Path.of("config")}; /** - * Creates a new helper for delivering files needed for secure admin to - * the remote instance. + * Creates a new helper for delivering files needed for secure admin to the remote instance. * - * @param habitat hk2 habitat - * @param DASInstanceDir directory of the local instance - source for the required files + * @param sshL + * @param dasInstanceDir directory of the local instance - source for the required files * @param remoteNodeDir directory of the remote node on the remote system * @param instance name of the instance on the remote node to bootstrap * @param node Node from the domain configuration for the target node - * @param logger * @return the remote helper * @throws BootstrapException */ public static SecureAdminBootstrapHelper getRemoteHelper( - final ServiceLocator habitat, - final File DASInstanceDir, + final SSHLauncher sshL, + final File dasInstanceDir, final String remoteNodeDir, final String instance, - final Node node, - final Logger logger) throws BootstrapException { - - RemoteType type = null; + final Node node) throws BootstrapException { + final RemoteType type; try { // this also handles the case where node is null type = RemoteType.valueOf(node.getType()); - } - catch (Exception e) { - throw new IllegalArgumentException( - Strings.get("internal.error", "unknown type")); + } catch (Exception e) { + throw new IllegalArgumentException(Strings.get("internal.error", "unknown type")); } switch (type) { case SSH: return new SSHHelper( - habitat, - DASInstanceDir, + sshL, + dasInstanceDir, remoteNodeDir, instance, - node, - logger); + node); default: throw new IllegalArgumentException( Strings.get("internal.error", "A new type must have " @@ -112,18 +102,19 @@ public static SecureAdminBootstrapHelper getRemoteHelper( * * @return the local helper */ - public static SecureAdminBootstrapHelper getLocalHelper( - final File existingInstanceDir, - final File newInstanceDir) { + public static SecureAdminBootstrapHelper getLocalHelper(final File existingInstanceDir, final File newInstanceDir) { return new LocalHelper(existingInstanceDir, newInstanceDir); } /** * Cleans up any allocated resources. */ - protected abstract void mkdirs(String dirURI) throws IOException; + protected abstract void mkdirs(Path dir) throws IOException; - protected abstract void close(); + @Override + public void close() { + // nothing + } /** * Copies the bootstrap files from their origin to their destination. @@ -158,21 +149,20 @@ public static SecureAdminBootstrapHelper getLocalHelper( /** * Bootstraps the instance for remote admin. * - * @throws IOException + * @throws BootstrapException */ public void bootstrapInstance() throws BootstrapException { try { mkdirs(); copyBootstrapFiles(); backdateInstanceDomainXML(); - } - catch (Exception ex) { + } catch (Exception ex) { throw new BootstrapException(ex); } } private void mkdirs() throws IOException { - for (String dirPath : SECURE_ADMIN_FILE_DIRS_TO_CREATE) { + for (Path dirPath : SECURE_ADMIN_FILE_DIRS_TO_CREATE) { mkdirs(dirPath); } } @@ -181,195 +171,129 @@ private void mkdirs() throws IOException { * Implements the helper functionality for a remote instance. */ private static abstract class RemoteHelper extends SecureAdminBootstrapHelper { - final Logger logger; final File dasInstanceDir; - final String instance; - final String remoteNodeDir; - final String remoteInstanceDir; + final Path remoteNodeDir; + final Path remoteInstanceDir; RemoteHelper( - final ServiceLocator habitat, final File dasInstanceDir, - String remoteNodeDir, + final String remoteNodeDir, final String instance, - final Node node, - final Logger logger) throws BootstrapException { + final Node node) { this.dasInstanceDir = dasInstanceDir; - this.instance = instance; - this.logger = logger; this.remoteNodeDir = remoteNodeDirUnixStyle(node, remoteNodeDir); - remoteInstanceDir = remoteInstanceDir(this.remoteNodeDir); + this.remoteInstanceDir = this.remoteNodeDir.resolve(instance); } -// private long dasDomainXMLTimestamp(final File dasInstanceDir) { -// return new File(dasInstanceDir.toURI().resolve(DOMAIN_XML_PATH)).lastModified(); -// } - abstract void writeToFile(final String path, final InputStream content) throws IOException; - - abstract void setLastModified(final String path, final long when) throws IOException; + abstract void writeToFile(final Path remotePath, final File localFile) throws IOException; - String ensureTrailingSlash(final String path) { - if (!path.endsWith("/")) { - return path + "/"; - } - else { - return path; - } - } + abstract void setLastModified(final Path remotePath, final long when) throws IOException; - String remoteNodeDirUnixStyle(final Node node, final String remoteNodeDir) { - /* - * Use the node dir if it was specified when the node was created. - * Otherwise derive it: ${remote-install-dir}/glassfish/${node-name} - */ - String result; - if (remoteNodeDir != null) { - result = remoteNodeDir; - } - else { - result = new StringBuilder(ensureTrailingSlash(node.getInstallDirUnixStyle())).append("glassfish/nodes/").append(node.getName()).toString(); + /** + * Use the node dir if it was specified when the node was created. + * Otherwise derive it: ${remote-install-dir}/glassfish/${node-name} + */ + private static Path remoteNodeDirUnixStyle(final Node node, final String remoteNodeDir) { + if (remoteNodeDir == null) { + return Path.of(node.getInstallDirUnixStyle()).resolve(Path.of("glassfish", "nodes", node.getName())); } - - return ensureTrailingSlash(result.replaceAll("\\\\", "/")); + return Path.of(remoteNodeDir); } - String remoteInstanceDir(final String remoteNodeDirPath) { - final StringBuilder remoteInstancePath = new StringBuilder(remoteNodeDirPath); - if (!remoteNodeDirPath.endsWith("/")) { - remoteInstancePath.append("/"); - } - remoteInstancePath.append(instance).append("/"); - return remoteInstancePath.toString().replaceAll("\\\\", "/"); - } @Override protected void copyBootstrapFiles() throws FileNotFoundException, IOException { - for (String fileRelativePath : SECURE_ADMIN_FILE_REL_URIS_TO_COPY) { - InputStream is = null; - String remoteFilePath = null; + for (Path fileRelativePath : SECURE_ADMIN_FILE_REL_URIS_TO_COPY) { + Path remoteFilePath = remoteInstanceDir.resolve(fileRelativePath); try { - is = new BufferedInputStream( - new FileInputStream( - new File(dasInstanceDir.toURI().resolve(fileRelativePath)))); - remoteFilePath = remoteInstanceDir + fileRelativePath; - writeToFile(remoteFilePath, is); - logger.log(Level.FINE, "Copied bootstrap file to {0}", remoteFilePath); - } - catch (Exception ex) { - if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, "Error copying bootstrap file to " + remoteFilePath, ex); - } + writeToFile(remoteFilePath, dasInstanceDir.toPath().resolve(fileRelativePath).toFile()); + LOG.log(Level.DEBUG, "Copied bootstrap file to {0}", remoteFilePath); + } catch (Exception ex) { + LOG.log(Level.DEBUG, "Error copying bootstrap file to " + remoteFilePath, ex); throw new IOException(ex); } - finally { - if (is != null) { - is.close(); - } - } } } - - /** - * Returns the specified system time in seconds since 01 Jan 1970. - * - * @param milliseconds normal Java time (in milliseconds) - * @return - */ - int secondsSince_01_Jan_1970(final long milliseconds) { - return (int) (milliseconds) / 1000; - } } private static class SSHHelper extends RemoteHelper { - final SFTPClient ftpClient; - final SSHLauncher launcher; + private final SSHSession session; + private final SFTPClient ftpClient; + private final SSHLauncher launcher; private SSHHelper( - final ServiceLocator habitat, + final SSHLauncher sshLauncher, final File dasInstanceDir, - String remoteNodeDir, + final String remoteNodeDir, final String instance, - final Node node, - final Logger logger) throws BootstrapException { - super(habitat, dasInstanceDir, remoteNodeDir, instance, node, logger); - - launcher = habitat.getService(SSHLauncher.class); - launcher.init(node, logger); - + final Node node) throws BootstrapException { + super(dasInstanceDir, remoteNodeDir, instance, node); + launcher = sshLauncher; try { - ftpClient = launcher.getSFTPClient(); + session = launcher.openSession(); + } catch (SSHException e) { + throw new BootstrapException(e); } - catch (JSchException ex) { - throw new BootstrapException(launcher, ex); + try { + ftpClient = session.createSFTPClient(); + } catch (SSHException e) { + if (session != null) { + session.close(); + } + throw new BootstrapException(e); } } @Override - protected void mkdirs(String dir) throws IOException { - String remoteDir = remoteInstanceDir + dir; - logger.log(Level.FINE, "Trying to create directories for remote path {0}", - remoteDir); - Integer instanceDirPermissions; - try { - instanceDirPermissions = ftpClient.getSftpChannel().lstat(remoteNodeDir).getPermissions(); - } - catch (SftpException ex) { - throw new IOException(remoteNodeDir, ex); + protected void mkdirs(Path dir) throws IOException { + Path remoteDir = remoteInstanceDir.resolve(dir); + LOG.log(Level.DEBUG, "Trying to create directories for remote path {0}", remoteDir); + SftpATTRS attrs = ftpClient.lstat(remoteNodeDir); + if (attrs == null) { + throw new IOException("Remote path " + remoteNodeDir + " does not exist."); } - logger.log(Level.FINE, "Creating remote bootstrap directory " - + remoteDir + " with permissions " - + instanceDirPermissions.toString()); - try { - ftpClient.mkdirs(remoteDir, instanceDirPermissions); - } - catch (SftpException ex) { - throw new IOException(remoteDir, ex); + int instanceDirPermissions = attrs.getPermissions(); + LOG.log(Level.DEBUG, "Creating remote bootstrap directory " + remoteDir + " with permissions " + + Integer.toOctalString(instanceDirPermissions)); + ftpClient.mkdirs(remoteDir); + if (launcher.getCapabilities().isChmodSupported()) { + ftpClient.chmod(remoteDir, instanceDirPermissions); } } @Override - protected void close() { + public void close() { if (ftpClient != null) { ftpClient.close(); } + if (session != null) { + session.close(); + } } @Override - void writeToFile(final String path, final InputStream content) throws IOException { - try { - ftpClient.getSftpChannel().put(content, path); - } - catch (SftpException ex) { - throw new IOException(ex); - } + void writeToFile(final Path remotePath, final File localFile) throws IOException { + ftpClient.put(localFile, remotePath); } - /* bnevins -- this method had to be made abstract ONLY because of the - * annoying special exception constructor that is SSH-specific. - */ @Override protected void backdateInstanceDomainXML() throws BootstrapException { - final String remoteDomainXML = remoteInstanceDir + DOMAIN_XML_PATH; + final Path remoteDomainXML = remoteInstanceDir.resolve(DOMAIN_XML_PATH); try { setLastModified(remoteDomainXML, 0); + } catch (IOException ex) { + throw new BootstrapException(ex); } - catch (IOException ex) { - throw new BootstrapException(launcher, ex); - } - logger.log(Level.FINE, "Backdated the instance's copy of domain.xml"); + LOG.log(Level.DEBUG, "Backdated the instance's copy of domain.xml"); } + /** + * Times over ssh are expressed as seconds since 01 Jan 1970. + */ @Override - void setLastModified(final String path, final long when) throws IOException { - /* - * Times over ssh are expressed as seconds since 01 Jan 1970. - */ - try { - ftpClient.getSftpChannel().setMtime(path, secondsSince_01_Jan_1970(when)); - } catch (SftpException e) { - throw new IOException(e); - } + void setLastModified(final Path path, final long when) throws IOException { + ftpClient.setTimeModified(path, when); } } @@ -386,8 +310,8 @@ private LocalHelper(final File existingInstanceDir, final File newInstanceDir) { } @Override - protected void mkdirs(String dir) { - final File newDir = new File(newInstanceDirURI.resolve(dir)); + protected void mkdirs(Path dir) { + final File newDir = Path.of(newInstanceDirURI).resolve(dir).toFile(); if (!newDir.exists() && !newDir.mkdirs()) { throw new RuntimeException(Strings.get("secure.admin.boot.errCreDir", newDir.getAbsolutePath())); } @@ -395,48 +319,35 @@ protected void mkdirs(String dir) { @Override public void copyBootstrapFiles() throws IOException { - for (String relativePathToFile : SECURE_ADMIN_FILE_REL_URIS_TO_COPY) { - final File origin = new File(existingInstanceDirURI.resolve(relativePathToFile)); - final File dest = new File(newInstanceDirURI.resolve(relativePathToFile)); + for (Path relativePathToFile : SECURE_ADMIN_FILE_REL_URIS_TO_COPY) { + final File origin = Path.of(existingInstanceDirURI).resolve(relativePathToFile).toFile(); + final File dest = Path.of(newInstanceDirURI).resolve(relativePathToFile).toFile(); FileUtils.copy(origin, dest); } } @Override protected void backdateInstanceDomainXML() throws BootstrapException { - final File newDomainXMLFile = new File(newInstanceDirURI.resolve(DOMAIN_XML_PATH)); + final File newDomainXMLFile = Path.of(newInstanceDirURI).resolve(DOMAIN_XML_PATH).toFile(); if (!newDomainXMLFile.setLastModified(0)) { throw new RuntimeException(Strings.get("secure.admin.boot.errSetLastMod", newDomainXMLFile.getAbsolutePath())); } } - - @Override - protected void close() { - // Nothing to do for local provider - return; - } } public static class BootstrapException extends Exception { - private transient final SSHLauncher launcher; + private static final long serialVersionUID = -5488899043810477670L; - public BootstrapException(final SSHLauncher launcher, final Exception ex) { - super(ex); - this.launcher = launcher; + public BootstrapException(final String message, final Exception ex) { + super(message + "; Cause: " + ex.getMessage(), ex); } public BootstrapException(final Exception ex) { - super(ex); - launcher = null; + super(ex.getMessage(), ex); } public BootstrapException(final String msg) { super(msg); - launcher = null; - } - - public String sshSettings() { - return (launcher != null ? launcher.toString() : ""); } } } diff --git a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/SetupSshCommand.java b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/SetupSshCommand.java index 5fdeec04c95..1706aabe466 100644 --- a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/SetupSshCommand.java +++ b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/SetupSshCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -20,8 +20,6 @@ import com.sun.enterprise.universal.glassfish.TokenResolver; import com.sun.enterprise.util.StringUtils; -import jakarta.inject.Inject; - import java.io.File; import java.io.IOException; import java.util.List; @@ -37,6 +35,7 @@ import org.glassfish.api.admin.CommandLock; import org.glassfish.api.admin.ExecuteOn; import org.glassfish.api.admin.RuntimeType; +import org.glassfish.cluster.ssh.launcher.SSHKeyInstaller; import org.glassfish.cluster.ssh.launcher.SSHLauncher; import org.glassfish.cluster.ssh.util.SSHUtil; import org.glassfish.hk2.api.PerLookup; @@ -72,8 +71,6 @@ public class SetupSshCommand implements AdminCommand { private String realPass; TokenResolver resolver = new TokenResolver(); - @Inject - SSHLauncher sshL; private void validate() throws CommandException { user = resolver.resolve(user); @@ -82,7 +79,7 @@ private void validate() throws CommandException { throw new CommandException(Strings.get("setup.ssh.null.sshpass")); } // obtain real password - realPass = sshL.expandPasswordAlias(sshpassword); + realPass = SSHLauncher.expandPasswordAlias(sshpassword); if (realPass == null) { throw new CommandException(Strings.get("setup.ssh.unalias.error", sshpassword)); @@ -128,9 +125,6 @@ private void validate() throws CommandException { public final void execute(AdminCommandContext context) { logger = context.getLogger(); - // initialize logger for SSHLauncher - sshL.init(logger); - ActionReport report = context.getActionReport(); try { @@ -143,7 +137,7 @@ public final void execute(AdminCommandContext context) { for (String node : hosts) { File keyFile = sshkeyfile == null ? null : new File(sshkeyfile); - sshL.init(user, node, port, realPass, keyFile, sshkeypassphrase, logger); + SSHLauncher sshL = new SSHLauncher(user, node, port, realPass, keyFile, sshkeypassphrase); if (generatekey) { if (sshkeyfile != null || SSHUtil.getExistingKeyFile() != null) { if (sshL.checkConnection()) { @@ -153,16 +147,16 @@ public final void execute(AdminCommandContext context) { } } try { - sshL.setupKey(node, sshpublickeyfile, generatekey, realPass); - } - catch (IOException ce) { - logger.log(Level.INFO, "SSH key setup failed: " + ce); + SSHKeyInstaller installer = new SSHKeyInstaller(sshL); + File pubKeyFile = sshpublickeyfile == null ? null : new File(sshpublickeyfile); + installer.setupKey(node, pubKeyFile, generatekey, realPass); + } catch (IOException ce) { + logger.log(Level.INFO, "SSH key setup failed.", ce); report.setMessage(Strings.get("setup.ssh.failed", ce.getMessage())); report.setActionExitCode(ActionReport.ExitCode.FAILURE); return; - } - catch (Exception e) { - //handle KeyStoreException + } catch (Exception e) { + // handle KeyStoreException if (logger.isLoggable(Level.FINER)) { logger.log(Level.FINER, "Keystore error: ", e); } @@ -185,7 +179,7 @@ private String getSSHPassphrase() throws CommandException { String key = ""; if (sshkeypassphrase != null && !sshkeypassphrase.isEmpty()) { - key = sshL.expandPasswordAlias(sshkeypassphrase); + key = SSHLauncher.expandPasswordAlias(sshkeypassphrase); if (key == null) { throw new CommandException("setup.ssh.null.keypassphrase"); diff --git a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/StartInstanceCommand.java b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/StartInstanceCommand.java index 55ddb20ce8f..c229f18ceb2 100644 --- a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/StartInstanceCommand.java +++ b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/StartInstanceCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2023, 2025 Contributors to the Eclipse Foundation * Copyright (c) 2008, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -67,7 +67,7 @@ }) public class StartInstanceCommand implements AdminCommand { @Inject - ServiceLocator habitat; + ServiceLocator locator; @Inject private Nodes nodes; @@ -114,13 +114,13 @@ public class StartInstanceCommand implements AdminCommand { StartInstanceCommand(ServiceLocator habitat_, String iname_, boolean debug_, ServerEnvironment env_) { instanceName = iname_; debug = debug_; - habitat = habitat_; - nodes = habitat.getService(Nodes.class); + locator = habitat_; + nodes = locator.getService(Nodes.class); // env: neither getByType or getByContract works. Not worth the effort //to find the correct magic incantation for HK2! env = env_; - servers = habitat.getService(Servers.class); + servers = locator.getService(Servers.class); } /** @@ -200,7 +200,7 @@ public void execute(AdminCommandContext ctx) { } private void startInstance(AdminCommandContext ctx) { - NodeUtils nodeUtils = new NodeUtils(habitat, logger); + NodeUtils nodeUtils = new NodeUtils(locator); ArrayList command = new ArrayList<>(); String humanCommand = null; diff --git a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/StopInstanceCommand.java b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/StopInstanceCommand.java index 5fa75b54bcc..83cfeb05d50 100644 --- a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/StopInstanceCommand.java +++ b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/StopInstanceCommand.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025 Contributors to the Eclipse Foundation * Copyright (c) 2008, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -13,26 +14,25 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 */ - package com.sun.enterprise.v3.admin.cluster; -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.SftpException; import com.sun.enterprise.admin.remote.RemoteRestAdminCommand; import com.sun.enterprise.admin.remote.ServerRemoteRestAdminCommand; import com.sun.enterprise.admin.util.RemoteInstanceCommandHelper; import com.sun.enterprise.config.serverbeans.Node; import com.sun.enterprise.config.serverbeans.Nodes; import com.sun.enterprise.config.serverbeans.Server; -import com.sun.enterprise.module.ModulesRegistry; import com.sun.enterprise.util.StringUtils; import com.sun.enterprise.v3.admin.StopServer; import jakarta.inject.Inject; import java.io.File; +import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; @@ -49,7 +49,9 @@ import org.glassfish.api.admin.RestParam; import org.glassfish.api.admin.RuntimeType; import org.glassfish.api.admin.ServerEnvironment; +import org.glassfish.cluster.ssh.launcher.SSHException; import org.glassfish.cluster.ssh.launcher.SSHLauncher; +import org.glassfish.cluster.ssh.launcher.SSHSession; import org.glassfish.cluster.ssh.sftp.SFTPClient; import org.glassfish.hk2.api.IterableProvider; import org.glassfish.hk2.api.PerLookup; @@ -84,7 +86,7 @@ public class StopInstanceCommand extends StopServer implements AdminCommand, PostConstruct { @Inject - private ServiceLocator habitat; + private ServiceLocator locator; @Inject private ServerContext serverContext; @Inject @@ -93,8 +95,6 @@ public class StopInstanceCommand extends StopServer implements AdminCommand, Pos private ServerEnvironment env; @Inject IterableProvider nodeList; - @Inject - private ModulesRegistry registry; @Param(optional = true, defaultValue = "true") private Boolean force = true; @Param(optional = true, defaultValue = "false") @@ -107,13 +107,11 @@ public class StopInstanceCommand extends StopServer implements AdminCommand, Pos private String errorMessage = null; private String cmdName = "stop-instance"; private Server instance; - File pidFile = null; - SFTPClient ftpClient=null; + @Override public void execute(AdminCommandContext context) { report = context.getActionReport(); logger = context.getLogger(); - SSHLauncher launcher; if (env.isDas()) { if (kill) { @@ -122,8 +120,7 @@ public void execute(AdminCommandContext context) { errorMessage = callInstance(); } } else { - errorMessage = Strings.get("stop.instance.notDas", - env.getRuntimeType().toString()); + errorMessage = Strings.get("stop.instance.notDas", env.getRuntimeType().toString()); } if(errorMessage == null && !kill) { @@ -137,8 +134,7 @@ public void execute(AdminCommandContext context) { } report.setActionExitCode(ActionReport.ExitCode.SUCCESS); - report.setMessage(Strings.get("stop.instance.success", - instanceName)); + report.setMessage(Strings.get("stop.instance.success", instanceName)); if (kill) { // If we killed then stop-local-instance already waited for death @@ -154,42 +150,36 @@ public void execute(AdminCommandContext context) { Node node = nodes.getNode(nodeName); InstanceDirUtils insDU = new InstanceDirUtils(node, serverContext); // this should be replaced with method from Node config bean. - if (node.isLocal()){ - try { - pidFile = new File (insDU.getLocalInstanceDir(instance.getName()) , "config/pid"); - } catch (java.io.IOException eio){ - // could not get the file name so can't see if it still exists. Need to exit - return; - } - if (pidFile.exists()){ - //server still not down completely, do we poll? - errorMessage = pollForRealDeath("local"); + final Path pidFilePath; + try { + pidFilePath = insDU.getLocalInstanceDir(instance.getName()).toPath().resolve(Path.of("config", "pid")); + } catch (IOException e) { + // could not get the file name so can't see if it still exists. Need to exit + return; + } + if (node.isLocal()) { + final File pidFile = pidFilePath.toFile(); + if (pidFile.exists()) { + // server still not down completely, do we poll? + errorMessage = pollForRealDeath(pidFile::exists); } - } else if (node.getType().equals("SSH")) { - try { - pidFile = new File (insDU.getLocalInstanceDir(instance.getName()) , "config/pid"); - } catch (java.io.IOException eio){ - // could not get the file name so can't see if it still exists. Need to exit - return; - } //use SFTPClient to see if file exists. - launcher = habitat.getService(SSHLauncher.class); - launcher.init(node, logger); - try { - ftpClient = launcher.getSFTPClient(); - if (ftpClient.exists(pidFile.toString())){ + SSHLauncher launcher = new SSHLauncher(node); + try (SSHSession session = launcher.openSession(); SFTPClient ftpClient = session.createSFTPClient()) { + if (ftpClient.exists(pidFilePath)){ // server still not down, do we poll? - errorMessage = pollForRealDeath("SSH"); + Supplier check = () -> { + try { + return ftpClient.exists(pidFilePath); + } catch (SSHException e) { + return false; + } + }; + errorMessage = pollForRealDeath(check); } - } catch (JSchException ex) { - //could not get to other host - } catch (SftpException ex) { + } catch (SSHException ex) { //could not get to other host - } finally { - if (ftpClient != null) { - ftpClient.close(); - } } } if (errorMessage != null) { @@ -200,16 +190,18 @@ public void execute(AdminCommandContext context) { @Override public void postConstruct() { - helper = new RemoteInstanceCommandHelper(habitat); + helper = new RemoteInstanceCommandHelper(locator); } private String initializeInstance() { - if (!StringUtils.ok(instanceName)) + if (!StringUtils.ok(instanceName)) { return Strings.get("stop.instance.noInstanceName", cmdName); + } instance = helper.getServer(instanceName); - if (instance == null) + if (instance == null) { return Strings.get("stop.instance.noSuchInstance", instanceName); + } return null; } @@ -221,25 +213,29 @@ private String initializeInstance() { private String callInstance() { String msg = initializeInstance(); - if (msg != null) + if (msg != null) { return msg; + } String host = instance.getAdminHost(); - if (host == null) + if (host == null) { return Strings.get("stop.instance.noHost", instanceName); + } int port = helper.getAdminPort(instance); - if (port < 0) + if (port < 0) { return Strings.get("stop.instance.noPort", instanceName); + } - if(!instance.isRunning()) + if(!instance.isRunning()) { return null; + } try { logger.info(Strings.get("stop.instance.init", instanceName)); - RemoteRestAdminCommand rac = new ServerRemoteRestAdminCommand(habitat, "_stop-instance", + RemoteRestAdminCommand rac = new ServerRemoteRestAdminCommand(locator, "_stop-instance", host, port, false, "admin", null, logger); // notice how we do NOT send in the instance's name as an operand!! @@ -258,12 +254,13 @@ private String callInstance() { private String killInstance(AdminCommandContext context) { String msg = initializeInstance(); - if (msg != null) + if (msg != null) { return msg; + } String nodeName = instance.getNodeRef(); Node node = nodes.getNode(nodeName); - NodeUtils nodeUtils = new NodeUtils(habitat, logger); + NodeUtils nodeUtils = new NodeUtils(locator); // asadmin command to run on instances node ArrayList command = new ArrayList(); @@ -274,9 +271,10 @@ private String killInstance(AdminCommandContext context) { String firstErrorMessage = Strings.get("stop.local.instance.kill", instanceName, nodeName, humanCommand); - if (logger.isLoggable(Level.FINE)) + if (logger.isLoggable(Level.FINE)) { logger.fine("stop-instance: running " + humanCommand + " on " + nodeName); + } nodeUtils.runAdminCommandOnNode(node, command, context, firstErrorMessage, humanCommand, null); @@ -294,41 +292,35 @@ private String pollForDeath() { int counter = 0; // 120 seconds while (++counter < 240) { - if (!instance.isRunning()) + if (!instance.isRunning()) { return null; + } try { - Thread.sleep(500); - } - catch (Exception e) { - // ignore + Thread.sleep(500L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } } return Strings.get("stop.instance.timeout", instanceName); } - private String pollForRealDeath(String mode){ - int counter = 0; // 30 seconds + + private String pollForRealDeath(Supplier pidFileExists) { + int counter = 0; // 30 seconds // 24 * 5 = 120 seconds while (++counter < 24) { try { - if (mode.equals("local")){ - if(!pidFile.exists()){ - return null; - } - }else if (mode.equals("SSH")){ - if (!ftpClient.exists(pidFile.toString())) - return null; + if (!pidFileExists.get()) { + return null; } - // Fairly long interval between tries because checking over // SSH is expensive. - Thread.sleep(5000); - } catch (Exception e) { - // ignore + Thread.sleep(1000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } - } return Strings.get("stop.instance.timeout.completely", instanceName); diff --git a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/UpdateNodeCommand.java b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/UpdateNodeCommand.java index 67a3b516cc4..1fab305f3ee 100644 --- a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/UpdateNodeCommand.java +++ b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/UpdateNodeCommand.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025 Contributors to the Eclipse Foundation * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -29,6 +30,7 @@ import java.beans.PropertyVetoException; import java.io.File; +import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; @@ -99,20 +101,20 @@ public class UpdateNodeCommand implements AdminCommand { @Param(name="sshnodehost", optional=true) String sshnodehost; - @Param(name="sshkeyfile", optional=true) + @Param(name = "sshkeyfile", optional = true) String sshkeyfile; - @Param(name = "sshpassword", optional = true, password=true) - String sshpassword; + @Param(name = "sshpassword", optional = true, password = true) + String sshpassword; - @Param(name = "sshkeypassphrase", optional = true, password=true) - String sshkeypassphrase; + @Param(name = "sshkeypassphrase", optional = true, password = true) + String sshkeypassphrase; @Param(name = "windowsdomain", optional = true) - String windowsdomain; + String windowsdomain; - @Param(name = "type", optional=true) - String type; + @Param(name = "type", optional = true) + String type; @Override public void execute(AdminCommandContext context) { @@ -134,15 +136,12 @@ public void execute(AdminCommandContext context) { TokenResolver resolver = null; // Create a resolver that can replace system properties in strings - Map systemPropsMap = - new HashMap((Map)(System.getProperties())); + Map systemPropsMap = new HashMap((Map) (System.getProperties())); resolver = new TokenResolver(systemPropsMap); - String resolvedInstallDir = resolver.resolve(installdir); - File actualInstallDir = new File( resolvedInstallDir+"/" + NodeUtils.LANDMARK_FILE); - - - if (!actualInstallDir.exists()){ - report.setMessage(Strings.get("invalid.installdir",installdir)); + Path resolvedInstallDir = new File(resolver.resolve(installdir)).toPath(); + Path actualInstallDir = resolvedInstallDir.resolve(NodeUtils.LANDMARK_FILE); + if (!actualInstallDir.toFile().exists()) { + report.setMessage(Strings.get("invalid.installdir", installdir)); report.setActionExitCode(ActionReport.ExitCode.FAILURE); return; } @@ -195,43 +194,56 @@ public Object run(ConfigBeanProxy param) throws PropertyVetoException, Transacti Nodes nodes = ((Domain)param).getNodes(); Node node = nodes.getNode(nodeName); Node writeableNode = t.enroll(node); - if (windowsdomain != null) + if (windowsdomain != null) { writeableNode.setWindowsDomain(windowsdomain); - if (nodedir != null) + } + if (nodedir != null) { writeableNode.setNodeDir(nodedir); - if (nodehost != null) + } + if (nodehost != null) { writeableNode.setNodeHost(nodehost); - if (installdir != null) + } + if (installdir != null) { writeableNode.setInstallDir(installdir); - if (type != null) + } + if (type != null) { writeableNode.setType(type); + } if (sshport != null || sshnodehost != null ||sshuser != null || sshkeyfile != null){ SshConnector sshC = writeableNode.getSshConnector(); if (sshC == null) { sshC =writeableNode.createChild(SshConnector.class); - }else + } else { sshC = t.enroll(sshC); + } - if (sshport != null) + if (sshport != null) { sshC.setSshPort(sshport); - if(sshnodehost != null) + } + if(sshnodehost != null) { sshC.setSshHost(sshnodehost); + } if (sshuser != null || sshkeyfile != null || sshpassword != null || sshkeypassphrase != null ) { SshAuth sshA = sshC.getSshAuth(); if (sshA == null) { sshA = sshC.createChild(SshAuth.class); - } else + } else { sshA = t.enroll(sshA); + } - if (sshuser != null) + if (sshuser != null) { sshA.setUserName(sshuser); - if (sshkeyfile != null) + } + if (sshkeyfile != null) { sshA.setKeyfile(sshkeyfile); - if(sshpassword != null) + } + if(sshpassword != null) { sshA.setPassword(sshpassword); - if(sshkeypassphrase != null) + } + if(sshkeypassphrase != null) { sshA.setKeyPassphrase(sshkeypassphrase); + } sshC.setSshAuth(sshA); } writeableNode.setSshConnector(sshC); diff --git a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/UpdateNodeRemoteCommand.java b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/UpdateNodeRemoteCommand.java index 34cc3f3ad8b..49eb53b35fd 100644 --- a/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/UpdateNodeRemoteCommand.java +++ b/nucleus/cluster/admin/src/main/java/com/sun/enterprise/v3/admin/cluster/UpdateNodeRemoteCommand.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025 Contributors to the Eclipse Foundation * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -13,7 +14,6 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 */ - package com.sun.enterprise.v3.admin.cluster; import com.sun.enterprise.config.serverbeans.Node; @@ -153,19 +153,19 @@ protected final void executeInternal(AdminCommandContext context) { // Validate the settings try { - NodeUtils nodeUtils = new NodeUtils(habitat, logger); + NodeUtils nodeUtils = new NodeUtils(habitat); nodeUtils.validate(validateMap); } catch (CommandValidationException e) { String m1 = Strings.get("node.ssh.invalid.params"); - if (!force) { + if (force) { + String m2 = Strings.get("update.node.ssh.continue.force"); + msg.append(StringUtils.cat(NL, m1, e.getMessage(), m2)); + } else { String m2 = Strings.get("update.node.ssh.not.updated"); msg.append(StringUtils.cat(NL, m1, m2, e.getMessage())); report.setMessage(msg.toString()); report.setActionExitCode(ActionReport.ExitCode.FAILURE); return; - } else { - String m2 = Strings.get("update.node.ssh.continue.force"); - msg.append(StringUtils.cat(NL, m1, e.getMessage(), m2)); } } // Settings are valid. Now use the generic update-node command to diff --git a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/CreateLocalInstanceFilesystemCommand.java b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/CreateLocalInstanceFilesystemCommand.java index 008f4396c23..a5916af4dc6 100644 --- a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/CreateLocalInstanceFilesystemCommand.java +++ b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/CreateLocalInstanceFilesystemCommand.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025 Contributors to the Eclipse Foundation * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -13,7 +14,6 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 */ - package com.sun.enterprise.admin.cli.cluster; import com.sun.enterprise.util.net.NetUtils; @@ -70,10 +70,11 @@ public class CreateLocalInstanceFilesystemCommand extends LocalInstanceCommand { protected void validate() throws CommandException { - if(ok(instanceName0)) + if (ok(instanceName0)) { instanceName = instanceName0; - else + } else { throw new CommandException(Strings.get("Instance.badInstanceName")); + } isCreateInstanceFilesystem = true; @@ -108,8 +109,6 @@ protected void validate() } - /** - */ @Override protected int executeCommand() throws CommandException { @@ -188,17 +187,15 @@ private void checkDASCoordinates() throws CommandException { InetAddress.getByName(DASHost); } catch (UnknownHostException e) { String thisHost = NetUtils.getHostName(); - String msg = Strings.get("Instance.DasHostUnknown", - DASHost, thisHost); + String msg = Strings.get("Instance.DasHostUnknown", DASHost, thisHost); throw new CommandException(msg, e); } // See if DAS is reachable - if (! NetUtils.isRunning(DASHost, DASPort)) { + if (!NetUtils.isRunning(DASHost, DASPort)) { // DAS provided host and port String thisHost = NetUtils.getHostName(); - String msg = Strings.get("Instance.DasHostUnreachable", - DASHost, Integer.toString(DASPort), thisHost); + String msg = Strings.get("Instance.DasHostUnreachable", DASHost, Integer.toString(DASPort), thisHost); throw new CommandException(msg); } } diff --git a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/InstallNodeBaseCommand.java b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/InstallNodeBaseCommand.java index 9150e483198..14be18425b5 100644 --- a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/InstallNodeBaseCommand.java +++ b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/InstallNodeBaseCommand.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025 Contributors to the Eclipse Foundation * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -34,7 +35,6 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; -import java.util.logging.Level; import org.glassfish.api.Param; import org.glassfish.api.admin.CommandException; @@ -43,6 +43,8 @@ import org.glassfish.internal.api.Globals; import org.jvnet.hk2.annotations.Service; +import static java.util.logging.Level.FINER; + /** * @author Rajiv Mordani * @author Byron Nevins @@ -83,24 +85,21 @@ protected void validate() throws CommandException { @Override protected int executeCommand() throws CommandException { File zipFile = null; - try { - ArrayList binDirFiles = new ArrayList(); + ArrayList binDirFiles = new ArrayList<>(); precopy(); zipFile = createZipFileIfNeeded(binDirFiles); copyToHosts(zipFile, binDirFiles); - } - catch (CommandException e) { + } catch (CommandException e) { throw e; - } - catch (Exception e) { + } catch (Exception e) { throw new CommandException(e); - } - finally { + } finally { if (!save && delete) { if (zipFile != null) { - if (!zipFile.delete()) + if (!zipFile.delete()) { zipFile.deleteOnExit(); + } } } } @@ -128,31 +127,28 @@ private File createZipFileIfNeeded(ArrayList binDirFiles) throws IOExcep File zipFileLocation = null; File glassFishZipFile = null; - if (archive != null) { + if (archive == null) { + zipFileLocation = new File("."); + if (!zipFileLocation.canWrite()) { + zipFileLocation = new File(System.getProperty("java.io.tmpdir")); + } + glassFishZipFile = File.createTempFile("glassfish", ".zip", zipFileLocation); + String filePath = glassFishZipFile.getCanonicalPath(); + filePath = filePath.replaceAll("\\\\", "/"); + archiveName = filePath.substring(filePath.lastIndexOf('/') + 1, filePath.length()); + } else { archive = archive.replace('\\', '/'); archiveName = archive.substring(archive.lastIndexOf("/") + 1, archive.length()); zipFileLocation = new File(archive.substring(0, archive.lastIndexOf("/"))); glassFishZipFile = new File(archive); if (glassFishZipFile.exists() && !create) { - if (logger.isLoggable(Level.FINER)) - logger.finer("Found " + archive); + logger.log(FINER, "Found {0}", archive); delete = false; return glassFishZipFile; - } - else if (!zipFileLocation.canWrite()) { + } else if (!zipFileLocation.canWrite()) { throw new IOException("Cannot write to " + archive); } } - else { - zipFileLocation = new File("."); - if (!zipFileLocation.canWrite()) { - zipFileLocation = new File(System.getProperty("java.io.tmpdir")); - } - glassFishZipFile = File.createTempFile("glassfish", ".zip", zipFileLocation); - String filePath = glassFishZipFile.getCanonicalPath(); - filePath = filePath.replaceAll("\\\\", "/"); - archiveName = filePath.substring(filePath.lastIndexOf("/") + 1, filePath.length()); - } FileListerRelative lister = new FileListerRelative(installRoot); lister.keepEmptyDirectories(); @@ -161,31 +157,25 @@ else if (!zipFileLocation.canWrite()) { List resultFiles1 = Arrays.asList(files); ArrayList resultFiles = new ArrayList(resultFiles1); - if (logger.isLoggable(Level.FINER)) - logger.finer("Number of files to be zipped = " + - resultFiles.size()); + logger.finer(() -> "Number of files to be zipped = " + resultFiles.size()); Iterator iter = resultFiles.iterator(); while (iter.hasNext()) { String fileName = iter.next(); - String fPath = fileName.substring(fileName.lastIndexOf("/") + 1); + String fPath = fileName.substring(fileName.lastIndexOf('/') + 1); if (fPath.equals(glassFishZipFile.getName())) { - if (logger.isLoggable(Level.FINER)) - logger.finer("Removing file = " + fileName); + logger.log(FINER, "Removing file = {0}", fileName); iter.remove(); continue; } if (fileName.contains("domains") || fileName.contains("nodes")) { iter.remove(); - } - else if (isFileWithinBinDirectory(fileName)) { + } else if (isFileWithinBinDirectory(fileName)) { binDirFiles.add(fileName); } } - if (logger.isLoggable(Level.FINER)) - logger.finer("Final number of files to be zipped = " + - resultFiles.size()); + logger.finer(() -> "Final number of files to be zipped = " + resultFiles.size()); String[] filesToZip = new String[resultFiles.size()]; filesToZip = resultFiles.toArray(filesToZip); @@ -218,8 +208,9 @@ public static String toString(InputStream ins) throws IOException { char[] buffer = new char[4096]; int n; - while ((n = reader.read(buffer)) >= 0) + while ((n = reader.read(buffer)) >= 0) { sw.write(buffer, 0, n); + } return sw.toString(); } diff --git a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/InstallNodeSshCommand.java b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/InstallNodeSshCommand.java index 58e403617c0..2a71341e725 100644 --- a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/InstallNodeSshCommand.java +++ b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/InstallNodeSshCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -17,31 +17,29 @@ package com.sun.enterprise.admin.cli.cluster; -import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.ChannelSftp.LsEntry; -import com.jcraft.jsch.JSchException; import com.jcraft.jsch.SftpException; import com.sun.enterprise.util.SystemPropertyConstants; -import jakarta.inject.Inject; - -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.logging.Level; import org.glassfish.api.Param; import org.glassfish.api.admin.CommandException; +import org.glassfish.cluster.ssh.launcher.SSHException; import org.glassfish.cluster.ssh.launcher.SSHLauncher; +import org.glassfish.cluster.ssh.launcher.SSHSession; import org.glassfish.cluster.ssh.sftp.SFTPClient; import org.glassfish.cluster.ssh.util.SSHUtil; import org.glassfish.hk2.api.PerLookup; import org.jvnet.hk2.annotations.Service; +import static java.util.logging.Level.SEVERE; + /** * @author Byron Nevins */ @@ -54,8 +52,7 @@ public class InstallNodeSshCommand extends InstallNodeBaseCommand { int port; @Param(optional = true) String sshkeyfile; - @Inject - private SSHLauncher sshLauncher; + //storing password to prevent prompting twice private final Map sshPasswords = new HashMap<>(); @@ -104,36 +101,33 @@ void copyToHosts(File zipFile, ArrayList binDirFiles) throws CommandExce // And it makes the signature simpler for other subclasses... try { copyToHostsInternal(zipFile, binDirFiles); - } - catch (CommandException ex) { + } catch (CommandException ex) { throw ex; - } - catch (JSchException ex) { - throw new CommandException(ex); - } - catch (InterruptedException ex) { - throw new CommandException(ex); - } - catch (IOException ex) { - throw new CommandException(ex); + } catch (IOException e) { + // Note: CommandException is not printed to logs. + logger.log(SEVERE, + "Failed to copy zip file " + zipFile + " and to make binary files " + binDirFiles + " executable.", e); + throw new CommandException("Failed to copy zip file " + zipFile + " and to make binary files " + binDirFiles + + " executable. Reason: " + e.getMessage(), e); } } - private void copyToHostsInternal(File zipFile, ArrayList binDirFiles) throws JSchException, IOException, InterruptedException, CommandException { - ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + + private void copyToHostsInternal(File zipFile, ArrayList binDirFiles) + throws IOException, CommandException { boolean prompt = promptPass; for (String host : hosts) { File keyFile = getSshKeyFile() == null ? null : new File(getSshKeyFile()); - sshLauncher.init(getRemoteUser(), host, getRemotePort(), sshpassword, keyFile, sshkeypassphrase, logger); + SSHLauncher sshLauncher = new SSHLauncher(getRemoteUser(), host, getRemotePort(), sshpassword, keyFile, sshkeypassphrase); if (getSshKeyFile() != null && !sshLauncher.checkConnection()) { - //key auth failed, so use password auth + // key auth failed, so use password auth prompt = true; } if (prompt) { - String sshpass = null; + final String sshpass; if (sshPasswords.containsKey(host)) { sshpass = String.valueOf(sshPasswords.get(host)); } else { @@ -141,133 +135,55 @@ private void copyToHostsInternal(File zipFile, ArrayList binDirFiles) th } //re-initialize - sshLauncher.init(getRemoteUser(), host, getRemotePort(), sshpass, keyFile, sshkeypassphrase, logger); + sshLauncher = new SSHLauncher(getRemoteUser(), host, getRemotePort(), sshpass, keyFile, sshkeypassphrase); prompt = false; } - String sshInstallDir = getInstallDir().replace('\\', '/'); - - SFTPClient sftpClient = sshLauncher.getSFTPClient(); - ChannelSftp sftpChannel = sftpClient.getSftpChannel(); - try { + Path sshInstallDir = Path.of(getInstallDir()); + try (SSHSession session = sshLauncher.openSession(); SFTPClient sftpClient = session.createSFTPClient()) { + sftpClient.rmDir(sshInstallDir, true); if (!sftpClient.exists(sshInstallDir)) { - sftpClient.mkdirs(sshInstallDir, 0755); - } - } - catch (SftpException ioe) { - logger.info(Strings.get("mkdir.failed", sshInstallDir, host)); - throw new IOException(ioe); - } - - //delete the sshInstallDir contents if non-empty - try { - //get list of file in DAS sshInstallDir - List files = getListOfInstallFiles(sshInstallDir); - deleteRemoteFiles(sftpClient, files, sshInstallDir, getForce()); - } - catch (SftpException ex) { - logger.finer("Failed to remove sshInstallDir contents"); - throw new IOException(ex); - } - catch (IOException ex) { - logger.finer("Failed to remove sshInstallDir contents"); - throw new IOException(ex); - } - - String zip = zipFile.getCanonicalPath(); - try { - logger.info("Copying " + zip + " (" + zipFile.length() + " bytes)" - + " to " + host + ":" + sshInstallDir); - // TODO: Looks like we need to quote the paths to scp in case they contain spaces. - sftpChannel.cd(sftpChannel.getHome()); - sftpChannel.cd(sshInstallDir); - sftpChannel.put(zipFile.getAbsolutePath(), zipFile.getName()); - if (logger.isLoggable(Level.FINER)) { - logger.finer("Copied " + zip + " to " + host + ":" + - sshInstallDir); - } - } - catch (SftpException ex) { - logger.info(Strings.get("cannot.copy.zip.file", zip, host)); - throw new IOException(ex); - } - - try { - logger.info("Installing " + getArchiveName() + " into " + host + ":" + sshInstallDir); - String unzipCommand = "cd '" + sshInstallDir + "'; jar -xvf " + getArchiveName(); - int status = sshLauncher.runCommand(unzipCommand, outStream); - if (status != 0) { - logger.info(Strings.get("jar.failed", host, outStream.toString())); - throw new CommandException("Remote command output: " + outStream.toString()); - } - if (logger.isLoggable(Level.FINER)) { - logger.finer("Installed " + getArchiveName() + " into " + - host + ":" + sshInstallDir); - } - } - catch (IOException ioe) { - logger.info(Strings.get("jar.failed", host, outStream.toString())); - throw new IOException(ioe); - } - - try { - logger.info("Removing " + host + ":" + sshInstallDir + "/" + getArchiveName()); - sftpChannel.cd(sftpChannel.getHome()); - sftpChannel.rm(sshInstallDir + "/" + getArchiveName()); - if (logger.isLoggable(Level.FINER)) { - logger.finer("Removed " + host + ":" + sshInstallDir + "/" + - getArchiveName()); - } - } - catch (SftpException ioe) { - logger.info(Strings.get("remove.glassfish.failed", host, sshInstallDir)); - throw new IOException(ioe); - } - sftpClient.close(); - - sftpClient = sshLauncher.getSFTPClient(); - - // unjarring doesn't retain file permissions, hence executables need - // to be fixed with proper permissions - logger.info("Fixing file permissions of all bin files under " + host + ":" + sshInstallDir); - try { - if (binDirFiles.isEmpty()) { - //binDirFiles can be empty if the archive isn't a fresh one - searchAndFixBinDirectoryFiles(sshInstallDir, sftpClient); - } - else { - for (String binDirFile : binDirFiles) { - sftpClient.chmod(sshInstallDir + "/" + binDirFile, 0755); + sftpClient.mkdirs(sshInstallDir); + if (sshLauncher.getCapabilities().isChmodSupported()) { + sftpClient.chmod(sshInstallDir, 0755); } } - if (logger.isLoggable(Level.FINER)) { - logger.finer("Fixed file permissions of all bin files " + - "under " + host + ":" + sshInstallDir); - } - } - catch (SftpException ioe) { - logger.info(Strings.get("fix.permissions.failed", host, sshInstallDir)); - throw new IOException(ioe); - } - if (Constants.v4) { - logger.info("Fixing file permissions for nadmin file under " + host + ":" - + sshInstallDir + "/" + SystemPropertyConstants.getComponentName() + "/lib"); - try { - sftpClient.chmod((sshInstallDir + "/" + SystemPropertyConstants.getComponentName() + "/lib/nadmin"), 0755); - if (logger.isLoggable(Level.FINER)) { - logger.finer("Fixed file permission for nadmin under " + - host + ":" + sshInstallDir + "/" + - SystemPropertyConstants.getComponentName() + - "/lib/nadmin"); + final Path remoteZipFile = sshInstallDir.resolve(zipFile.getName()); + logger.info(() -> "Copying " + zipFile + " (" + zipFile.length() + " bytes)" + " to " + host + ":" + + sshInstallDir); + sftpClient.put(zipFile, remoteZipFile); + logger.finer(() -> "Copied " + zipFile + " to " + host + ":" + remoteZipFile); + + logger.info(() -> "Unpacking " + remoteZipFile + " on " + host + " to " + sshInstallDir); + session.unzip(remoteZipFile, sshInstallDir); + logger.finer(() -> "Unpacked " + getArchiveName() + " into " + host + ":" + sshInstallDir); + + logger.info(() -> "Removing " + host + ":" + remoteZipFile); + sftpClient.rm(remoteZipFile); + logger.finer(() -> "Removed " + host + ":" + remoteZipFile); + + // zip doesn't retain file permissions, hence executables need + // to be fixed with proper permissions + if (sshLauncher.getCapabilities().isChmodSupported()) { + logger.info(() -> "Fixing file permissions of all bin files under " + host + ":" + sshInstallDir); + try { + if (binDirFiles.isEmpty()) { + // binDirFiles can be empty if the archive isn't a fresh one + searchAndFixBinDirectoryFiles(sshInstallDir, sftpClient); + } else { + for (String binDirFile : binDirFiles) { + sftpClient.chmod(sshInstallDir.resolve(binDirFile), 0755); + } + } + logger.finer( + () -> "Fixed file permissions of all bin files under " + host + ":" + sshInstallDir); + } catch (SSHException e) { + throw new IOException("Could not set permissions on commands in bin directories under " + + sshInstallDir + " directory on host " + host + ". Cause: " + e, e); } } - catch (SftpException ioe) { - logger.info(Strings.get("fix.permissions.failed", host, sshInstallDir)); - throw new IOException(ioe); - } } - sftpClient.close(); } } @@ -278,14 +194,12 @@ private void copyToHostsInternal(File zipFile, ArrayList binDirFiles) th * @param sftpClient ftp client handle * @throws SftpException */ - private void searchAndFixBinDirectoryFiles(String installDir, SFTPClient sftpClient) throws SftpException { - for (LsEntry directoryEntry : (List) sftpClient.getSftpChannel().ls(installDir)) { - if (directoryEntry.getFilename().equals(".") || directoryEntry.getFilename().equals("..")) { - continue; - } else if (directoryEntry.getAttrs().isDir()) { - String subDir = installDir + "/" + directoryEntry.getFilename(); + private void searchAndFixBinDirectoryFiles(Path installDir, SFTPClient sftpClient) throws SSHException { + for (LsEntry directoryEntry : sftpClient.lsDetails(installDir, e -> true)) { + if (directoryEntry.getAttrs().isDir()) { + Path subDir = installDir.resolve(directoryEntry.getFilename()); if (directoryEntry.getFilename().equals("bin")) { - fixAllFiles(subDir, sftpClient); + fixFilePermissions(subDir, sftpClient); } else { searchAndFixBinDirectoryFiles(subDir, sftpClient); } @@ -300,52 +214,12 @@ private void searchAndFixBinDirectoryFiles(String installDir, SFTPClient sftpCli * @param sftpClient ftp client handle * @throws SftpException */ - private void fixAllFiles(String binDir, SFTPClient sftpClient) throws SftpException { - for (LsEntry directoryEntry : (List) sftpClient.getSftpChannel().ls(binDir)) { - if (directoryEntry.getFilename().equals(".") || directoryEntry.getFilename().equals("..")) { - continue; - } else { - String fName = binDir + "/" + directoryEntry.getFilename(); - sftpClient.chmod(fName, 0755); - } + private void fixFilePermissions(Path binDir, SFTPClient sftpClient) throws SSHException { + for (String directoryEntry : sftpClient.ls(binDir, entry -> !entry.getAttrs().isDir())) { + sftpClient.chmod(binDir.resolve(directoryEntry), 0755); } } - /** - * Determines if GlassFish is installed on remote host at specified location. - * Uses SSH launcher to execute 'asadmin version' - * @param host remote host - * @throws JSchException - * @throws CommandException - * @throws IOException - * @throws InterruptedException - */ - private void checkIfAlreadyInstalled(String host, String sshInstallDir) throws JSchException, CommandException, IOException, InterruptedException { - //check if an installation already exists on remote host - ByteArrayOutputStream outStream = new ByteArrayOutputStream(); - try { - String asadmin = Constants.v4 ? "/lib/nadmin' version --local --terse" : "/bin/asadmin' version --local --terse"; - String cmd = "'" + sshInstallDir + "/" + SystemPropertyConstants.getComponentName() + asadmin; - int status = sshLauncher.runCommand(cmd, outStream); - if (status == 0) { - if (logger.isLoggable(Level.FINER)) { - logger.finer(host + ":'" + cmd + "'" + - " returned [" + outStream.toString() + "]"); - } - throw new CommandException(Strings.get("install.dir.exists", sshInstallDir)); - } - else { - if (logger.isLoggable(Level.FINER)) { - logger.finer(host + ":'" + cmd + "'" + - " failed [" + outStream.toString() + "]"); - } - } - } - catch (IOException ex) { - logger.info(Strings.get("glassfish.install.check.failed", host)); - throw new IOException(ex); - } - } @Override final void precopy() throws CommandException { @@ -356,7 +230,7 @@ final void precopy() throws CommandException { boolean prompt = promptPass; for (String host : hosts) { File keyFile = getSshKeyFile() == null ? null : new File(getSshKeyFile()); - sshLauncher.init(getRemoteUser(), host, getRemotePort(), sshpassword, keyFile, sshkeypassphrase, logger); + SSHLauncher sshLauncher = new SSHLauncher(getRemoteUser(), host, getRemotePort(), sshpassword, keyFile, sshkeypassphrase); if (keyFile != null && !sshLauncher.checkConnection()) { //key auth failed, so use password auth @@ -366,28 +240,36 @@ final void precopy() throws CommandException { if (prompt) { String sshpass = getSSHPassword(host); sshPasswords.put(host, sshpass.toCharArray()); - //re-initialize - sshLauncher.init(getRemoteUser(), host, getRemotePort(), sshpass, keyFile, sshkeypassphrase, logger); + sshLauncher = new SSHLauncher(getRemoteUser(), host, getRemotePort(), sshpass, keyFile, sshkeypassphrase); prompt = false; } - String sshInstallDir = getInstallDir().replaceAll("\\\\", "/"); - - try { - SFTPClient sftpClient = sshLauncher.getSFTPClient(); + Path sshInstallDir = Path.of(getInstallDir()); + try (SSHSession session = sshLauncher.openSession(); SFTPClient sftpClient = session.createSFTPClient()) { if (sftpClient.exists(sshInstallDir)) { - checkIfAlreadyInstalled(host, sshInstallDir); + checkIfAlreadyInstalled(session, host, sshInstallDir); } - sftpClient.close(); - } catch (SftpException ex) { - throw new CommandException(ex); } catch (IOException ex) { throw new CommandException(ex); - } catch (JSchException ex) { - throw new CommandException(ex); - } catch (InterruptedException ex) { - throw new CommandException(ex); } } } + + + /** + * Determines if GlassFish is installed on remote host at specified location. + * Uses SSH launcher to execute 'asadmin version' + * + * @param host remote host + */ + private void checkIfAlreadyInstalled(SSHSession session, String host, Path sshInstallDir) + throws CommandException, SSHException { + //check if an installation already exists on remote host + String asadmin = Constants.v4 ? "/lib/nadmin' version --local --terse" : "/bin/asadmin' version --local --terse"; + String cmd = "'" + sshInstallDir + "/" + SystemPropertyConstants.getComponentName() + asadmin; + int status = session.exec(cmd); + if (status == 0) { + throw new CommandException(Strings.get("install.dir.exists", sshInstallDir)); + } + } } diff --git a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/LocalInstanceCommand.java b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/LocalInstanceCommand.java index 4926baddbd3..a42c679ad19 100644 --- a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/LocalInstanceCommand.java +++ b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/LocalInstanceCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * Copyright (c) 2024, 2025 Contributors to the Eclipse Foundation. * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -131,10 +131,11 @@ protected boolean setServerDirs() { protected void initInstance() throws CommandException { /* node dir - parent directory of all node(s) */ String nodeDirRootPath = null; - if (ok(nodeDir)) + if (ok(nodeDir)) { nodeDirRootPath = nodeDir; - else // node dir = /nodes + } else { nodeDirRootPath = getNodeDirRootDefault(); + } nodeDirRoot = new File(nodeDirRootPath); mkdirs(nodeDirRoot); @@ -351,8 +352,9 @@ final protected void whackFilesystem() throws CommandException { if (files == null || files.length <= 0) { // empty dir - if (files != null) + if (files != null) { FileUtils.whack(whackee); + } throw new CommandException(Strings.get("DeleteInstance.noWhack", whackee)); @@ -368,22 +370,21 @@ final protected void whackFilesystem() throws CommandException { } FileUtils.renameFile(whackee, tmpwhackee); FileUtils.whack(tmpwhackee); - } - catch (IOException ioe) { - throw new CommandException(Strings.get("DeleteInstance.badWhackWithException", - whackee, ioe, StringUtils.getStackTrace(ioe))); + } catch (IOException ioe) { + throw new CommandException( + Strings.get("DeleteInstance.badWhackWithException", whackee, ioe, StringUtils.getStackTrace(ioe))); } if (whackee.isDirectory()) { StringBuilder sb = new StringBuilder(); sb.append("whackee=").append(whackee.toString()); sb.append(", files in parent:"); files = parent.listFiles(); - for (File f : files) + for (File f : files) { sb.append(f.toString()).append(", "); + } File f1 = new File(whackee.toString()); sb.append(", new wackee.exists=").append(f1.exists()); - throw new CommandException(Strings.get("DeleteInstance.badWhack", - whackee) + ", " + sb.toString()); + throw new CommandException(Strings.get("DeleteInstance.badWhack", whackee) + ", " + sb); } // now see if the parent dir is empty. If so wipe it out. @@ -406,13 +407,15 @@ final protected void whackFilesystem() throws CommandException { } private boolean noInstancesRemain(File[] files) { - if (files == null || files.length <= 0) + if (files == null || files.length <= 0) { return true; + } if (files.length == 1 && files[0].isDirectory() - && files[0].getName().equals("agent")) + && files[0].getName().equals("agent")) { return true; + } return false; } @@ -428,12 +431,14 @@ protected String getInstallRootPath() throws CommandException { String installRootPath = getSystemProperty( SystemPropertyConstants.INSTALL_ROOT_PROPERTY); - if (!StringUtils.ok(installRootPath)) + if (!StringUtils.ok(installRootPath)) { installRootPath = System.getProperty( SystemPropertyConstants.INSTALL_ROOT_PROPERTY); + } - if (!StringUtils.ok(installRootPath)) + if (!StringUtils.ok(installRootPath)) { throw new CommandException("noInstallDirPath"); + } return installRootPath; } @@ -451,9 +456,10 @@ protected String getProductRootPath() throws CommandException { String productRootPath = getSystemProperty( SystemPropertyConstants.PRODUCT_ROOT_PROPERTY); - if (!StringUtils.ok(productRootPath)) + if (!StringUtils.ok(productRootPath)) { productRootPath = System.getProperty( SystemPropertyConstants.PRODUCT_ROOT_PROPERTY); + } if (!StringUtils.ok(productRootPath)) { // Product install root is parent of glassfish install root @@ -504,8 +510,9 @@ else if ((cons = System.console()) != null) { while (line != null && line.length() > 0) { try { port = Integer.parseInt(line); - if (port > 0 && port <= 65535) + if (port > 0 && port <= 65535) { break; + } } catch (NumberFormatException nfex) { } @@ -567,8 +574,9 @@ public boolean accept(File f) { } // the usual case -- one node dir child - if (files != null && files.length == 1) + if (files != null && files.length == 1) { return files[0]; + } /* * If there is no existing node dir child -- create one! @@ -623,8 +631,9 @@ public boolean accept(File f) { } for (File f : files) { - if (!f.getName().equals("agent")) + if (!f.getName().equals("agent")) { return f; + } } throw new CommandException( Strings.get("Instance.noInstanceDirs", parent)); @@ -643,8 +652,9 @@ private String getNodeDirRootDefault() throws CommandException { String nodeDirDefault = getSystemProperty( SystemPropertyConstants.AGENT_ROOT_PROPERTY); - if (StringUtils.ok(nodeDirDefault)) + if (StringUtils.ok(nodeDirDefault)) { return nodeDirDefault; + } String installRootPath = getInstallRootPath(); return installRootPath + "/" + "nodes"; @@ -653,12 +663,14 @@ private String getNodeDirRootDefault() throws CommandException { @Override protected File getMasterPasswordFile() { - if (nodeDirChild == null) + if (nodeDirChild == null) { return null; + } File mp = new File(new File(nodeDirChild,"agent"), "master-password"); - if (!mp.canRead()) + if (!mp.canRead()) { return null; + } return mp; } diff --git a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/LocalStrings.properties b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/LocalStrings.properties index aca98da8193..d10ff74c5e7 100644 --- a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/LocalStrings.properties +++ b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/LocalStrings.properties @@ -150,13 +150,6 @@ DomainMasterPasswordPrompt=Password is aliased. To obtain the real password, ent GetPasswordFailure=Failed to get the password from {0}''s keystore. PasswordAuthFailure=Failed to authenticate using the aliased password stored in {0}''s keystore. -#installNode -mkdir.failed=Could not create the directory {0} on host {1} -cannot.copy.zip.file=Could not copy zip file {0} to host {1} -jar.failed=jar command failed while installing glassfish on host {0}. Command output {1} -fix.permissions.failed=Could not set permissions on commands in bin directory on host {0} for glassfish installation {1} -glassfish.install.check.failed=Problem encountered while checking if installation already exists on remote host. - StopInstance.nopidprev=Can not find the process ID of the server. It is supposed to be here: {0}. Unable to kill the process. StopInstance.pidprevreaderror=Error trying to read the Process ID from {0}: {1} CreateLocalInstance.errSetLastMod=Attempt to set lastModified date for {0} failed; no further information is available diff --git a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/NativeRemoteCommandsBase.java b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/NativeRemoteCommandsBase.java index 35ac1d8c202..9a6a012f67f 100644 --- a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/NativeRemoteCommandsBase.java +++ b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/NativeRemoteCommandsBase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -17,8 +17,6 @@ package com.sun.enterprise.admin.cli.cluster; -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.SftpException; import com.sun.enterprise.admin.cli.CLICommand; import com.sun.enterprise.security.store.PasswordAdapter; import com.sun.enterprise.universal.glassfish.TokenResolver; @@ -28,8 +26,8 @@ import com.sun.enterprise.util.io.FileUtils; import java.io.File; -import java.io.FileFilter; import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -38,7 +36,6 @@ import org.glassfish.api.Param; import org.glassfish.api.admin.CommandException; import org.glassfish.cluster.ssh.launcher.SSHLauncher; -import org.glassfish.cluster.ssh.sftp.SFTPClient; import org.glassfish.internal.api.RelativePathResolver; /** @@ -119,7 +116,7 @@ private String getRemotePassword(String node, String key) throws CommandExceptio /** * Get SSH key passphrase from password file or user. */ - String getSSHPassphrase(boolean verifyConn) throws CommandException { + String getSSHPassphrase(boolean verifyConn) { String passphrase = getFromPasswordFile("AS_ADMIN_SSHKEYPASSPHRASE"); if (passphrase != null) { @@ -147,7 +144,7 @@ String getSSHPassphrase(boolean verifyConn) throws CommandException { /** * Get domain master password from password file or user. */ - String getMasterPassword(String domain) throws CommandException { + String getMasterPassword(String domain) { String masterPass = getFromPasswordFile("AS_ADMIN_MASTERPASSWORD"); //get password from user if not found in password file @@ -173,96 +170,6 @@ boolean isValidAnswer(String val) { || val.equalsIgnoreCase("y") || val.equalsIgnoreCase("n"); } - /** - * Method to delete files and directories on remote host - * 'nodes' directory is not considered for deletion since it would contain - * configuration information. - * @param sftpClient sftp client instance - * @param dasFiles file layout on DAS - * @param dir directory to be removed - * @param force true means delete all files, false means leave non-GlassFish files - * untouched - * @throws SftpException in case of error - * @throws IOException in case of error - */ - // byron XXXX - void deleteRemoteFiles(SFTPClient sftpClient, List dasFiles, String dir, boolean force) - throws SftpException, IOException { - - for (ChannelSftp.LsEntry directoryEntry : (List) sftpClient.getSftpChannel().ls(dir)) { - if (directoryEntry.getFilename().equals(".") || directoryEntry.getFilename().equals("..") - || directoryEntry.getFilename().equals("nodes")) { - continue; - } - else if (directoryEntry.getAttrs().isDir()) { - String f1 = dir + "/" + directoryEntry.getFilename(); - deleteRemoteFiles(sftpClient, dasFiles, f1, force); - //only if file is present in DAS, it is targeted for removal on remote host - //using force deletes all files on remote host - if (force) { - if (logger.isLoggable(Level.FINE)) { - logger.fine("Force removing directory " + f1); - } - if (isRemoteDirectoryEmpty(sftpClient, f1)) { - sftpClient.getSftpChannel().rmdir(f1); - } - } - else { - if (dasFiles.contains(f1)) { - if (isRemoteDirectoryEmpty(sftpClient, f1)) { - sftpClient.getSftpChannel().rmdir(f1); - } - } - } - } - else { - String f2 = dir + "/" + directoryEntry.getFilename(); - if (force) { - if (logger.isLoggable(Level.FINE)) { - logger.fine("Force removing file " + f2); - } - sftpClient.getSftpChannel().rm(f2); - } - else { - if (dasFiles.contains(f2)) { - sftpClient.getSftpChannel().rm(f2); - } - } - } - } - } - - /** - * Method to check if specified remote directory contains files - * - * @param sftp SFTP client handle - * @param file path to remote directory - * @return true if empty, false otherwise - * @throws SftpException - */ - boolean isRemoteDirectoryEmpty(SFTPClient sftp, String file) throws SftpException { - List l = sftp.getSftpChannel().ls(file); - if (l.size() > 2) { - return false; - } - return true; - } - - /** - * Remove trailing slash from a path string - * @param s - * @return - */ - String removeTrailingSlash(String s) { - if (!StringUtils.ok(s)) { - return s; - } - - if (s.endsWith("/")) { - s = s.substring(0, s.length() - 1); - } - return s; - } /** * Obtains the real password from the domain specific keystore given an alias @@ -277,14 +184,8 @@ String expandPasswordAlias(String host, String alias, boolean verifyConn) { try { File domainsDirFile = DomainDirs.getDefaultDomainsDir(); - //get the list of domains - File[] files = domainsDirFile.listFiles(new FileFilter() { - @Override - public boolean accept(File f) { - return f.isDirectory(); - } - }); - + // get the list of domains + File[] files = domainsDirFile.listFiles(File::isDirectory); for (File f : files) { //the following property is required for initializing the password helper System.setProperty(SystemPropertyConstants.INSTANCE_ROOT_PROPERTY, f.getAbsolutePath()); @@ -305,20 +206,19 @@ public boolean accept(File f) { } if (expandedPassword != null) { - SSHLauncher sshL = new SSHLauncher(); + SSHLauncher sshL; if (host != null) { sshpassword = expandedPassword; - sshL.init(getRemoteUser(), host, getRemotePort(), sshpassword, null, null, logger); + sshL = new SSHLauncher(getRemoteUser(), host, getRemotePort(), sshpassword, null, null); connStatus = sshL.checkPasswordAuth(); if (!connStatus) { logger.warning(Strings.get("PasswordAuthFailure", f.getName())); } - } - else { + } else { sshkeypassphrase = expandedPassword; if (verifyConn) { File keyFile = getSshKeyFile() == null ? null : new File(getSshKeyFile()); - sshL.init(getRemoteUser(), hosts[0], getRemotePort(), sshpassword, keyFile, sshkeypassphrase, logger); + sshL = new SSHLauncher(getRemoteUser(), hosts[0], getRemotePort(), sshpassword, keyFile, sshkeypassphrase); connStatus = sshL.checkConnection(); if (!connStatus) { logger.warning(Strings.get("PasswordAuthFailure", f.getName())); @@ -331,10 +231,9 @@ public boolean accept(File f) { } } } - } - catch (IOException ioe) { + } catch (IOException e) { if (logger.isLoggable(Level.FINER)) { - logger.finer(ioe.getMessage()); + logger.log(Level.FINER, e.getMessage(), e); } } return expandedPassword; @@ -348,20 +247,13 @@ public boolean accept(File f) { * @return List of files and directories * @throws IOException */ - List getListOfInstallFiles(String installDir) throws IOException { + List getListOfInstallFiles(Path installDir) throws IOException { String ins = resolver.resolve("${com.sun.aas.productRoot}"); - Set files = FileUtils.getAllFilesAndDirectoriesUnder(new File(ins)); - if (logger.isLoggable(Level.FINER)) { - logger.finer("Total number of files under " + ins + " = " + - files.size()); - } - String remoteDir = installDir; - if (!installDir.endsWith("/")) { - remoteDir = remoteDir + "/"; - } - List modList = new ArrayList<>(); - for (Object f : files) { - modList.add(remoteDir + FileUtils.makeForwardSlashes(((File) f).getPath())); + Set files = FileUtils.getAllFilesAndDirectoriesUnder(new File(ins)); + logger.finer(() -> "Total number of files under " + ins + " = " + files.size()); + List modList = new ArrayList<>(); + for (File f : files) { + modList.add(installDir.resolve(f.toPath())); } return modList; } diff --git a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/SetupSshKey.java b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/SetupSshKey.java index c19efae41bb..04a47102205 100644 --- a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/SetupSshKey.java +++ b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/SetupSshKey.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -21,7 +21,6 @@ import java.io.Console; import java.io.File; -import java.io.IOException; import java.util.Arrays; import java.util.logging.Level; @@ -29,6 +28,7 @@ import org.glassfish.api.admin.CommandException; import org.glassfish.api.admin.ExecuteOn; import org.glassfish.api.admin.RuntimeType; +import org.glassfish.cluster.ssh.launcher.SSHKeyInstaller; import org.glassfish.cluster.ssh.launcher.SSHLauncher; import org.glassfish.cluster.ssh.util.SSHUtil; import org.glassfish.hk2.api.PerLookup; @@ -82,7 +82,8 @@ protected void validate() throws CommandException { } } else { final File keyFile = new File(sshkeyfile); - promptPass = SSHUtil.validateKeyFile(keyFile); + SSHUtil.validateKeyFile(keyFile); + promptPass = true; if (SSHUtil.isEncryptedKey(keyFile)) { sshkeypassphrase = getSSHPassphrase(false); } @@ -96,15 +97,14 @@ protected void validate() throws CommandException { @Override protected int executeCommand() throws CommandException { - SSHLauncher sshL = habitat.getService(SSHLauncher.class); - String previousPassword = null; boolean status = false; for (String node : hosts) { final File keyFile = sshkeyfile == null ? null : new File(sshkeyfile); - sshL.init(getRemoteUser(), node, getRemotePort(), sshpassword, keyFile, sshkeypassphrase, logger); + final SSHLauncher sshL = new SSHLauncher(getRemoteUser(), node, getRemotePort(), sshpassword, keyFile, + sshkeypassphrase); if (generatekey || promptPass) { - //prompt for password iff required + // prompt for password iff required if (keyFile != null || SSHUtil.getExistingKeyFile() != null) { if (sshL.checkConnection()) { logger.info(Strings.get("SSHAlreadySetup", getRemoteUser(), node)); @@ -121,12 +121,12 @@ protected int executeCommand() throws CommandException { } try { - sshL.setupKey(node, sshpublickeyfile, generatekey, sshpassword); - } catch (IOException ce) { - // logger.fine("SSH key setup failed: " + ce.getMessage()); - throw new CommandException(Strings.get("KeySetupFailed", ce.getMessage())); + SSHKeyInstaller installer = new SSHKeyInstaller(sshL); + File pubKeyFile = sshpublickeyfile == null ? null : new File(sshpublickeyfile); + installer.setupKey(node, pubKeyFile, generatekey, sshpassword); } catch (Exception e) { - logger.log(Level.WARNING, "Keystore error for node: " + node, e); + logger.log(Level.SEVERE, "SSH key setup failed: ", e); + throw new CommandException(Strings.get("KeySetupFailed", e.getMessage()), e); } if (!sshL.checkConnection()) { @@ -160,8 +160,7 @@ private boolean promptForKeyGeneration() { logger.finer("Generate key!"); } return true; - } - else if (val != null && (val.equalsIgnoreCase("no") || val.equalsIgnoreCase("n"))) { + } else if (val != null && (val.equalsIgnoreCase("no") || val.equalsIgnoreCase("n"))) { break; } } diff --git a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/StartLocalInstanceCommand.java b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/StartLocalInstanceCommand.java index 1e1cad3d65a..4b624d6880d 100644 --- a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/StartLocalInstanceCommand.java +++ b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/StartLocalInstanceCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -50,8 +50,8 @@ @Service(name = "start-local-instance") @ExecuteOn(RuntimeType.DAS) @PerLookup -public class StartLocalInstanceCommand extends SynchronizeInstanceCommand - implements StartServerCommand { +public class StartLocalInstanceCommand extends SynchronizeInstanceCommand implements StartServerCommand { + @Param(optional = true, shortName = "v", defaultValue = "false") private boolean verbose; @@ -61,10 +61,12 @@ public class StartLocalInstanceCommand extends SynchronizeInstanceCommand @Param(optional = true, shortName = "d", defaultValue = "false") private boolean debug; - @Param(name = "dry-run", shortName = "n", optional = true, - defaultValue = "false") + @Param(name = "dry-run", shortName = "n", optional = true, defaultValue = "false") private boolean dry_run; + private GFLauncherInfo info; + private GFLauncher launcher; + // handled by superclass //@Param(name = "instance_name", primary = true, optional = false) //private String instanceName0; @@ -104,10 +106,7 @@ protected void validate() throws CommandException { */ @Override protected int executeCommand() throws CommandException { - - if (logger.isLoggable(Level.FINER)) { - logger.finer(toString()); - } + logger.finer(() -> toString()); if (sync.equals("none")) { logger.info(Strings.get("Instance.nosync")); @@ -150,38 +149,37 @@ protected int executeCommand() throws CommandException { getLauncher().launch(); - if (verbose || watchdog) { // we can potentially loop forever here... - while (true) { - int returnValue = getLauncher().getExitValue(); - - switch (returnValue) { - case RESTART_NORMAL: - logger.info(Strings.get("restart")); - break; - case RESTART_DEBUG_ON: - logger.info(Strings.get("restartChangeDebug", "on")); - getInfo().setDebug(true); - break; - case RESTART_DEBUG_OFF: - logger.info(Strings.get("restartChangeDebug", "off")); - getInfo().setDebug(false); - break; - default: - return returnValue; - } - - if (env.debug()) { - System.setProperty(CLIConstants.WALL_CLOCK_START_PROP, - "" + System.currentTimeMillis()); - } - getLauncher().relaunch(); - } - - } else { + if (!verbose && !watchdog) { helper.waitForServerStart(); helper.report(); return SUCCESS; } + + // we can potentially loop forever here... + while (true) { + int returnValue = getLauncher().getExitValue(); + switch (returnValue) { + case RESTART_NORMAL: + logger.info(Strings.get("restart")); + break; + case RESTART_DEBUG_ON: + logger.info(Strings.get("restartChangeDebug", "on")); + getInfo().setDebug(true); + break; + case RESTART_DEBUG_OFF: + logger.info(Strings.get("restartChangeDebug", "off")); + getInfo().setDebug(false); + break; + default: + return returnValue; + } + + if (env.debug()) { + System.setProperty(CLIConstants.WALL_CLOCK_START_PROP, + "" + System.currentTimeMillis()); + } + getLauncher().relaunch(); + } } catch (GFLauncherException gfle) { throw new CommandException(gfle.getMessage()); } catch (MiniXmlParserException me) { @@ -238,9 +236,8 @@ private String[] respawnArgs() { args.add("--node"); args.add(node); } - if (ok(instanceName)) - { - args.add(instanceName); // the operand + if (ok(instanceName)) { + args.add(instanceName); // the operand } if (logger.isLoggable(Level.FINER)) { @@ -278,8 +275,4 @@ private void setInfo(GFLauncherInfo inf) { public String toString() { return ObjectAnalyzer.toStringWithSuper(this); } - - private GFLauncherInfo info; - private GFLauncher launcher; - } diff --git a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/UninstallNodeSshCommand.java b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/UninstallNodeSshCommand.java index dc8e930aa94..25d06dead7d 100644 --- a/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/UninstallNodeSshCommand.java +++ b/nucleus/cluster/cli/src/main/java/com/sun/enterprise/admin/cli/cluster/UninstallNodeSshCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -17,15 +17,14 @@ package com.sun.enterprise.admin.cli.cluster; -import jakarta.inject.Inject; - import java.io.File; import java.io.IOException; -import java.util.List; +import java.nio.file.Path; import org.glassfish.api.Param; import org.glassfish.api.admin.CommandException; import org.glassfish.cluster.ssh.launcher.SSHLauncher; +import org.glassfish.cluster.ssh.launcher.SSHSession; import org.glassfish.cluster.ssh.sftp.SFTPClient; import org.glassfish.cluster.ssh.util.SSHUtil; import org.glassfish.hk2.api.PerLookup; @@ -43,8 +42,6 @@ public class UninstallNodeSshCommand extends UninstallNodeBaseCommand { private int port; @Param(optional = true) private String sshkeyfile; - @Inject - private SSHLauncher sshLauncher; @Override String getRawRemoteUser() { @@ -86,11 +83,10 @@ protected void validate() throws CommandException { @Override void deleteFromHosts() throws CommandException { try { - List files = getListOfInstallFiles(getInstallDir()); - + Path installDir = Path.of(getInstallDir()); for (String host : hosts) { File keyFile = sshkeyfile == null ? null : new File(sshkeyfile); - sshLauncher.init(getRemoteUser(), host, getRemotePort(), sshpassword, keyFile, sshkeypassphrase, logger); + SSHLauncher sshLauncher = new SSHLauncher(getRemoteUser(), host, getRemotePort(), sshpassword, keyFile, sshkeypassphrase); if (keyFile != null && !sshLauncher.checkConnection()) { //key auth failed, so use password auth @@ -99,18 +95,15 @@ void deleteFromHosts() throws CommandException { if (promptPass) { sshpassword = getSSHPassword(host); - //re-initialize - sshLauncher.init(getRemoteUser(), host, getRemotePort(), sshpassword, keyFile, sshkeypassphrase, logger); + sshLauncher = new SSHLauncher(getRemoteUser(), host, getRemotePort(), sshpassword, keyFile, sshkeypassphrase); } - try (SFTPClient sftpClient = sshLauncher.getSFTPClient()) { - if (!sftpClient.exists(getInstallDir())) { - throw new IOException(getInstallDir() + " Directory does not exist"); - } - deleteRemoteFiles(sftpClient, files, getInstallDir(), getForce()); - if (isRemoteDirectoryEmpty(sftpClient, getInstallDir())) { - sftpClient.getSftpChannel().rmdir(getInstallDir()); + try (SSHSession session = sshLauncher.openSession(); + SFTPClient sftpClient = session.createSFTPClient()) { + if (!sftpClient.exists(installDir)) { + throw new IOException("Directory does not exist: " + getInstallDir()); } + sftpClient.rmDir(installDir, false); } } } catch (CommandException ce) { diff --git a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/connect/NodeRunner.java b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/connect/NodeRunner.java index c9ebe34a7b9..c5907f89376 100644 --- a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/connect/NodeRunner.java +++ b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/connect/NodeRunner.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -24,45 +24,41 @@ import com.sun.enterprise.util.SystemPropertyConstants; import java.io.File; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.util.ArrayList; import java.util.List; -import java.util.logging.Logger; import org.glassfish.api.admin.AdminCommandContext; import org.glassfish.api.admin.SSHCommandExecutionException; import org.glassfish.common.util.admin.AsadminInput; import org.glassfish.common.util.admin.AuthTokenManager; -import org.glassfish.hk2.api.ServiceLocator; +/** + * This class is responsible for running asadmin commands on nodes. + */ public class NodeRunner { - private static final String NL = System.lineSeparator(); + private static final Logger LOG = System.getLogger(NodeRunner.class.getName()); private static final String AUTH_TOKEN_STDIN_LINE_PREFIX = "option." + AuthTokenManager.AUTH_TOKEN_OPTION_NAME + "="; - private final ServiceLocator habitat; - private final Logger logger; - private String lastCommandRun = null; + private final AuthTokenManager authTokenManager; + private String lastCommandRun; - public NodeRunner(ServiceLocator habitat, Logger logger) { - this.logger = logger; - this.habitat = habitat; - authTokenManager = habitat.getService(AuthTokenManager.class); + /** + * + * @param authTokenManager Used to generate the auth token. + */ + public NodeRunner(AuthTokenManager authTokenManager) { + this.authTokenManager = authTokenManager; } + /** + * @return the last command we tried to execute. Useful for error logs. + */ public String getLastCommandRun() { return lastCommandRun; } - public boolean isSshNode(Node node) { - - if (node == null) { - throw new IllegalArgumentException("Node is null"); - } - if (node.getType() == null) { - return false; - } - return node.getType().equals("SSH"); - } - /** * Run an asadmin command on a Node. The node may be local or remote. If * it is remote then SSH is used to execute the command on the node. @@ -80,6 +76,7 @@ public boolean isSshNode(Node node) { * command (like start-local-instance) as well as an * parameters for the command. It does not include the * string "asadmin" itself. + * @param context Used to get the Subject and to generate a token for it. * @return The status of the asadmin command. Typically 0 if the * command was successful else 1. * @@ -100,7 +97,6 @@ public int runAdminCommandOnNode(Node node, StringBuilder output, UnsupportedOperationException, IllegalArgumentException { - if (node == null) { throw new IllegalArgumentException("Node is null"); } @@ -112,9 +108,12 @@ public int runAdminCommandOnNode(Node node, StringBuilder output, if (node.isLocal()) { return runAdminCommandOnLocalNode(node, output, args, stdinLines); - } else { + } + final String type = node.getType(); + if ("SSH".equals(type)) { return runAdminCommandOnRemoteNode(node, output, args, stdinLines); } + throw new UnsupportedOperationException("Node type is not supported: " + type); } private int runAdminCommandOnLocalNode(Node node, StringBuilder output, @@ -140,7 +139,7 @@ private int runAdminCommandOnLocalNode(Node node, StringBuilder output, lastCommandRun = commandListToString(fullcommand); - trace("Running command locally: " + lastCommandRun); + LOG.log(Level.DEBUG,"Running command locally: {0}", lastCommandRun); ProcessManager pm = new ProcessManager(fullcommand); pm.setStdinLines(stdinLines); @@ -156,7 +155,7 @@ private int runAdminCommandOnLocalNode(Node node, StringBuilder output, if (StringUtils.ok(stderr)) { if (output.length() > 0) { - output.append(NL); + output.append(System.lineSeparator()); } output.append(stderr); } @@ -169,23 +168,12 @@ private int runAdminCommandOnRemoteNode(Node node, StringBuilder output, List stdinLines) throws SSHCommandExecutionException, IllegalArgumentException, UnsupportedOperationException { - - // don't want to call a config object proxy more than absolutely necessary! - String type = node.getType(); - - if ("SSH".equals(type)) { - NodeRunnerSsh nrs = new NodeRunnerSsh(habitat, logger); - int result = nrs.runAdminCommandOnRemoteNode(node, output, args, stdinLines); - lastCommandRun = nrs.getLastCommandRun(); - return result; - } - - throw new UnsupportedOperationException("Node is not of type SSH"); + NodeRunnerSsh nrs = new NodeRunnerSsh(); + int result = nrs.runAdminCommandOnRemoteNode(node, output, args, stdinLines); + lastCommandRun = nrs.getLastCommandRun(); + return result; } - private void trace(String s) { - logger.fine(String.format("%s: %s", this.getClass().getSimpleName(), s)); - } private String commandListToString(List command) { StringBuilder fullCommand = new StringBuilder(); diff --git a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/connect/NodeRunnerSsh.java b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/connect/NodeRunnerSsh.java index 2b4e8b19c4c..e99b373e5e3 100644 --- a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/connect/NodeRunnerSsh.java +++ b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/connect/NodeRunnerSsh.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025 Contributors to the Eclipse Foundation * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -16,134 +17,102 @@ package org.glassfish.cluster.ssh.connect; -import com.jcraft.jsch.JSchException; import com.sun.enterprise.config.serverbeans.Node; -import com.sun.enterprise.util.StringUtils; import com.sun.enterprise.util.SystemPropertyConstants; -import java.io.ByteArrayOutputStream; -import java.io.IOException; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.util.ArrayList; import java.util.List; -import java.util.logging.Logger; import org.glassfish.api.admin.SSHCommandExecutionException; +import org.glassfish.cluster.ssh.launcher.SSHException; import org.glassfish.cluster.ssh.launcher.SSHLauncher; +import org.glassfish.cluster.ssh.launcher.SSHSession; import org.glassfish.common.util.admin.AsadminInput; -import org.glassfish.hk2.api.ServiceLocator; -public class NodeRunnerSsh { +import static java.lang.System.Logger.Level.DEBUG; - private ServiceLocator habitat; - private Logger logger; - - private String lastCommandRun = null; +/** + * This class is responsible for running asadmin commands on SSH nodes. + */ +public class NodeRunnerSsh { + private static final Logger LOG = System.getLogger(NodeRunnerSsh.class.getName()); + private String lastCommandRun; private int commandStatus; - private SSHLauncher sshL = null; - - public NodeRunnerSsh(ServiceLocator habitat, Logger logger) { - this.logger = logger; - this.habitat = habitat; - } - - + /** + * @param node + * @return false if node is null or is not of SSH type. + */ public boolean isSshNode(Node node) { - if (node == null) { throw new IllegalArgumentException("Node is null"); } - if (node.getType() ==null) - return false; - return node.getType().equals("SSH"); + return "SSH".equals(node.getType()); } + /** + * @return the last command we tried to execute. Useful for error logs. + */ String getLastCommandRun() { return lastCommandRun; } + /** + * Runs the command on the SSH node. + * + * @param node + * @param output This will contain command output after execution. + * @param args + * @param stdinLines + * @return exit code + * @throws SSHCommandExecutionException communication or command failed. + * @throws UnsupportedOperationException incompatible node. + */ public int runAdminCommandOnRemoteNode(Node node, StringBuilder output, - List args, - List stdinLines) throws - SSHCommandExecutionException, IllegalArgumentException, - UnsupportedOperationException { - + List args, List stdinLines) + throws SSHCommandExecutionException, UnsupportedOperationException { args.add(0, AsadminInput.CLI_INPUT_OPTION); args.add(1, AsadminInput.SYSTEM_IN_INDICATOR); // specified to read from System.in - if (! isSshNode(node)) { - throw new UnsupportedOperationException( - "Node is not of type SSH"); - } - - String installDir = node.getInstallDirUnixStyle() + "/" + - SystemPropertyConstants.getComponentName(); - if (!StringUtils.ok(installDir)) { - throw new IllegalArgumentException("Node does not have an installDir"); + if (!isSshNode(node)) { + throw new UnsupportedOperationException("Node is not of type SSH"); } - List fullcommand = new ArrayList(); - - // We can just use "nadmin" even on Windows since the SSHD provider - // will locate the command (.exe or .bat) for us - fullcommand.add(installDir + "/lib/nadmin"); - fullcommand.addAll(args); - - try{ + String installDir = node.getInstallDirUnixStyle() + "/" + SystemPropertyConstants.getComponentName(); + List fullcommand = new ArrayList<>(); + final SSHLauncher sshL = new SSHLauncher(node); + try { + // We can just use "nadmin" even on Windows since the SSHD provider + // will locate the command (.exe or .bat) for us + fullcommand.add(installDir + "/lib/nadmin"); + fullcommand.addAll(args); lastCommandRun = commandListToString(fullcommand); - trace("Running command on " + node.getNodeHost() + ": " + - lastCommandRun); - sshL=habitat.getService(SSHLauncher.class); - sshL.init(node, logger); - - ByteArrayOutputStream outStream = new ByteArrayOutputStream(); - commandStatus = sshL.runCommand(fullcommand, outStream, stdinLines); - output.append(outStream.toString()); - return commandStatus; - - }catch (JSchException | IOException ex) { - String m1 = " Command execution failed. " +ex.getMessage(); - String m2 = ""; - Throwable e2 = ex.getCause(); - if(e2 != null) { - m2 = e2.getMessage(); - } - logger.severe("Command execution failed for "+ lastCommandRun); - SSHCommandExecutionException cee = new SSHCommandExecutionException(StringUtils.cat(":", - m1, m2)); - cee.setSSHSettings(sshL.toString()); - cee.setCommandRun(lastCommandRun); - throw cee; - - } catch (java.lang.InterruptedException ei){ - ei.printStackTrace(); - String m1 = ei.getMessage(); - String m2 = ""; - Throwable e2 = ei.getCause(); - if(e2 != null) { - m2 = e2.getMessage(); + LOG.log(DEBUG, () -> "Running command on " + node.getNodeHost() + ": " + lastCommandRun); + final StringBuilder commandOutput = new StringBuilder(); + try (SSHSession session = sshL.openSession()) { + commandStatus = session.exec(fullcommand, stdinLines, commandOutput); } - logger.severe("Command interrupted "+ lastCommandRun); - SSHCommandExecutionException cee = new SSHCommandExecutionException(StringUtils.cat(":", - m1, m2)); + output.append(commandOutput); + return commandStatus; + } catch (SSHException e) { + LOG.log(Level.ERROR, "Command execution failed for " + lastCommandRun, e); + SSHCommandExecutionException cee = new SSHCommandExecutionException(e.getMessage(), e); cee.setSSHSettings(sshL.toString()); cee.setCommandRun(lastCommandRun); throw cee; } } - private void trace(String s) { - logger.fine(String.format("%s: %s", this.getClass().getSimpleName(), s)); - } + private String commandListToString(List command) { StringBuilder fullCommand = new StringBuilder(); - for (String s : command) { - fullCommand.append(" "); + fullCommand.append(' '); fullCommand.append(s); } - return fullCommand.toString(); } } diff --git a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/connect/Strings.java b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/connect/Strings.java index 3ab558f840e..e3d2043e629 100644 --- a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/connect/Strings.java +++ b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/connect/Strings.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025 Contributors to the Eclipse Foundation * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -13,8 +14,8 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 */ - package org.glassfish.cluster.ssh.connect; + import com.sun.enterprise.universal.i18n.LocalStringsImpl; /** @@ -26,17 +27,17 @@ */ final class Strings { + final private static LocalStringsImpl strings = new LocalStringsImpl(Strings.class); + private Strings() { // no instances allowed! } - final static String get(String indexString) { + static final String get(String indexString) { return strings.get(indexString); } - final static String get(String indexString, Object... objects) { + static final String get(String indexString, Object... objects) { return strings.get(indexString, objects); } - - final private static LocalStringsImpl strings = new LocalStringsImpl(Strings.class); } diff --git a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/GlassFishSshUserInfo.java b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/GlassFishSshUserInfo.java new file mode 100644 index 00000000000..d7e588d559a --- /dev/null +++ b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/GlassFishSshUserInfo.java @@ -0,0 +1,70 @@ +/* + * 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.cluster.ssh.launcher; + +import com.jcraft.jsch.UserInfo; + +import java.lang.System.Logger; +import java.util.concurrent.atomic.AtomicInteger; + +import static java.lang.System.Logger.Level.DEBUG; + +/** + * This object can be used to fake user input for Jsch. + */ +class GlassFishSshUserInfo implements UserInfo { + + private static final Logger LOG = System.getLogger(GlassFishSshUserInfo.class.getName()); + private final AtomicInteger counter = new AtomicInteger(0); + + @Override + public String getPassphrase() { + counter.incrementAndGet(); + LOG.log(DEBUG, "getPassphrase(); counter: " + counter); + return null; + } + + @Override + public String getPassword() { + LOG.log(DEBUG, "getPassword()"); + return null; + } + + @Override + public boolean promptPassword(String message) { + LOG.log(DEBUG, "promptPassword(message={0})", message); + return false; + } + + @Override + public boolean promptPassphrase(String message) { + LOG.log(DEBUG, "promptPassphrase(message={0})", message); + return true; + } + + @Override + public boolean promptYesNo(String message) { + LOG.log(DEBUG, "promptYesNo(message={0})", message); + return true; + } + + @Override + public void showMessage(String message) { + LOG.log(DEBUG, "showMessage(message={0})", message); + } + +} diff --git a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/JavaSystemJschLogger.java b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/JavaSystemJschLogger.java index 75b14c11d4f..cd526ce9e32 100644 --- a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/JavaSystemJschLogger.java +++ b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/JavaSystemJschLogger.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2023, 2025 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 @@ -20,31 +20,34 @@ import java.lang.System.Logger; import java.lang.System.Logger.Level; -/* JSCH triggers a lot of unimportant messages, which have a lower level than issued by JSCH - For example, for a single SSH connection, it triggers 1 WARNING about adding a host to the list of known hosts, - 47 INFO messages, and 6 DEBUG messages. Therefore we decrease their level to match their real severity. - - The log levels from JSCH are mapped to these levels in GlassFish: - - JSCH DEBUG level -> TRACE level in GlassFish - JSCH INFO level -> DEBUG level in GlassFish - JSCH WARN level -> INFO level in GlassFish - JSCH ERROR level -> WARNING level in GlassFish - JSCH FATAL level -> ERROR/SEVERE level in GlassFish +import org.glassfish.internal.api.RelativePathResolver; +/** + * JSCH triggers a lot of unimportant messages, which have a lower level than issued by JSCH. + * For example, for a single SSH connection, it triggers 1 WARNING about adding a host to the list + * of known hosts, 47 INFO messages, and 6 DEBUG messages. Therefore we decrease their level to + * match their real severity. + *

+ * The log levels from JSCH are mapped to these levels in GlassFish: + *

    + *
  • JSCH DEBUG level -> TRACE level in GlassFish + *
  • JSCH INFO level -> DEBUG level in GlassFish + *
  • JSCH WARN level -> INFO level in GlassFish + *
  • JSCH ERROR level -> WARNING level in GlassFish + *
  • JSCH FATAL level -> ERROR/SEVERE level in GlassFish + *
*/ public class JavaSystemJschLogger implements com.jcraft.jsch.Logger { private final Logger logger; + /** + * The logger name to be used is com.jcraft.jsch + */ public JavaSystemJschLogger() { this.logger = System.getLogger(JSch.class.getName()); } - public JavaSystemJschLogger(String loggerName) { - this.logger = System.getLogger(loggerName); - } - @Override public boolean isEnabled(int jschLevel) { return logger.isLoggable(getSystemLoggerLevel(jschLevel)); @@ -64,7 +67,36 @@ public void log(int jschLevel, String message, Throwable cause) { } } - static Level getSystemLoggerLevel(int jschLevel) { + + + /** + * Return a version of the password that is printable. + * + * @param privateValue password, keyphrase, etc. + * @return printable version of password + */ + public static String maskForLogging(String privateValue) { + // We only display the password if it is an alias, else + // we display "". + String printable = "null"; + if (privateValue != null) { + if (isPasswordAlias(privateValue)) { + printable = privateValue; + } else { + printable = ""; + } + } + return printable; + } + + private static boolean isPasswordAlias(String alias) { + // Check if the passed string is specified using the alias syntax + String aliasName = RelativePathResolver.getAlias(alias); + return aliasName != null; + } + + + private static Level getSystemLoggerLevel(int jschLevel) { switch (jschLevel) { case DEBUG: return Level.TRACE; @@ -80,5 +112,4 @@ static Level getSystemLoggerLevel(int jschLevel) { return Level.TRACE; } } - } diff --git a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/OperatingSystem.java b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/OperatingSystem.java new file mode 100644 index 00000000000..43501b66deb --- /dev/null +++ b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/OperatingSystem.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 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.cluster.ssh.launcher; + +import java.util.Locale; + +/** + * This enum serves to distinguish operating system capabilities over SSH. + */ +public enum OperatingSystem { + /** Linux based operating systems usually use Bash */ + LINUX, + /** windows based operating systems usually use PowerShell and cmd.exe and don't support POSIX permissions. */ + WINDOWS, + /** + * Generic operating systems are big unknown. + * This enum can be used when we don't care about system capabilities. + */ + GENERIC, + ; + + static OperatingSystem parse(String osNameProperty) { + String osName = osNameProperty.toLowerCase(Locale.ENGLISH); + if (osName.contains("linux")) { + return LINUX; + } + if (osName.contains("win")) { + return WINDOWS; + } + return GENERIC; + } +} diff --git a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/RemoteSystemCapabilities.java b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/RemoteSystemCapabilities.java new file mode 100644 index 00000000000..ff7addf0377 --- /dev/null +++ b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/RemoteSystemCapabilities.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025 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.cluster.ssh.launcher; + +import java.lang.Runtime.Version; +import java.nio.charset.Charset; + +/** + * Detected capabilities of the remote operating system. + */ +public class RemoteSystemCapabilities { + + private final String javaHome; + private final Version javaVersion; + private final OperatingSystem operatingSystem; + private final Charset charset; + + /** + * @param javaHome - it is a string, because we want to use it as it is reported from the operating system. + * @param javaVersion + * @param operatingSystem + * @param charset + */ + RemoteSystemCapabilities(String javaHome, Version javaVersion, OperatingSystem operatingSystem, Charset charset) { + this.javaHome = javaHome; + this.javaVersion = javaVersion; + this.operatingSystem = operatingSystem; + this.charset = charset; + } + + + /** + * @return true if the java command is supported by the remote operating system. + */ + public boolean isJavaSupported() { + return javaHome != null && javaVersion != null; + } + + + /** + * @return true if the remote system is NOT Windows. + */ + public boolean isChmodSupported() { + return operatingSystem != OperatingSystem.WINDOWS; + } + + + /** + * @return detected operating system + * @see OperatingSystem + */ + public OperatingSystem getOperatingSystem() { + return operatingSystem; + } + + + /** + * @return detected charset used by the remote system. + */ + public Charset getCharset() { + return charset; + } + + + @Override + public String toString() { + return getClass().getSimpleName() + "[os=" + operatingSystem + ", charset=" + charset + ", javaHome=" + javaHome + + ", javaVersion=" + javaVersion + "]"; + } +} diff --git a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/SSHException.java b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/SSHException.java new file mode 100644 index 00000000000..725f90ed2bd --- /dev/null +++ b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/SSHException.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2025 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.cluster.ssh.launcher; + +import com.jcraft.jsch.JSchException; + +import java.io.IOException; + +/** + * Communication failure or unsuccessful command. + * As usually we lack to have detailed information about what happened, this exception + * adds the cause to its message, separated just by a space. + * Exception instances then can be layered without losing the information. + */ +public class SSHException extends IOException { + private static final long serialVersionUID = 1L; + + /** + * @param message + * @param cause the description of the cause will be appended to the message in parameter. + */ + public SSHException(String message, Exception cause) { + super(message + ' ' + toCauseMessage(cause), cause); + } + + /** + * @param message what happened. + */ + public SSHException(String message) { + super(message); + } + + + private static String toCauseMessage(Exception e) { + if (e instanceof SSHException || e instanceof JSchException) { + return e.getMessage(); + } + // note: SftpException contains also an error id. + return e.toString(); + } +} diff --git a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/SSHKeyInstaller.java b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/SSHKeyInstaller.java new file mode 100644 index 00000000000..03d49b6ba80 --- /dev/null +++ b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/SSHKeyInstaller.java @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2025 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.cluster.ssh.launcher; + +import com.sun.enterprise.universal.process.ProcessManager; +import com.sun.enterprise.universal.process.ProcessManagerException; +import com.sun.enterprise.universal.process.ProcessUtils; +import com.sun.enterprise.util.OS; +import com.sun.enterprise.util.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.lang.System.Logger; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.glassfish.cluster.ssh.sftp.SFTPClient; + +import static java.lang.System.Logger.Level.DEBUG; +import static java.lang.System.Logger.Level.INFO; +import static java.lang.System.Logger.Level.WARNING; +import static org.glassfish.cluster.ssh.launcher.JavaSystemJschLogger.maskForLogging; +import static org.glassfish.cluster.ssh.launcher.SSHLauncher.SSH_DIR_NAME; + +/** + * This class serves to generate a private key and then upload it to the remote host using password + * authentication. + */ +public class SSHKeyInstaller { + private static final Logger LOG = System.getLogger(SSHKeyInstaller.class.getName()); + + private static final String AUTH_KEY_FILE = "authorized_keys"; + private static final int DEFAULT_TIMEOUT_MSEC = 120000; // 2 minutes + private static final String SSH_KEYGEN = "ssh-keygen"; + + private final SSHLauncher ssh; + + + /** + * Creates this instance. + * + * @param ssh settings used to establish the connection. Note that after the installation it + * should not be used any more as this class serves for enabling the key + * authentication. + */ + public SSHKeyInstaller(SSHLauncher ssh) { + this.ssh = ssh; + } + + + /** + * Setting up the key involves the following steps: + * -If a key exists and we can connect using the key, do nothing. + * -Generate a key pair if there isn't one + * -Connect to remote host using password auth and do the following: + * 1. create .ssh directory if it doesn't exist + * 2. copy over the key as key.tmp + * 3. Append the key to authorized_keys file + * 4. Remove the temporary key file key.tmp + * 5. Fix permissions for home, .ssh and authorized_keys + * @param node - remote host + * @param pubKeyFile - .pub file + * @param generateKey - flag to indicate if key needs to be generated or not + * @param passwd - ssh user password + * @throws IOException + */ + public void setupKey(String node, File pubKeyFile, boolean generateKey, String passwd) throws IOException { + File keyFile = ssh.getKeyFile(); + String userName = ssh.getUserName(); + LOG.log(DEBUG, () -> "Key = " + keyFile); + if (keyFile.exists()) { + if (ssh.checkConnection()) { + throw new IOException("SSH public key authentication is already configured for " + userName + "@" + node); + } + } else { + if (generateKey) { + if (!generateKeyPair(keyFile)) { + throw new IOException("SSH key pair generation failed. Please generate key manually."); + } + } else { + throw new IOException("SSH key pair not present. Please generate a key pair manually or specify an existing one and re-run the command."); + } + } + + //password is must for key distribution + if (passwd == null) { + throw new IOException("SSH password is required for distributing the public key. You can specify the SSH password in a password file and pass it through --passwordfile option."); + } + try (SSHSession session = ssh.openSession(passwd); SFTPClient sftp = session.createSFTPClient()) { + + // fixes .ssh file mode + setupSSHDir(); + + final File pubKey; + if (pubKeyFile == null) { + pubKey = new File(keyFile.getParent(), keyFile.getName() + ".pub"); + } else { + pubKey = pubKeyFile; + } + if (!pubKey.exists()) { + throw new IOException("Public key file " + pubKey + " does not exist."); + } + + final Path remoteSshDir; + try { + remoteSshDir = sftp.getHome().resolve(SSH_DIR_NAME); + } catch (InvalidPathException e) { + throw new SSHException("Could not resolve ssh home directory of the remote user.", e); + } + final RemoteSystemCapabilities capabilities = ssh.getCapabilities(); + if (!sftp.exists(remoteSshDir)) { + LOG.log(DEBUG, () -> SSH_DIR_NAME + " does not exist"); + sftp.mkdirs(remoteSshDir); + if (capabilities.isChmodSupported()) { + sftp.chmod(remoteSshDir, 0700); + } + } + + // copy over the public key to remote host + final Path remoteKeyTmp = remoteSshDir.resolve("key.tmp"); + sftp.put(pubKey, remoteKeyTmp); + if (capabilities.isChmodSupported()) { + sftp.chmod(remoteKeyTmp, 0600); + } + + // append the public key file contents to authorized_keys file on remote host + final Path authKeyFile = remoteSshDir.resolve(AUTH_KEY_FILE); + String mergeCommand = "cat " + remoteKeyTmp + " >> " + authKeyFile; + LOG.log(DEBUG, () -> "mergeCommand = " + mergeCommand); + if (session.exec(mergeCommand) != 0) { + throw new IOException("Failed to propogate the public key " + pubKey + " to " + ssh.getHost()); + } + LOG.log(INFO, "Copied keyfile " + pubKey + " to " + userName + "@" + ssh.getHost()); + + try { + sftp.rm(remoteKeyTmp); + LOG.log(DEBUG, "Removed the temporary key file on remote host"); + } catch (SSHException e) { + LOG.log(WARNING, "Failed to remove the public key file key.tmp on remote host " + ssh.getHost()); + } + + if (capabilities.isChmodSupported()) { + LOG.log(INFO, "Fixing file permissions for home(755), .ssh(700) and authorized_keys file(600)"); + sftp.cd(sftp.getHome()); + sftp.chmod(remoteSshDir.getParent(), 0755); + sftp.chmod(remoteSshDir, 0700); + sftp.chmod(authKeyFile, 0600); + } + } + } + + + /** + * Invoke ssh-keygen using ProcessManager API + */ + private boolean generateKeyPair(File keyFile) throws IOException { + File keygenCmd = findSSHKeygen(); + LOG.log(DEBUG, () -> "Using " + keygenCmd + " to generate key pair"); + + if (!setupSSHDir()) { + throw new IOException("Failed to set proper permissions on .ssh directory"); + } + + StringBuilder log = new StringBuilder(); + List cmdLine = new ArrayList<>(); + cmdLine.add(keygenCmd.getAbsolutePath()); + log.append(keygenCmd); + cmdLine.add("-t"); + log.append(" ").append("-t"); + cmdLine.add("rsa"); + log.append(" ").append("rsa"); + + cmdLine.add("-N"); + log.append(" ").append("-N"); + if (ssh.getKeyFilePassphrase() == null) { + log.append(" ").append("\"\""); + // special handling for empty passphrase on Windows + if(OS.isWindows()) { + cmdLine.add("\"\""); + } else { + cmdLine.add(""); + } + } else { + cmdLine.add(ssh.getKeyFilePassphrase()); + log.append(" ").append(maskForLogging(ssh.getKeyFilePassphrase())); + } + cmdLine.add("-f"); + log.append(" ").append("-f"); + cmdLine.add(keyFile.getAbsolutePath()); + log.append(" ").append(keyFile); + //cmdLine.add("-vvv"); + + ProcessManager pm = new ProcessManager(cmdLine); + + LOG.log(DEBUG, () -> "Command = " + log); + pm.setTimeoutMsec(DEFAULT_TIMEOUT_MSEC); + + if (LOG.isLoggable(DEBUG)) { + pm.setEcho(true); + } else { + pm.setEcho(false); + } + + int exit; + try { + exit = pm.execute(); + } catch (ProcessManagerException ex) { + LOG.log(DEBUG, () -> "Error while executing ssh-keygen.", ex); + exit = 1; + } + if (exit == 0) { + LOG.log(INFO, () -> keygenCmd + " successfully generated the identification " + keyFile); + } else { + LOG.log(WARNING, () -> keygenCmd + " failed. It produced standard error output:\n" + pm.getStderr() + + "\n and standard output:\n" + pm.getStdout()); + } + return exit == 0; + } + + + /** + * Create .ssh directory on this host and set the permissions correctly + */ + private boolean setupSSHDir() throws IOException { + boolean ret = true; + File f = new File(FileUtils.USER_HOME, SSH_DIR_NAME); + if (!FileUtils.safeIsDirectory(f)) { + if (!f.mkdirs()) { + throw new IOException("Failed to create " + f.getPath()); + } + LOG.log(INFO, "Created directory {0}", f); + } + + if (!f.setReadable(false, false) || !f.setReadable(true)) { + ret = false; + } + + if (!f.setWritable(false,false) || !f.setWritable(true)) { + ret = false; + } + + if (!f.setExecutable(false, false) || !f.setExecutable(true)) { + ret = false; + } + + LOG.log(DEBUG, "Fixed the .ssh directory permissions to 0700"); + return ret; + } + + + /** + * Method to locate ssh-keygen. If found in path, return the same or else look + * for it in a pre defined list of search paths. + * @return ssh-keygen command + */ + private File findSSHKeygen() { + List paths = new ArrayList<>(List.of("/usr/bin/", "/usr/local/bin/")); + if (OS.isWindows()) { + paths.add("C:/cygwin/bin/"); + //Windows MKS Toolkit install path + String mks = System.getenv("ROOTDIR"); + if (mks != null) { + paths.add(mks + "/bin/"); + } + } + + LOG.log(DEBUG, () -> "Paths = " + paths); + + File exe = ProcessUtils.getExe(SSH_KEYGEN); + if( exe != null){ + return exe; + } + + for (String path : paths) { + File f = new File(path, SSH_KEYGEN); + if (f.canExecute()) { + return f.getAbsoluteFile(); + } + } + return new File(SSH_KEYGEN); + } + +} diff --git a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/SSHLauncher.java b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/SSHLauncher.java index 3d7ee8c3015..0b7e163d2ba 100644 --- a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/SSHLauncher.java +++ b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/SSHLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -17,147 +17,102 @@ package org.glassfish.cluster.ssh.launcher; -import com.jcraft.jsch.ChannelExec; -import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; -import com.jcraft.jsch.SftpException; import com.sun.enterprise.config.serverbeans.Node; import com.sun.enterprise.config.serverbeans.SshAuth; import com.sun.enterprise.config.serverbeans.SshConnector; -import com.sun.enterprise.universal.process.ProcessManager; -import com.sun.enterprise.universal.process.ProcessManagerException; -import com.sun.enterprise.universal.process.ProcessUtils; -import com.sun.enterprise.util.OS; -import com.sun.enterprise.util.StringUtils; import com.sun.enterprise.util.io.FileUtils; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.lang.Runtime.Version; +import java.lang.System.Logger; +import java.nio.charset.Charset; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; +import java.util.Map; +import java.util.function.Function; import org.glassfish.cluster.ssh.sftp.SFTPClient; import org.glassfish.cluster.ssh.util.SSHUtil; -import org.glassfish.hk2.api.PerLookup; import org.glassfish.internal.api.RelativePathResolver; -import org.jvnet.hk2.annotations.Service; + +import static java.lang.System.Logger.Level.DEBUG; +import static java.lang.System.Logger.Level.ERROR; +import static java.lang.System.Logger.Level.INFO; +import static java.lang.System.Logger.Level.WARNING; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.glassfish.cluster.ssh.launcher.JavaSystemJschLogger.maskForLogging; +import static org.glassfish.cluster.ssh.launcher.OperatingSystem.GENERIC; /** * @author Rajiv Mordani, Krishna Deepak */ -@Service(name="SSHLauncher") -@PerLookup public class SSHLauncher { - private static final String SSH_DIR_NAME = ".ssh"; - private static final String AUTH_KEY_FILE = "authorized_keys"; - private static final int DEFAULT_TIMEOUT_MSEC = 120000; // 2 minutes - private static final String SSH_KEYGEN = "ssh-keygen"; - private static final char LINE_SEP = System.getProperty("line.separator").charAt(0); - - /** - * The host name which to connect to via ssh - */ - private String host; - - /** - * The port on which the ssh daemon is running - */ - private int port; - - /** - * The user name to use for authenticating with the ssh daemon - */ - private String userName; + static final String SSH_DIR_NAME = ".ssh"; + private static final int SSH_PORT_DEFAULT = 22; - /** - * The name of private key file. - */ - private File keyFile; + private static final Logger LOG = System.getLogger(SSHLauncher.class.getName()); - /** - * The connection object that represents the connection to the host - * via ssh - */ - private Session session; + static { + JSch.setLogger(new JavaSystemJschLogger()); + } - private String authType; + /** The host name which to connect to via ssh */ + private final String host; - private String keyPassPhrase; + /** The port on which the ssh daemon is running */ + private final int port; - private String knownHostsLocation; + /** The user name to use for authenticating with the ssh daemon */ + private final String userName; - private Logger logger; + /** The name of private key file. */ + private final File keyFile; - private String password; + private final String keyPassPhraseParameter; + private final String keyPassPhrase; - // Password before it has been expanded. Used for debugging. - private String rawPassword = null; - private String rawKeyPassPhrase = null; + /** Password before it has been expanded. */ + private final String passwordParameter; + private final String password; + private final RemoteSystemCapabilities capabilities; - public void init(Logger logger) { - this.logger = logger; - } /** - * Initialize the SSHLauncher use a Node config object + * Initialize the SSHLauncher use a {@link Node} config object + * * @param node - * @param logger */ - public void init(Node node, Logger logger) { - this.logger = logger; - int port; - String host; - - SshConnector connector = node.getSshConnector(); - - host = connector.getSshHost(); - if (SSHUtil.checkString(host) != null) { - this.host = host; - } else { - this.host = node.getNodeHost(); - } - if (logger.isLoggable(Level.FINE)) { - logger.fine("Connecting to host " + host); - } - - //XXX Why do we need this again? This is already done above and set to host - String sshHost = connector.getSshHost(); - if (sshHost != null) { - this.host = sshHost; - } - - SshAuth sshAuth = connector.getSshAuth(); - String userName = null; - if (sshAuth != null) { - userName = sshAuth.getUserName(); - this.keyFile = sshAuth.getKeyfile() == null ? null : new File(sshAuth.getKeyfile()); - this.rawPassword = sshAuth.getPassword(); - this.rawKeyPassPhrase = sshAuth.getKeyPassphrase(); - } - try { - port = Integer.parseInt(connector.getSshPort()); - } catch(NumberFormatException nfe) { - port = 22; - } - - init(userName, this.host, port, this.rawPassword, keyFile, - this.rawKeyPassPhrase, logger); + public SSHLauncher(Node node) { + final SshConnector connector = node.getSshConnector(); + this.host = getHost(node); + LOG.log(DEBUG, "Connecting to host {0}", host); + this.port = getPort(connector); + + final SshAuth sshAuth = connector.getSshAuth(); + this.userName = getUserName(sshAuth == null ? null : sshAuth.getUserName()); + this.keyFile = sshAuth == null || sshAuth.getKeyfile() == null + ? SSHUtil.getExistingKeyFile() + : new File(sshAuth.getKeyfile()); + this.passwordParameter = sshAuth == null ? null : sshAuth.getPassword(); + this.password = passwordParameter == null || passwordParameter.isEmpty() + ? null + : expandPasswordAlias(passwordParameter); + this.keyPassPhraseParameter = sshAuth == null ? null : sshAuth.getKeyPassphrase(); + this.keyPassPhrase = keyPassPhraseParameter == null || keyPassPhraseParameter.isEmpty() + ? null + : expandPasswordAlias(keyPassPhraseParameter); + this.capabilities = analyzeRemote(this.host, this.port, this.userName, this.password, this.keyFile, + this.keyPassPhrase); + LOG.log(DEBUG, "SSH client configuration: {0}", this); } + /** - * Initialize the SSHLauncher using a private key file + * Initialize the SSHLauncher * * @param userName * @param host @@ -165,561 +120,97 @@ public void init(Node node, Logger logger) { * @param password * @param keyFile * @param keyPassPhrase - * @param logger */ - public void init(String userName, String host, int port, String password, File keyFile, String keyPassPhrase, Logger logger) { - this.port = port == 0 ? 22 : port; - + public SSHLauncher(String userName, String host, int port, String password, File keyFile, String keyPassPhrase) { this.host = host; + this.port = port == 0 ? SSH_PORT_DEFAULT : port; this.keyFile = keyFile == null ? SSHUtil.getExistingKeyFile(): keyFile; - this.logger = logger; - this.userName = SSHUtil.checkString(userName) == null ? System.getProperty("user.name") : userName; - this.rawPassword = password; - this.password = expandPasswordAlias(password); - this.rawKeyPassPhrase = keyPassPhrase; - this.keyPassPhrase = expandPasswordAlias(keyPassPhrase); - - File knownHosts = FileUtils.USER_HOME.toPath().resolve(Path.of(SSH_DIR_NAME, "known_hosts")).toFile(); - if (knownHosts.exists()) { - knownHostsLocation = knownHosts.getAbsolutePath(); - } - logger.log(Level.FINER, "SSH info is {0}", this); + this.userName = getUserName(userName); + this.passwordParameter = password; + this.password = password == null || password.isEmpty() ? null : expandPasswordAlias(password); + this.keyPassPhraseParameter = keyPassPhrase; + this.keyPassPhrase = keyPassPhrase == null || keyPassPhrase.isEmpty() + ? null + : expandPasswordAlias(keyPassPhrase); + this.capabilities = analyzeRemote(this.host, this.port, this.userName, this.password, this.keyFile, + this.keyPassPhrase); + LOG.log(DEBUG, "SSH client configuration: {0}", this); } - private void openConnection() throws JSchException { - assert session == null; - JSch jsch = new JSch(); - JSch.setLogger(new JavaSystemJschLogger(logger != null ? logger.getName() + ".ssh" : JSch.class.getName())); - - // Client Auth - String message = ""; - boolean triedAuthentication = false; - // Private key file is provided - Public Key Authentication - if (keyFile != null) { - if (logger.isLoggable(Level.FINER)) { - logger.finer("Specified key file is " + keyFile); - } - if (keyFile.exists()) { - triedAuthentication = true; - if (logger.isLoggable(Level.FINER)) { - logger.finer("Specified key file exists at " + keyFile); - } - jsch.addIdentity(keyFile.getAbsolutePath(), keyPassPhrase); - } else { - message = "Specified key file does not exist \n"; - } - } else if (SSHUtil.checkString(password) == null) { - message += "No key or password specified - trying default keys \n"; - logger.fine("keyfile and password are null. Will try to authenticate with default key file if available"); - // check the default key locations if no authentication - // method is explicitly configured. - Path home = FileUtils.USER_HOME.toPath(); - for (String keyName : Arrays.asList("id_rsa", "id_dsa", "identity")) { - message += "Tried to authenticate using " + keyName + "\n"; - File key = home.resolve(Path.of(SSH_DIR_NAME, keyName)).toFile(); - if (key.exists()) { - triedAuthentication = true; - jsch.addIdentity(key.getAbsolutePath()); - } - } - } - - session = jsch.getSession(userName, host, port); - session.setConfig("StrictHostKeyChecking", "no"); - // Password Auth - if (SSHUtil.checkString(password) != null) { - if (logger.isLoggable(Level.FINE)) { - logger.fine("Authenticating with password " + getPrintablePassword(password)); - } - triedAuthentication = true; - session.setPassword(password); - } - if (!triedAuthentication) { - if (logger.isLoggable(Level.FINE)) { - logger.fine("Could not authenticate"); - } - throw new JSchException("Could not authenticate. " + message); - } - SSHUtil.register(session); - session.connect(); - } /** - * Executes a command on the remote system via ssh, optionally sending - * lines of data to the remote process's System.in. - * - * @param command the command to execute in the form of an argv style list - * @param os stream to receive the output from the command - * @param stdinLines optional data to be sent to the process's System.in - * stream; null if no input should be sent - * @return - * @throws IOException - * @throws InterruptedException + * @return the remote host or IP address */ - public int runCommand(List command, OutputStream os, - List stdinLines) throws JSchException, IOException, - InterruptedException - { - return runCommand(commandListToQuotedString(command), os, stdinLines); + public String getHost() { + return this.host; } - public int runCommand(List command, OutputStream os) - throws JSchException, IOException, - InterruptedException - { - return runCommand(command, os, null); - } - - /** - * WARNING! This method does not handle paths with spaces in them. - * To use this method you must make sure all paths in the command string - * are quoted correctly. Otherwise use the methods that take command as - * a list instead. - */ - public int runCommand(String command, OutputStream os) throws JSchException, IOException, - InterruptedException - { - return runCommand(command, os, null); - } /** - * Executes a command on the remote system via ssh, optionally sending - * lines of data to the remote process's System.in. - * - * WARNING! This method does not handle paths with spaces in them. - * To use this method you must make sure all paths in the command string - * are quoted correctly. Otherwise use the methods that take command as - * a list instead. - * - * @param command the command to execute - * @param os stream to receive the output from the command - * @param stdinLines optional data to be sent to the process's System.in stream; null if no input should be sent - * @return - * @throws IOException - * @throws InterruptedException + * @return the remote port supporting SSH */ - public int runCommand(String command, OutputStream os, - List stdinLines) throws JSchException, IOException, - InterruptedException - { - command = SFTPClient.normalizePath(command); - return runCommandAsIs(command, os, stdinLines); - } - - /** - * Executes a command on the remote system via ssh without normalizing - * the command line - * - * @param command the command to execute - * @param os stream to receive the output from the command - * @param stdinLines optional data to be sent to the process's System.in - * stream; null if no input should be sent - * @return - * @throws IOException - * @throws InterruptedException - **/ - public int runCommandAsIs(List command, OutputStream os, - List stdinLines) throws JSchException, IOException, - InterruptedException - { - return runCommandAsIs(commandListToQuotedString(command), os, stdinLines); + public int getPort() { + return this.port; } - private int runCommandAsIs(String command, OutputStream os, - List stdinLines) throws JSchException, IOException, - InterruptedException - { - if (logger.isLoggable(Level.FINER)) { - logger.finer("Running command " + command + " on host: " + this.host); - } - boolean createNewSession = false; - if (session == null) { - createNewSession = true; - } - if(createNewSession) { - openConnection(); - } - - int status = exec(command, os, listInputStream(stdinLines)); - - if(createNewSession) { - SSHUtil.unregister(session); - session = null; - } - return status; - } /** - * To be called for after opening the connection using openConnection() - * - * @param command - * @param os - * @param is - * @return - * @throws JSchException - * @throws IOException - * @throws InterruptedException + * @return ssh login */ - private int exec(final String command, final OutputStream os, - final InputStream is) - throws JSchException, IOException, InterruptedException { - ChannelExec execChannel = (ChannelExec) session.openChannel("exec"); - try { - execChannel.setInputStream(is); - execChannel.setCommand(command); - InputStream in = execChannel.getInputStream(); - execChannel.connect(); - PumpThread t1 = new PumpThread(in, os); - t1.start(); - PumpThread t2 = new PumpThread(execChannel.getErrStream(), os); - t2.start(); - - t1.join(); - t2.join(); - - return execChannel.getExitStatus(); - } finally { - execChannel.disconnect(); - } + public String getUserName() { + return this.userName; } - /** - * To be called for after opening the connection using openConnection() - */ - private int exec(final String command, final OutputStream os) - throws JSchException, IOException, InterruptedException { - return exec(command, os, null); - } - private InputStream listInputStream(final List stdinLines) throws IOException { - if (stdinLines == null) { - return null; - } - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - for (String line : stdinLines) { - baos.write(line.getBytes()); - baos.write(LINE_SEP); - } - return new ByteArrayInputStream(baos.toByteArray()); + File getKeyFile() { + return this.keyFile; } - /** - * Pumps {@link InputStream} to {@link OutputStream}. - * - * @author Kohsuke Kawaguchi - */ - private static final class PumpThread extends Thread { - private final InputStream in; - private final OutputStream out; - - public PumpThread(InputStream in, OutputStream out) { - super("pump thread"); - this.in = in; - this.out = out; - } - @Override - public void run() { - byte[] buf = new byte[1024]; - try { - while(true) { - int len = in.read(buf); - if(len<0) { - in.close(); - return; - } - out.write(buf,0,len); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - public void pingConnection() throws JSchException, InterruptedException - { - logger.fine("Pinging connection for host: " + this.host); - openConnection(); - SSHUtil.unregister(session); - session = null; + String getKeyFilePassphrase() { + return this.keyPassPhrase; } /** - * Validate user provided args. - * Check connecton to host. - * Check that the install dir is correct - * - * @param landmarkPath must be relative to the installdir + * @return preloaded {@link RemoteSystemCapabilities} */ - public void validate(String host, int port, - String userName, String password, - File keyFile, String keyPassPhrase, - String installDir, String landmarkPath, - Logger logger) throws IOException { - boolean validInstallDir = false; - init(userName, host, port, password, keyFile, keyPassPhrase, logger); - - try { - openConnection(); - logger.fine("Connection settings valid"); - String testPath = installDir; - if (StringUtils.ok(testPath)) { - try (SFTPClient sftpClient = new SFTPClient(session)) { - // Validate if installDir exists - if (sftpClient.exists(testPath)) { - // installDir exists. Now check for landmark if provided - if (StringUtils.ok(landmarkPath)) { - testPath = installDir + "/" + landmarkPath; - } - validInstallDir = sftpClient.exists(testPath); - } else { - validInstallDir = false; - } - } - SSHUtil.unregister(session); - session = null; - - if (!validInstallDir) { - String msg = "Invalid install directory: could not find " + - testPath + " on " + host; - throw new FileNotFoundException(msg); - } - logger.fine("Node home validated"); - } - } catch (JSchException ex) { - throw new IOException(ex); - } catch (SftpException ex) { - throw new IOException(ex); - } - } - - - public SFTPClient getSFTPClient() throws JSchException { - openConnection(); - return new SFTPClient(session); - } - - public String expandPasswordAlias(String alias) { - - String expandedPassword = null; - - if (alias == null) { - return null; - } - - try { - expandedPassword = RelativePathResolver.getRealPasswordFromAlias(alias); - } catch (Exception e) { - logger.log(Level.WARNING, "Expansion failed for {0}: {1}", new Object[] {alias, e.getMessage()}); - return null; - } - - return expandedPassword; + public RemoteSystemCapabilities getCapabilities() { + return this.capabilities; } - public boolean isPasswordAlias(String alias) { - // Check if the passed string is specified using the alias syntax - String aliasName = RelativePathResolver.getAlias(alias); - return (aliasName != null); - } - - /** - * Return a version of the password that is printable. - * @param p password string - * @return printable version of password - */ - private String getPrintablePassword(String p) { - // We only display the password if it is an alias, else - // we display "". - String printable = "null"; - if (p != null) { - if (isPasswordAlias(p)) { - printable = p; - } else { - printable = ""; - } - } - return printable; - } - - /** - * Setting up the key involves the following steps: - * -If a key exists and we can connect using the key, do nothing. - * -Generate a key pair if there isn't one - * -Connect to remote host using password auth and do the following: - * 1. create .ssh directory if it doesn't exist - * 2. copy over the key as key.tmp - * 3. Append the key to authorized_keys file - * 4. Remove the temporary key file key.tmp - * 5. Fix permissions for home, .ssh and authorized_keys - * @param node - remote host - * @param pubKeyFile - .pub file - * @param generateKey - flag to indicate if key needs to be generated or not - * @param passwd - ssh user password - * @throws IOException - * @throws InterruptedException - */ - public void setupKey(String node, String pubKeyFile, boolean generateKey, String passwd) - throws IOException, InterruptedException { - - File key = keyFile; - if (logger.isLoggable(Level.FINER)) { - logger.finer("Key = " + keyFile); - } - if (key.exists()) { - if (checkConnection()) { - throw new IOException("SSH public key authentication is already configured for " + userName + "@" + node); - } - } else { - if (generateKey) { - if(!generateKeyPair()) { - throw new IOException("SSH key pair generation failed. Please generate key manually."); - } - } else { - throw new IOException("SSH key pair not present. Please generate a key pair manually or specify an existing one and re-run the command."); - } - } - - //password is must for key distribution - if (passwd == null) { - throw new IOException("SSH password is required for distributing the public key. You can specify the SSH password in a password file and pass it through --passwordfile option."); - } - try { - JSch jsch = new JSch(); - Session s1 = jsch.getSession(userName, host, port); - s1.setConfig("StrictHostKeyChecking", "no"); - s1.setPassword(passwd); - s1.connect(); - - if (!s1.isConnected()) { - throw new IOException("SSH password authentication failed for user " + userName + " on host " + node); - } - try (SFTPClient sftp = new SFTPClient(s1)) { - ChannelSftp sftpChannel = sftp.getSftpChannel(); - - this.session = s1; - - if (key.exists()) { - - // fixes .ssh file mode - setupSSHDir(); - - if (pubKeyFile == null) { - pubKeyFile = keyFile + ".pub"; - } - - File pubKey = new File(pubKeyFile); - if (!pubKey.exists()) { - throw new IOException("Public key file " + pubKeyFile + " does not exist."); - } - - try { - if (!sftp.exists(SSH_DIR_NAME)) { - logger.fine(SSH_DIR_NAME + " does not exist"); - sftpChannel.cd(sftpChannel.getHome()); - sftpChannel.mkdir(SSH_DIR_NAME); - sftpChannel.chmod(0700, SSH_DIR_NAME); - } - } catch (Exception e) { - throw new IOException("Error while creating .ssh directory on remote host: " + e.getMessage(), e); - } - - // copy over the public key to remote host - // scp.put(pubKey.getAbsolutePath(), "key.tmp", ".ssh", "0600"); - try { - sftpChannel.cd(SSH_DIR_NAME); - sftpChannel.put(pubKey.getAbsolutePath(), "key.tmp"); - sftpChannel.chmod(0600, "key.tmp"); - } catch (SftpException ex) { - throw new IOException("Unable to copy the public key", ex); - } - - // append the public key file contents to authorized_keys file on remote host - String mergeCommand = "cd .ssh; cat key.tmp >> " + AUTH_KEY_FILE; - if (logger.isLoggable(Level.FINER)) { - logger.finer("mergeCommand = " + mergeCommand); - } - if (exec(mergeCommand, new ByteArrayOutputStream()) != 0) { - throw new IOException("Failed to propogate the public key " + pubKeyFile + " to " + host); - } - logger.info("Copied keyfile " + pubKeyFile + " to " + userName + "@" + host); - - // remove the public key file on remote host - if (exec("rm .ssh/key.tmp", new ByteArrayOutputStream()) != 0) { - logger.warning("WARNING: Failed to remove the public key file key.tmp on remote host " + host); - } - if (logger.isLoggable(Level.FINER)) { - logger.finer("Removed the temporary key file on remote host"); - } - - // Lets fix all the permissions - // On MKS, chmod doesn't work as expected. StrictMode needs to be disabled - // for connection to go through - logger.info("Fixing file permissions for home(755), .ssh(700) and authorized_keys file(644)"); - try { - sftpChannel.cd(sftpChannel.getHome()); - sftpChannel.chmod(0755, "."); - sftpChannel.chmod(0700, SSH_DIR_NAME); - sftpChannel.chmod(0644, SSH_DIR_NAME + "/" + AUTH_KEY_FILE); - } catch (SftpException ex) { - throw new IOException("Unable to fix file permissions", ex); - } - } - } - } catch (JSchException ex) { - throw new IOException(ex); - } - } - - - public static byte[] toByteArray(InputStream input) throws IOException { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - byte[] buf = new byte[4096]; - int len; - while ((len = input.read(buf)) >= 0) { - output.write(buf, 0, len); - } - byte[] o = output.toByteArray(); - output.close(); - return o; - } /** * Check if we can authenticate using public key auth * @return true|false */ public boolean checkConnection() { - boolean status = false; + LOG.log(DEBUG, "Checking connection..."); JSch jsch = new JSch(); Session sess = null; - try { - logger.finer("Checking connection..."); - jsch.addIdentity(keyFile.getAbsolutePath(), rawKeyPassPhrase); + jsch.addIdentity(keyFile.getAbsolutePath(), keyPassPhrase); sess = jsch.getSession(userName, host, port); - sess.setConfig("StrictHostKeyChecking", "no"); + sess.setConfig("StrictHostKeyChecking", "accept-new"); sess.connect(); - status = sess.isConnected(); - if (status) { - logger.info("Successfully connected to " + userName + "@" + host + " using keyfile " + keyFile); + if (sess.isConnected()) { + LOG.log(INFO, () -> "Successfully connected to " + userName + "@" + host + " using keyfile " + keyFile); + return true; } - } - catch (JSchException ex) { + return false; + } catch (JSchException ex) { Throwable t = ex.getCause(); if (t != null) { String msg = t.getMessage(); - logger.warning("Failed to connect or authenticate: " + msg); - } - if (logger.isLoggable(Level.FINER)) { - logger.log(Level.FINER, "Failed to connect or autheticate: ", ex); + LOG.log(WARNING, "Failed to connect or authenticate: " + msg); } + LOG.log(DEBUG, "Failed to connect or autheticate: ", ex); + return false; } finally { if (sess != null) { sess.disconnect(); } } - return status; } /** @@ -727,225 +218,295 @@ public boolean checkConnection() { * @return true|false */ public boolean checkPasswordAuth() { - boolean status = false; + LOG.log(DEBUG, "Checking connection..."); JSch jsch = new JSch(); Session sess = null; - try { - if(logger.isLoggable(Level.FINER)) { - logger.finer("Checking connection..."); - } sess = jsch.getSession(userName, host, port); - sess.setConfig("StrictHostKeyChecking", "no"); + sess.setConfig("StrictHostKeyChecking", "accept-new"); sess.setPassword(password); sess.connect(); - status = sess.isConnected(); - if (status) { - if (logger.isLoggable(Level.FINER)) { - logger.finer("Successfully connected to " + userName + "@" + host + " using password authentication"); - } - } - } - catch (JSchException ex) { - Throwable t = ex.getCause(); - if (t != null) { - String msg = t.getMessage(); - logger.warning("Failed to connect or authenticate: " + msg); - } - if (logger.isLoggable(Level.FINER)) { - logger.log(Level.FINER, "Failed to connect or autheticate: ", ex); + if (sess.isConnected()) { + LOG.log(DEBUG, + () -> "Successfully connected to " + userName + "@" + host + " using password authentication"); + return true; } + return false; + } catch (JSchException ex) { + LOG.log(ERROR, "Failed to connect or autheticate: ", ex); + return false; } finally { if (sess != null) { sess.disconnect(); } } - return status; } + /** - * Invoke ssh-keygen using ProcessManager API - */ - private boolean generateKeyPair() throws IOException { - String keygenCmd = findSSHKeygen(); - if(logger.isLoggable(Level.FINER)) { - logger.finer("Using " + keygenCmd + " to generate key pair"); - } + * Open the {@link SSHSession}. + * + * @return open {@link SSHSession} + * @throws SSHException + */ + public SSHSession openSession() throws SSHException { + return openSession(getCapabilities(), host, port, userName, password, keyFile, keyPassPhrase); + } - if (!setupSSHDir()) { - throw new IOException("Failed to set proper permissions on .ssh directory"); - } - StringBuffer k = new StringBuffer(); - List cmdLine = new ArrayList<>(); - cmdLine.add(keygenCmd); - k.append(keygenCmd); - cmdLine.add("-t"); - k.append(" ").append("-t"); - cmdLine.add("rsa"); - k.append(" ").append("rsa"); - cmdLine.add("-N"); - k.append(" ").append("-N"); - - if (rawKeyPassPhrase != null && rawKeyPassPhrase.length() > 0) { - cmdLine.add(rawKeyPassPhrase); - k.append(" ").append(getPrintablePassword(rawKeyPassPhrase)); - } else { - //special handling for empty passphrase on Windows - if(OS.isWindows()) { - cmdLine.add("\"\""); - k.append(" ").append("\"\""); + /** + * Open the {@link SSHSession}. + * + * @return open {@link SSHSession} + * @throws SSHException + */ + private static SSHSession openSession( + RemoteSystemCapabilities capabilities, + String host, + int port, + String userName, + String password, + File keyFile, + String keyPassPhrase) throws SSHException { + JSch jsch = new JSch(); + String message = ""; + boolean triedAuthentication = false; + // Private key file is provided - Public Key Authentication + if (keyFile != null) { + LOG.log(DEBUG, () -> "Specified key file is " + keyFile); + if (keyFile.exists()) { + triedAuthentication = true; + LOG.log(DEBUG, () -> "Adding identity for private key at " + keyFile); + addIdentity(jsch, keyFile, keyPassPhrase); } else { - cmdLine.add(""); - k.append(" ").append(""); + message = "Specified key file does not exist \n"; + } + } else if (password == null || password.isEmpty()) { + message += "No key or password specified - trying default keys \n"; + LOG.log(DEBUG, "keyfile and password are null. Will try to authenticate with default key file if available"); + // check the default key locations if no authentication + // method is explicitly configured. + Path home = FileUtils.USER_HOME.toPath(); + for (String keyName : SSHUtil.SSH_KEY_FILE_NAMES) { + message += "Tried to authenticate using " + keyName + "\n"; + File key = home.resolve(Path.of(SSH_DIR_NAME, keyName)).toFile(); + if (key.exists()) { + triedAuthentication = true; + addIdentity(jsch, key, keyPassPhrase); + } } } - cmdLine.add("-f"); - k.append(" ").append("-f"); - cmdLine.add(keyFile.getAbsolutePath()); - k.append(" ").append(keyFile); - //cmdLine.add("-vvv"); - - ProcessManager pm = new ProcessManager(cmdLine); - if(logger.isLoggable(Level.FINER)) { - logger.finer("Command = " + k); + final Session session = openSession(jsch, host, port, userName); + try { + // TODO: Insecure, maybe we could create an input field and allow user to check the host key? + session.setConfig("StrictHostKeyChecking", "accept-new"); + session.setUserInfo(new GlassFishSshUserInfo()); + if (password != null && !password.isEmpty()) { + LOG.log(DEBUG, () -> "Authenticating with password " + maskForLogging(password)); + triedAuthentication = true; + session.setPassword(password); + } + if (!triedAuthentication) { + throw new SSHException("Could not authenticate: " + message + '.'); + } + session.connect(); + return new SSHSession(session, capabilities); + } catch (SSHException e) { + session.disconnect(); + throw e; + } catch (JSchException e) { + session.disconnect(); + throw new SSHException("Could not authenticate: " + message + '.', e); } - pm.setTimeoutMsec(DEFAULT_TIMEOUT_MSEC); + } - if (logger.isLoggable(Level.FINER)) { - pm.setEcho(true); - } else { - pm.setEcho(false); - } - int exit; + /** + * Opens the SSH session using user password. + * The resulting {@link SSHSession} is very limited, doesn't distinguish between operating + * system capabilities, etc. + * + * @param passwordParam + * @return {@link SSHSession} + * @throws SSHException if the connection attempt failed. + */ + public SSHSession openSession(String passwordParam) throws SSHException { + JSch jsch = new JSch(); + Session session = openSession(jsch, host, port, userName); try { - exit = pm.execute(); + session.setConfig("StrictHostKeyChecking", "accept-new"); + session.setPassword(passwordParam); + session.connect(); + return new SSHSession(session, getCapabilities()); + } catch (JSchException e) { + throw new SSHException("Failed to connect.", e); } - catch (ProcessManagerException ex) { - if (logger.isLoggable(Level.FINE)) { - logger.fine("Error while executing ssh-keygen: " + ex.getMessage()); - } - exit = 1; + } + + + /** + * Open and close the session. + * + * @throws SSHException + */ + public void pingConnection() throws SSHException { + LOG.log(DEBUG, () -> "Trying to establish connection to host: " + this.host); + try (SSHSession session = openSession()) { + LOG.log(INFO, () -> "Establishing SSH connection to host " + this.host + " succeeded!"); } - if (exit == 0){ - logger.info(keygenCmd + " successfully generated the identification " + keyFile); - } else { - if(logger.isLoggable(Level.FINER)) { - logger.finer(pm.getStderr()); - } - logger.info(keygenCmd + " failed"); + } + + + /** + * Check if the remote path exists. + * This method is a shortcut to simplify your code as it uses {@link SSHSession} + * and {@link SFTPClient}. + * + * @param path absolute path + * @return true if the path exists in the SFTP server. + * @throws SSHException + */ + public boolean exists(Path path) throws SSHException { + try (SSHSession session = openSession(); SFTPClient sftpClient = session.createSFTPClient()) { + return sftpClient.exists(path); } + } + - return (exit == 0) ? true : false; + @Override + public String toString() { + String displayPassword = maskForLogging(passwordParameter); + String displayKeyPassPhrase = maskForLogging(keyPassPhraseParameter); + return String.format("host=%s port=%d user=%s password=%s keyFile=%s keyPassPhrase=%s, capabilities=%s", host, port, userName, + displayPassword, keyFile, displayKeyPassPhrase, capabilities); } + /** - * Method to locate ssh-keygen. If found in path, return the same or else look - * for it in a pre defined list of search paths. - * @return ssh-keygen command + * Connects to the remote SSH server and does some simple analysis to be able to work both + * with Linux or Windows based operating systems. + * + * @return {@link RemoteSystemCapabilities} + * @throws SSHException */ - private String findSSHKeygen() { - List paths = new ArrayList<>(Arrays.asList( - "/usr/bin/", - "/usr/local/bin/")); - - if (OS.isWindows()) { - paths.add("C:/cygwin/bin/"); - //Windows MKS Toolkit install path - String mks = System.getenv("ROOTDIR"); - if (mks != null) { - paths.add(mks + "/bin/"); + private static RemoteSystemCapabilities analyzeRemote(String host, int port, String userName, String password, + File keyFile, String keyPassPhrase) { + final String[] sysPropOutputLines; + final RemoteSystemCapabilities capabilities = new RemoteSystemCapabilities(null, null, GENERIC, UTF_8); + try (SSHSession session = openSession(capabilities, host, port, userName, password, keyFile, keyPassPhrase)) { + if (LOG.isLoggable(DEBUG)) { + Map env = session.detectShellEnv(); + LOG.log(DEBUG, "Environment of the operating system obtained for the SSH client: {0}", env); } - } + sysPropOutputLines = loadRemoteJavaSystemProperties(session); + } catch (SSHException e) { + String msg = "Failed to analyze the remote system. Some commands probably are not supported."; + LOG.log(WARNING, msg, e); + return new RemoteSystemCapabilities(null, null, GENERIC, UTF_8); + } + + String javaHome = getValue("java.home", sysPropOutputLines); + Version javaVersion = getProperty("java.version", sysPropOutputLines, Version::parse); + OperatingSystem os = getProperty("os.name", sysPropOutputLines, OperatingSystem::parse); + Charset charset = getProperty("file.encoding", sysPropOutputLines, Charset::forName); + return new RemoteSystemCapabilities(javaHome, javaVersion, os, charset); + } - if (logger.isLoggable(Level.FINER)) { - logger.finer("Paths = " + paths); - } - File exe = ProcessUtils.getExe(SSH_KEYGEN); - if( exe != null){ - return exe.getPath(); + private static String[] loadRemoteJavaSystemProperties(SSHSession session) throws SSHException { + StringBuilder outputBuilder = new StringBuilder(); + // java must be available in the environment. + // If you use docker java images, check UsePam=yes and /etc/environment + // By default images configure ENV properties just for the container app, not for ssh clients. + final int code = session.exec(Arrays.asList("java", "-XshowSettings:properties", "-version"), null, + outputBuilder); + if (code != 0) { + throw new SSHException("Java command on the remote host failed. Output: " + outputBuilder + '.'); } - for (String s :paths) { - File f = new File(s + SSH_KEYGEN); - if (f.canExecute()) { - return f.getAbsolutePath(); - } - } - return SSH_KEYGEN; + return outputBuilder.toString().split("\\R"); } + /** - * Create .ssh directory and set the permissions correctly - */ - private boolean setupSSHDir() throws IOException { - boolean ret = true; - File f = new File(FileUtils.USER_HOME, SSH_DIR_NAME); - if (!FileUtils.safeIsDirectory(f)) { - if (!f.mkdirs()) { - throw new IOException("Failed to create " + f.getPath()); - } - logger.log(Level.INFO, "Created directory {0}", f); + * @param alias The alias in the format of ${ALIAS=aliasname} + * @return expanded password + */ + public static String expandPasswordAlias(String alias) { + String expandedPassword = null; + if (alias == null) { + return null; } - - if (!f.setReadable(false, false) || !f.setReadable(true)) { - ret = false; + try { + expandedPassword = RelativePathResolver.getRealPasswordFromAlias(alias); + } catch (Exception e) { + LOG.log(WARNING, "Expansion failed for {0}: {1}", new Object[] {alias, e.getMessage()}); + return null; } + return expandedPassword; + } - if (!f.setWritable(false,false) || !f.setWritable(true)) { - ret = false; - } - if (!f.setExecutable(false, false) || !f.setExecutable(true)) { - ret = false; + private static void addIdentity(JSch jsch, File identityFile, String keyPassPhrase) throws SSHException { + try { + jsch.addIdentity(identityFile.getAbsolutePath(), keyPassPhrase); + } catch (JSchException e) { + throw new SSHException("Invalid key passphrase for key: " + identityFile.getAbsolutePath() + ".", e); } + } - logger.finer("Fixed the .ssh directory permissions to 0700"); - return ret; + private static String getHost(Node node) { + SshConnector sshConnector = node.getSshConnector(); + String sshHost = sshConnector.getSshHost(); + return sshHost == null || sshHost.isEmpty() ? node.getNodeHost() : sshConnector.getSshHost(); } - @Override - public String toString() { - String displayPassword = getPrintablePassword(rawPassword); - String displayKeyPassPhrase = getPrintablePassword(rawKeyPassPhrase); + private static int getPort(final SshConnector connector) { + try { + int sshPort = Integer.parseInt(connector.getSshPort()); + return sshPort > 0 ? sshPort : SSH_PORT_DEFAULT; + } catch(NumberFormatException nfe) { + return SSH_PORT_DEFAULT; + } + } + - return String.format("host=%s port=%d user=%s password=%s keyFile=%s keyPassPhrase=%s authType=%s knownHostFile=%s", - host, port, userName, displayPassword, keyFile, - displayKeyPassPhrase, authType, knownHostsLocation); + private static String getUserName(final String userName) { + return userName == null || userName.isEmpty() ? System.getProperty("user.name") : userName; } - /** - * Take a command in the form of a list and convert it to a command string. - * If any string in the list has spaces then the string is quoted before - * being added to the final command string. - * - * @param command - * @return - */ - private static String commandListToQuotedString(List command) { - if(command.size()==1) { - return command.get(0); + + private static T getProperty(String key, String[] lines, Function converter) { + String value = getValue(key, lines); + if (value == null) { + return null; } - StringBuilder commandBuilder = new StringBuilder(); - boolean first = true; + return converter.apply(value); + } - for (String s : command) { - if (!first) { - commandBuilder.append(" "); - } else { - first = false; + + private static String getValue(String keyName, String[] propertiesOutputLines) { + for (String line : propertiesOutputLines) { + int equalSignPosition = line.indexOf('='); + if (equalSignPosition <= 0) { + continue; } - if (s.contains(" ")) { - // Quote parts of the command that contain a space - commandBuilder.append(FileUtils.quoteString(s)); - } else { - commandBuilder.append(s); + String key = line.substring(0, equalSignPosition).strip(); + if (keyName.equals(key)) { + return equalSignPosition == line.length() - 1 ? "" : line.substring(equalSignPosition + 1).strip(); } } - return commandBuilder.toString(); + return null; + } + + + private static Session openSession(JSch jsch, String host, int port, String userName) throws SSHException { + try { + return jsch.getSession(userName, host, port); + } catch (JSchException e) { + throw new SSHException("Could not authenticate user " + userName + " to " + host + ':' + port + '.', e); + } } } diff --git a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/SSHSession.java b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/SSHSession.java new file mode 100644 index 00000000000..625d023751f --- /dev/null +++ b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/launcher/SSHSession.java @@ -0,0 +1,412 @@ +/* + * Copyright (c) 2025 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.cluster.ssh.launcher; + +import com.jcraft.jsch.Channel; +import com.jcraft.jsch.ChannelExec; +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.ChannelShell; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import com.sun.enterprise.util.io.FileUtils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.System.Logger; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.glassfish.cluster.ssh.sftp.SFTPClient; + +import static java.lang.System.Logger.Level.DEBUG; +import static java.lang.System.Logger.Level.INFO; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Bridge for the Jsch {@link Session}. + */ +public class SSHSession implements AutoCloseable { + + private static final Logger LOG = System.getLogger(SSHSession.class.getName()); + + /** The connection object that represents the connection to the host via ssh */ + private final Session session; + private final RemoteSystemCapabilities capabilities; + + /** + * This constructor uses GENERIC operating system. Suitable just for operations where + * you don't care about commands available on the operating system and you are happy + * with the default UTF-8 charset in outputs (might be corrupted).. + * + * @param session + */ + SSHSession(Session session) { + this.session = session; + this.capabilities = new RemoteSystemCapabilities(null, null, OperatingSystem.GENERIC, UTF_8); + } + + + /** + * This constructor should be preferred to provide the full service - respects the operating + * system capabilities. + * + * @param session + * @param capabilities + */ + SSHSession(Session session, RemoteSystemCapabilities capabilities) { + this.session = session; + this.capabilities = capabilities; + } + + + /** + * @return true if connected + */ + public boolean isOpen() { + return session.isConnected(); + } + + + /** + * Detects environment variables configured in the remote shell. + * + * @return map of environment variables + * @throws SSHException + */ + public Map detectShellEnv() throws SSHException { + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(8192); + final ChannelShell shell = openChannel(session, "shell"); + try { + // Command is executable both in Linux and Windows os + shell.setInputStream(listInputStream(List.of("env || set"), UTF_8)); + shell.setPty(false); + InputStream in = shell.getInputStream(); + PumpThread t1 = new PumpThread(in, outputStream); + t1.start(); + shell.connect(); + t1.join(); + // Don't check the exit code, returns -1 on windows. + // Expect UTF-8 for now. It will be probably different on Windows, + // but we will parse it from the output. + String output = outputStream.toString(UTF_8); + LOG.log(DEBUG, () -> "Environment options - command output: \n" + output); + return parseProperties(output); + } catch (Exception e) { + throw new SSHException("Could not detect shell environment options. Output: " + + outputStream.toString(UTF_8), e); + } finally { + shell.disconnect(); + } + } + + + /** + * Unpacks the zip file to the target directory. + *

+ * On Linux it uses cd and jar commands.
+ * On Windows it uses PowerShell commands. + * + * @param remoteZipFile + * @param remoteDir + * @throws SSHException + */ + public void unzip(Path remoteZipFile, Path remoteDir) throws SSHException { + final String unzipCommand; + if (capabilities.getOperatingSystem() == OperatingSystem.WINDOWS) { + unzipCommand = "PowerShell.exe -Command \"Expand-Archive -LiteralPath '" + remoteZipFile + + "' -DestinationPath '" + remoteDir + "'\""; + } else { + unzipCommand = "cd \"" + remoteDir + "\"; jar -xvf \"" + remoteZipFile + "\""; + } + + final StringBuilder output = new StringBuilder(); + final int status = exec(unzipCommand, null, output); + if (status != 0) { + throw new SSHException("Failed unpacking glassfish zip file. Output: " + output + "."); + } + LOG.log(DEBUG, () -> "Unpacked " + remoteZipFile + " to directory " + remoteDir); + } + + + /** + * SFTP exec command. + * Executes a command on the remote system via ssh, optionally sending + * lines of data to the remote process's System.in. + * + * @param command - command line parts + * @param stdinLines - lines used to fake standard input in STDIN stream. Can be null. + * @param output - empty collector of the output. Can be null. + * @return exit code + * @throws SSHException + */ + public int exec(List command, List stdinLines, StringBuilder output) throws SSHException { + return exec(commandListToQuotedString(command), listInputStream(stdinLines, capabilities.getCharset()), output); + } + + + /** + * SFTP exec command. + * Executes a command on the remote system via ssh, optionally sending + * lines of data to the remote process's System.in. + * + * @param command - command line parts + * @param stdinLines - lines used to fake standard input in STDIN stream. Can be null. + * @return exit code + * @throws SSHException + */ + public int exec(List command, List stdinLines) throws SSHException { + return exec(commandListToQuotedString(command), stdinLines); + } + + + /** + * SFTP exec command. + * Executes a command on the remote system via ssh, optionally sending + * lines of data to the remote process's System.in. + * + * @param command - command to execute. If it has arguments, better use {@link #exec(List, List)}. + * @param stdinLines - lines used to fake standard input in STDIN stream. Can be null. + * @return exit code + * @throws SSHException + */ + public int exec(String command, List stdinLines) throws SSHException { + return exec(command, listInputStream(stdinLines, capabilities.getCharset())); + } + + + /** + * SFTP exec command without STDIN and without reading the output. + * Executes a command on the remote system via ssh. + * + * @param command - command to execute. If it has arguments, better use {@link #exec(List, List)}. + * @return exit code + * @throws SSHException + */ + public int exec(final String command) throws SSHException { + return exec(command, (InputStream) null); + } + + + /** + * SFTP exec command. + * Executes a command on the remote system via ssh, optionally sending + * lines of data to the remote process's System.in. + * + * @param command - command to execute. If it has arguments, better use {@link #exec(List, List)}. + * @param stdin - stream used to fake standard input in STDIN stream. Can be null. + * @return exit code + * @throws SSHException + */ + public int exec(final String command, final InputStream stdin) throws SSHException { + return exec(command, stdin, null); + } + + + /** + * SFTP exec command. + * Executes a command on the remote system via ssh, optionally sending + * lines of data to the remote process's System.in. + * + * @param command - command to execute. If it has arguments, better use {@link #exec(List, List, StringBuilder)}. + * @param stdin - stream used to fake standard input in STDIN stream. Can be null. + * @param output + * @return exit code + * @throws SSHException + */ + public int exec(final String command, final InputStream stdin, final StringBuilder output) throws SSHException { + return exec(command, stdin, output, capabilities.getCharset()); + } + + + /** + * SFTP exec command. + * Executes a command on the remote system via ssh, optionally sending + * lines of data to the remote process's System.in. + * + * @param command - command to execute. If it has arguments, better use {@link #exec(List, List, StringBuilder)}. + * @param stdin - stream used to fake standard input in STDIN stream. Can be null. + * @param output + * @return exit code + * @throws SSHException + */ + int exec(final String command, final InputStream stdin, final StringBuilder output, final Charset charset) + throws SSHException { + LOG.log(INFO, () -> "Executing command " + command + " on host: " + session.getHost()); + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(8192); + final ChannelExec execChannel = openChannel(session, "exec"); + try { + execChannel.setInputStream(stdin); + execChannel.setCommand(command); + InputStream in = execChannel.getInputStream(); + PumpThread t1 = new PumpThread(in, outputStream); + t1.start(); + PumpThread t2 = new PumpThread(execChannel.getErrStream(), outputStream); + t2.start(); + execChannel.connect(); + + t1.join(); + t2.join(); + if (output != null || LOG.isLoggable(DEBUG)) { + String commandOutput = outputStream.toString(charset); + LOG.log(DEBUG, () -> "Command output: \n" + commandOutput); + if (output != null) { + output.append(commandOutput); + } + } + if (execChannel.isClosed()) { + return execChannel.getExitStatus(); + } + return -1; + } catch (Exception e) { + throw new SSHException("Command " + command + " failed. Output: " + outputStream.toString(charset), e); + } finally { + execChannel.disconnect(); + } + } + + + /** + * @return new {@link SFTPClient} + * @throws SSHException if the connection failed, ie because the server does not support SFTP. + */ + public SFTPClient createSFTPClient() throws SSHException { + return new SFTPClient((ChannelSftp) openChannel(session, "sftp")); + } + + + @Override + public void close() { + if (session.isConnected()) { + session.disconnect(); + } + } + + + /** + * Take a command in the form of a list and convert it to a command string. + * If any string in the list has spaces then the string is quoted before + * being added to the final command string. + * + * @param command + * @return + */ + private static String commandListToQuotedString(List command) { + if (command.size() == 1) { + return command.get(0); + } + StringBuilder commandBuilder = new StringBuilder(); + boolean first = true; + + for (String s : command) { + if (!first) { + commandBuilder.append(" "); + } else { + first = false; + } + if (s.contains(" ")) { + // Quote parts of the command that contain a space + commandBuilder.append(FileUtils.quoteString(s)); + } else { + commandBuilder.append(s); + } + } + return commandBuilder.toString(); + } + + + private static InputStream listInputStream(final List stdinLines, Charset charset) { + if (stdinLines == null) { + return null; + } + try { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + for (String line : stdinLines) { + baos.write(line.getBytes(charset)); + baos.write('\n'); + } + return new ByteArrayInputStream(baos.toByteArray()); + } catch (IOException e) { + throw new IllegalStateException("Cannot copy the input to UTF-8 byte array input stream.", e); + } + } + + + @SuppressWarnings("unchecked") + private static T openChannel(Session session, String type) throws SSHException { + try { + return (T) session.openChannel(type); + } catch (JSchException e) { + throw new SSHException("Could not open the session of type=" + type, e); + } + } + + + private static Map parseProperties(String output) { + String[] lines = output.split("\\R"); + Map properties = new TreeMap<>(); + for (String line : lines) { + int equalSignPosition = line.indexOf('='); + if (equalSignPosition <= 0) { + continue; + } + String key = line.substring(0, equalSignPosition); + String value = equalSignPosition == line.length() - 1 ? "" : line.substring(equalSignPosition + 1); + properties.put(key.strip(), value.strip()); + } + return properties; + } + + + /** + * Pumps {@link InputStream} to {@link OutputStream}. + * + * @author Kohsuke Kawaguchi + */ + private static final class PumpThread extends Thread { + private final InputStream in; + private final OutputStream out; + + public PumpThread(InputStream in, OutputStream out) { + super("pump thread"); + this.in = in; + this.out = out; + } + + @Override + public void run() { + byte[] buf = new byte[8192]; + try { + while(true) { + int len = in.read(buf); + if(len<0) { + in.close(); + return; + } + out.write(buf,0,len); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/sftp/SFTPClient.java b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/sftp/SFTPClient.java index 327e5a43933..d56decc950b 100644 --- a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/sftp/SFTPClient.java +++ b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/sftp/SFTPClient.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -18,28 +18,57 @@ package org.glassfish.cluster.ssh.sftp; import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.ChannelSftp.LsEntry; import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.Session; import com.jcraft.jsch.SftpATTRS; import com.jcraft.jsch.SftpException; -import org.glassfish.cluster.ssh.util.SSHUtil; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.System.Logger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; -public class SFTPClient implements AutoCloseable { +import org.glassfish.cluster.ssh.launcher.RemoteSystemCapabilities; +import org.glassfish.cluster.ssh.launcher.SSHException; +import org.glassfish.cluster.ssh.launcher.SSHSession; - private Session session = null; +import static java.lang.System.Logger.Level.TRACE; - private ChannelSftp sftpChannel = null; +/** + * SFTP client. + * + * @see SSHSession + */ +public class SFTPClient implements AutoCloseable { + private static final Logger LOG = System.getLogger(SFTPClient.class.getName()); + /** + * This is required on Linux based hosts; directories are not in the list on Windows. + */ + private static final Predicate PREDICATE_NO_DOTS = p -> !".".equals(p.getFilename()) + && !"..".equals(p.getFilename()); - public SFTPClient(Session session) throws JSchException { - this.session = session; - sftpChannel = (ChannelSftp) session.openChannel("sftp"); - sftpChannel.connect(); - SSHUtil.register(session); - } + private final ChannelSftp sftpChannel; - public ChannelSftp getSftpChannel() { - return sftpChannel; + /** + * Creates the instance which immediately tries to open the SFTP connection.. + * + * @param channel + * @throws SSHException if the connection could not be established, usually because the SSH + * server doesn't support SFTP. + */ + public SFTPClient(ChannelSftp channel) throws SSHException { + this.sftpChannel = channel; + try { + this.sftpChannel.connect(); + } catch (JSchException e) { + throw new SSHException("Failed to connect to the SFTP server. Is it correctly configured on the server?", e); + } } /** @@ -48,69 +77,312 @@ public ChannelSftp getSftpChannel() { */ @Override public void close() { - if (session != null) { - SSHUtil.unregister(session); - session = null; + if (sftpChannel != null) { + sftpChannel.disconnect(); } } + /** - * Checks if the given path exists. + * @return Configured SSH server home directory. Usually user's home directory. + * @throws SSHException Command failed. */ - public boolean exists(String path) throws SftpException { - return _stat(normalizePath(path))!=null; + public Path getHome() throws SSHException { + try { + return Path.of(sftpChannel.getHome()); + } catch (SftpException e) { + throw new SSHException("Could not resolve SFTP Home path.", e); + } } + /** - * Graceful stat that returns null if the path doesn't exist. + * Makes sure that the directory exists, by creating it if necessary. + * @param path the remote path + * @throws SSHException Command failed. */ - public SftpATTRS _stat(String path) throws SftpException { + public void mkdirs(Path path) throws SSHException { + if (existsDirectory(path)) { + return; + } + Path current = Path.of("/"); + for (Path part : path.normalize()) { + current = current.resolve(part); + if (existsDirectory(current)) { + continue; + } + try { + sftpChannel.mkdir(current.toString()); + } catch (SftpException e) { + throw new SSHException("Failed to create the directory " + path + '.', e); + } + } + } + + + /** + * @param path + * @return true if the path exists and is a directory + * @throws SSHException Command failed. + */ + public boolean existsDirectory(Path path) throws SSHException { + SftpATTRS attrs = stat(path); + return attrs != null && attrs.isDir(); + } + + + /** + * @param path + * @return true if the path exists, is a directory and is empty. + * @throws SSHException Command failed. + */ + public boolean isEmptyDirectory(Path path) throws SSHException { + SftpATTRS attrs = stat(path); + return attrs != null && attrs.isDir() && ls(path, e -> true).isEmpty(); + } + + + /** + * Recursively deletes the specified directory. + * + * @param path + * @param onlyContent + * @param exclude + * @throws SSHException Command failed. Usually some file is not removable or is open. + */ + public void rmDir(Path path, boolean onlyContent, Path... exclude) throws SSHException { + if (!exists(path)) { + return; + } + // We use recursion while the channel is stateful + cd(path.getParent()); + List content = lsDetails(path, p -> true); + for (LsEntry entry : content) { + final String filename = entry.getFilename(); + final Path entryPath = path.resolve(filename); + if (isExcludedFromDeletion(filename, exclude)) { + LOG.log(TRACE, "Skipping excluded {0}", entryPath); + continue; + } + if (entry.getAttrs().isDir()) { + rmDir(entryPath, false, getSubDirectoryExclusions(filename, exclude)); + } else { + LOG.log(TRACE, "Deleting file {0}", entryPath); + rm(entryPath); + } + } + if (!onlyContent) { + try { + sftpChannel.cd(path.getParent().toString()); + LOG.log(TRACE, "Deleting directory {0}", path); + sftpChannel.rmdir(path.toString()); + } catch (SftpException e) { + throw new SSHException("Failed to delete directory: " + path + '.', e); + } + } + } + + + private static boolean isExcludedFromDeletion(String firstName, Path... exclusions) { + if (exclusions == null) { + return false; + } + return Arrays.stream(exclusions).filter(p -> p.getNameCount() == 1) + .anyMatch(p -> p.getFileName().toString().equals(firstName)); + } + + + private static Path[] getSubDirectoryExclusions(String firstName, Path... exclusions) { + if (exclusions == null) { + return new Path[0]; + } + return Arrays.stream(exclusions).filter(p -> p.getNameCount() > 1).filter(p -> p.startsWith(firstName)) + .map(p -> p.subpath(1, p.getNameCount())).toArray(Path[]::new); + } + + + /** + * Upload local file to the remote file. + * + * @param localFile + * @param remoteFile + * @throws SSHException Command failed. + */ + public void put(File localFile, Path remoteFile) throws SSHException { try { - return sftpChannel.stat(normalizePath(path)); + sftpChannel.cd(remoteFile.getParent().toString()); + sftpChannel.put(localFile.getAbsolutePath(), remoteFile.toString()); } catch (SftpException e) { - int c = e.id; - if (c == ChannelSftp.SSH_FX_NO_SUCH_FILE) { + throw new SSHException( + "Failed to upload the local file " + localFile + " to remote file " + remoteFile + '.', e); + } + } + + + /** + * Downloads the remote file to the local file. The local file must not exist yet. + * + * @param remoteFile + * @param localFile + * @throws SSHException Command failed. + */ + public void download(Path remoteFile, Path localFile) throws SSHException { + try (InputStream inputStream = sftpChannel.get(remoteFile.toString())) { + Files.copy(inputStream, localFile); + } catch (SftpException | IOException e) { + throw new SSHException( + "Failed to download the remote file " + remoteFile + " to local file " + localFile + '.', e); + } + } + + + /** + * Deletes the specified remote file. + * + * @param path + * @throws SSHException + */ + public void rm(Path path) throws SSHException { + try { + sftpChannel.cd(path.getParent().toString()); + sftpChannel.rm(path.toString()); + } catch (SftpException e) { + throw new SSHException("Failed to remove path " + path + '.', e); + } + } + + + /** + * @param path + * @return true if the remote path exists. + * @throws SSHException Command failed. + */ + public boolean exists(Path path) throws SSHException { + return stat(path) != null; + } + + + /** + * Providing file details. This method follows symlinks. + * + * @param path + * @return {@link SftpATTRS} or null if the path doesn't exist. + * @throws SSHException Command failed. + */ + public SftpATTRS stat(Path path) throws SSHException { + try { + return sftpChannel.stat(path.toString()); + } catch (SftpException e) { + if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) { return null; } - throw e; + throw new SSHException("Failed to call SFTP stat for " + path + '.', e); } } + /** - * Makes sure that the directory exists, by creating it if necessary. + * Providing file details. This method does not follow symlinks. + * + * @param path + * @return {@link SftpATTRS} or null if the path doesn't exist. + * @throws SSHException Command failed. */ - public void mkdirs(String path, int posixPermission) throws SftpException { - // remove trailing slash if present - if (path.endsWith("/")) { - path = path.substring(0, path.length() - 1); + public SftpATTRS lstat(Path path) throws SSHException { + try { + return sftpChannel.lstat(path.toString()); + } catch (SftpException e) { + if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) { + return null; + } + throw new SSHException("Failed to call SFTP lstat for " + path + '.', e); } + } - path = normalizePath(path); - SftpATTRS attrs = _stat(path); - if (attrs != null && attrs.isDir()) { - return; + + /** + * Calls SFTP MTIME for given path and millis. + * + * @param path + * @param millisSinceUnixEpoch + * @throws SSHException Command failed. + */ + public void setTimeModified(Path path, long millisSinceUnixEpoch) throws SSHException { + try { + sftpChannel.setMtime(path.toString(), (int) (millisSinceUnixEpoch / 1000)); + } catch (SftpException e) { + throw new SSHException("Failed to set time modification for path " + path + '.', e); } + } + - int idx = path.lastIndexOf("/"); - if (idx>0) { - mkdirs(path.substring(0,idx), posixPermission); + /** + * Calls SFTP CHMOD. Note that this command is not supported on Windows. + * + * @param path + * @param permissions + * @throws SSHException Command failed. + * @see RemoteSystemCapabilities#isChmodSupported() + */ + public void chmod(Path path, int permissions) throws SSHException { + try { + sftpChannel.chmod(permissions, path.toString()); + } catch (SftpException e) { + throw new SSHException( + "Failed to call chmod for remote path " + path + " and permissions " + permissions + ".", e); } - sftpChannel.mkdir(path); - sftpChannel.chmod(posixPermission, path); } - public void chmod(String path, int permissions) throws SftpException { - path = normalizePath(path); - sftpChannel.chmod(permissions, path); + + /** + * Changes the current directory on the remote SFTP server. + * + * @param path + * @throws SSHException Command failed. + */ + public void cd(Path path) throws SSHException { + try { + sftpChannel.cd(path.toString()); + } catch (SftpException e) { + throw new SSHException("Failed to change the remote directory to " + path + '.', e); + } } - // Commands run in a shell on Windows need to have forward slashes. - public static String normalizePath(String path){ - return path.replaceAll("\\\\","/"); + + /** + * Lists file names the given remote directory. Excludes current directory and the parent + * directory links (dot, double dot) + * + * @param path + * @param filter additional filter, ie. to filter by file extension. + * @return list of file names in the given directory + * @throws SSHException Command failed. + */ + public List ls(Path path, Predicate filter) throws SSHException { + try { + return sftpChannel.ls(path.toString()).stream().filter(filter.and(PREDICATE_NO_DOTS)) + .map(LsEntry::getFilename).collect(Collectors.toList()); + } catch (SftpException e) { + throw new SSHException("Failed to list remote directory " + path + '.', e); + } } - public void cd(String path) throws SftpException { - path = normalizePath(path); - sftpChannel.cd(path); + + /** + * Lists entries in the given remote directory. Excludes current directory and the parent + * directory links (dot, double dot) + * + * @param path + * @param filter additional filter, ie. to filter by file extension. + * @return list of file names in the given directory + * @throws SSHException Command failed. + */ + public List lsDetails(Path path, Predicate filter) throws SSHException { + try { + return sftpChannel.ls(path.toString()).stream().filter(filter.and(PREDICATE_NO_DOTS)) + .collect(Collectors.toList()); + } catch (SftpException e) { + throw new SSHException("Failed to list remote directory " + path + '.', e); + } } } diff --git a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/util/SSHUtil.java b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/util/SSHUtil.java index 93c9b0eff4b..2817bd5e2b3 100644 --- a/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/util/SSHUtil.java +++ b/nucleus/cluster/ssh/src/main/java/org/glassfish/cluster/ssh/util/SSHUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -17,14 +17,11 @@ package org.glassfish.cluster.ssh.util; -import com.jcraft.jsch.Session; import com.sun.enterprise.util.io.FileUtils; import java.io.File; import java.io.IOException; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import org.glassfish.api.admin.CommandException; @@ -36,48 +33,15 @@ */ public class SSHUtil { - private static final List activeConnections = new ArrayList<>(); + /** List of supported SSH key file names */ + public static final List SSH_KEY_FILE_NAMES = List.of("id_rsa", "id_dsa", "id_ecdsa", "identity"); /** - * Registers a connection for cleanup when the plugin is stopped. - * - * @param session The connection. - */ - public static synchronized void register(Session session) { - if (!activeConnections.contains(session)) { - activeConnections.add(session); - } - } - - - /** - * Unregisters a connection for cleanup when the plugin is stopped. - * - * @param session The connection. - */ - public static synchronized void unregister(Session session) { - session.disconnect(); - activeConnections.remove(session); - } - - - /** - * Convert empty string to null. - */ - public static String checkString(String s) { - if (s == null || s.isEmpty()) { - return null; - } - return s; - } - - - /** - * @return null or id_rsa/id_dsa/identity at user's home directory + * @return null or one of {@link #SSH_KEY_FILE_NAMES} at user's home directory */ public static File getExistingKeyFile() { Path h = FileUtils.USER_HOME.toPath(); - for (String keyName : Arrays.asList("id_rsa", "id_dsa", "identity")) { + for (String keyName : SSH_KEY_FILE_NAMES) { File f = h.resolve(Path.of(".ssh", keyName)).toFile(); if (f.exists()) { return f; @@ -87,43 +51,46 @@ public static File getExistingKeyFile() { } + /** + * @return .ssh/id_rsa in the current user's home directory. + */ public static File getDefaultKeyFile() { return FileUtils.USER_HOME.toPath().resolve(Path.of(".ssh", "id_rsa")).toFile(); } /** * Simple method to validate an encrypted key file - * @return true|false + * + * @param keyFile + * @return true if the key file is encrypted using standard format * @throws CommandException */ public static boolean isEncryptedKey(File keyFile) throws CommandException { - boolean res = false; try { String f = FileUtils.readSmallFile(keyFile, ISO_8859_1).trim(); - if (f.startsWith("-----BEGIN ") && f.contains("ENCRYPTED") - && f.endsWith(" PRIVATE KEY-----")) { - res=true; + if (f.startsWith("-----BEGIN ") && f.contains("ENCRYPTED") && f.endsWith(" PRIVATE KEY-----")) { + return true; } - } - catch (IOException ioe) { + return false; + } catch (IOException ioe) { throw new CommandException(Strings.get("error.parsing.key", keyFile, ioe.getMessage()), ioe); } - return res; } /** - * This method validates either private or public key file. In case of private - * key, it parses the key file contents to verify if it indeed contains a key - * @param file the key file - * @return success if file exists, false otherwise + * This method validates either private or public key file. + * In case of private key, it parses the key file contents to verify if it indeed contains a key + * + * @param file the key file + * @throws CommandException */ - public static boolean validateKeyFile(File file) throws CommandException { + public static void validateKeyFile(File file) throws CommandException { if (!file.exists()) { throw new CommandException(Strings.get("key.does.not.exist", file)); } if (!file.getName().endsWith(".pub")) { - String key = null; + final String key; try { key = FileUtils.readSmallFile(file, ISO_8859_1).trim(); } catch (IOException ioe) { @@ -133,6 +100,5 @@ public static boolean validateKeyFile(File file) throws CommandException { throw new CommandException(Strings.get("invalid.key.file", file)); } } - return true; } } diff --git a/nucleus/common/common-util/src/main/java/com/sun/common/util/logging/LoggingConfigImpl.java b/nucleus/common/common-util/src/main/java/com/sun/common/util/logging/LoggingConfigImpl.java index e95e918aa29..c89bb782bb7 100644 --- a/nucleus/common/common-util/src/main/java/com/sun/common/util/logging/LoggingConfigImpl.java +++ b/nucleus/common/common-util/src/main/java/com/sun/common/util/logging/LoggingConfigImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -606,8 +606,7 @@ private void addDirectory(ZipOutputStream zout, File fileSource, int ignoreLengt } /** - * Return a logging file details in the logging.properties file. - * + * @return a logging file path from the logging.properties file. * @throws IOException If an I/O error occurs */ public synchronized String getLoggingFileDetails() throws IOException { @@ -631,7 +630,7 @@ public synchronized String getLoggingFileDetails() throws IOException { /** - * @return a logging file details in the logging.properties file for given target. + * @return a logging file path in the logging.properties file for given target. * @throws IOException If an I/O error occurs */ public synchronized String getLoggingFileDetails(String targetConfigName) throws IOException { diff --git a/nucleus/common/common-util/src/main/java/com/sun/enterprise/util/io/InstanceDirs.java b/nucleus/common/common-util/src/main/java/com/sun/enterprise/util/io/InstanceDirs.java index 5dfa7aa6452..97373a306fd 100644 --- a/nucleus/common/common-util/src/main/java/com/sun/enterprise/util/io/InstanceDirs.java +++ b/nucleus/common/common-util/src/main/java/com/sun/enterprise/util/io/InstanceDirs.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * Copyright (c) 2024, 2025 Contributors to the Eclipse Foundation. * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -65,6 +65,7 @@ public InstanceDirs(File instanceDir) throws IOException { /** * This constructor handles 0, 1, 2 or 3 null args. * It is smart enough to figure out many defaults. + * * @param nodeDirParentPath E.g. install-dir/nodes * @param nodeDirName E.g. install-dir/nodes/localhost * @param instanceName E.g. i1 @@ -74,67 +75,46 @@ public InstanceDirs(String nodeDirParentPath, String nodeDirName, String instanc nodeDirParentPath = getNodeDirRootDefault(); } - File nodeDirParent = new File(nodeDirParentPath); - + final File nodeDirParent = new File(nodeDirParentPath); if (!nodeDirParent.isDirectory()) { dirs = null; - throw new IOException(Strings.get("InstanceDirs.noNodeParent")); + throw new IOException(Strings.get("InstanceDirs.noNodeParent", nodeDirParent)); } - File nodeDir; - + final File nodeDir; if (StringUtils.ok(nodeDirName)) { nodeDir = new File(nodeDirParent, nodeDirName); } else { nodeDir = getTheOneAndOnlyNode(nodeDirParent); } - if (!nodeDir.isDirectory()) { dirs = null; throw new IOException(Strings.get("InstanceDirs.badNodeDir", nodeDir)); } - File instanceDir; - + final File instanceDir; if (StringUtils.ok(instanceName)) { instanceDir = new File(nodeDir, instanceName); } else { instanceDir = getTheOneAndOnlyInstance(nodeDir); } - if (!instanceDir.isDirectory()) { dirs = null; throw new IOException(Strings.get("InstanceDirs.badInstanceDir", instanceDir)); } - - // whew!!! - dirs = new ServerDirs(instanceDir); } - private File getTheOneAndOnlyNode(File parent) throws IOException { - // look for subdirs in the parent dir -- there must be one and only one - - File[] files = parent.listFiles(new FileFilter() { - - @Override - public boolean accept(File f) { - return f != null && f.isDirectory(); - } - }); - // ERROR: No node dirs + /** Look for subdirs in the parent dir -- there must be one and only one */ + private File getTheOneAndOnlyNode(File parent) throws IOException { + File[] files = parent.listFiles(f -> f != null && f.isDirectory()); if (files == null || files.length < 1) { - throw new IOException( - Strings.get("InstanceDirs.noNodes", parent)); + throw new IOException(Strings.get("InstanceDirs.noNodes", parent)); } - // ERROR: more than one node dir child if (files.length > 1) { - throw new IOException( - Strings.get("InstanceDirs.tooManyNodes", parent, files.length)); + throw new IOException(Strings.get("InstanceDirs.tooManyNodes", parent, files.length)); } - - // the usual case -- one node dir child return files[0]; } diff --git a/nucleus/common/common-util/src/main/java/com/sun/enterprise/util/io/LocalStrings.properties b/nucleus/common/common-util/src/main/java/com/sun/enterprise/util/io/LocalStrings.properties index 47de09275a6..e08e7dd54d2 100644 --- a/nucleus/common/common-util/src/main/java/com/sun/enterprise/util/io/LocalStrings.properties +++ b/nucleus/common/common-util/src/main/java/com/sun/enterprise/util/io/LocalStrings.properties @@ -22,7 +22,7 @@ ServerDirs.invalidState=This method call is illegal because the object is in an ServerDirs.nullArg=Null arguments are not allowed in {0} InstanceDirs.noGrandParent=Server instances are required to have a grandparent \ directory for backward compatability. Here is the instance's directory: {0} -InstanceDirs.noNodeParent=No node parent directory found. +InstanceDirs.noNodeParent=No node parent directory found: {0} InstanceDirs.tooManyNodes=There is more than one directory ({1}) in the node parent directory ({0}). Cannot choose a default node. InstanceDirs.noNodes=There are no nodes in {0}. InstanceDirs.badNodeDir=The specified node directory doesn''t exist: {0} diff --git a/nucleus/common/common-util/src/main/java/com/sun/enterprise/util/net/NetUtils.java b/nucleus/common/common-util/src/main/java/com/sun/enterprise/util/net/NetUtils.java index ee3730dab24..dcd124cc0db 100644 --- a/nucleus/common/common-util/src/main/java/com/sun/enterprise/util/net/NetUtils.java +++ b/nucleus/common/common-util/src/main/java/com/sun/enterprise/util/net/NetUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * Copyright (c) 2024, 2025 Contributors to the Eclipse Foundation. * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -31,6 +31,7 @@ import java.util.ArrayList; import java.util.Enumeration; import java.util.List; +import java.util.function.Predicate; public final class NetUtils { @@ -475,6 +476,24 @@ public static String getCanonicalHostName() throws UnknownHostException { return defaultHostname; } + // If the DNS is inconsistent between domain hosts, it is sometimes better to use + // any reachable public IP address. This check is not perfect - however usually if + // the host name doesn't contain any dots, it is already a bad name for wider networks. + if (!hostname.contains(".")) { + ThrowingPredicate isLoopback = NetworkInterface::isLoopback; + try { + String host = NetworkInterface.networkInterfaces().filter(Predicate.not(isLoopback)) + .flatMap(NetworkInterface::inetAddresses) + .map(InetAddress::getHostAddress) + .filter(name -> name.indexOf('.') > 0) + .findFirst().orElse(hostname); + return host; + } catch (SocketException e) { + e.printStackTrace(); + } + + } + return hostname; } @@ -495,4 +514,22 @@ private static String trimIP(String ip) { public enum PortAvailability { illegalNumber, noPermission, inUse, unknown, OK } + + + @FunctionalInterface + private static interface ThrowingPredicate extends Predicate { + + boolean throwing(T object) throws Exception; + + @Override + public default boolean test(T object) { + try { + return throwing(object); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + } } diff --git a/nucleus/common/common-util/src/test/resources/clusters1.xml b/nucleus/common/common-util/src/test/resources/clusters1.xml index 50728ddf616..d8f6838799e 100644 --- a/nucleus/common/common-util/src/test/resources/clusters1.xml +++ b/nucleus/common/common-util/src/test/resources/clusters1.xml @@ -341,7 +341,7 @@ -Dosgi.shell.telnet.port=${OSGI_SHELL_TELNET_PORT} -Dosgi.shell.telnet.maxconn=1 -Dosgi.shell.telnet.ip=127.0.0.1 - -Dgosh.args=--noshutdown -c noop=true + -Dgosh.args=--nointeractive -Dfelix.fileinstall.dir=${com.sun.aas.installRoot}/modules/autostart/ -Dfelix.fileinstall.poll=5000 -Dfelix.fileinstall.log.level=3 @@ -507,7 +507,7 @@ -Dosgi.shell.telnet.port=${OSGI_SHELL_TELNET_PORT} -Dosgi.shell.telnet.maxconn=1 -Dosgi.shell.telnet.ip=127.0.0.1 - -Dgosh.args=--noshutdown -c noop=true + -Dgosh.args=--nointeractive -Dfelix.fileinstall.dir=${com.sun.aas.installRoot}/modules/autostart/ -Dfelix.fileinstall.poll=5000 -Dfelix.fileinstall.log.level=3 @@ -673,7 +673,7 @@ -Dosgi.shell.telnet.port=${OSGI_SHELL_TELNET_PORT} -Dosgi.shell.telnet.maxconn=1 -Dosgi.shell.telnet.ip=127.0.0.1 - -Dgosh.args=--noshutdown -c noop=true + -Dgosh.args=--nointeractive -Dfelix.fileinstall.dir=${com.sun.aas.installRoot}/modules/autostart/ -Dfelix.fileinstall.poll=5000 -Dfelix.fileinstall.log.level=3 @@ -839,7 +839,7 @@ -Dosgi.shell.telnet.port=${OSGI_SHELL_TELNET_PORT} -Dosgi.shell.telnet.maxconn=1 -Dosgi.shell.telnet.ip=127.0.0.1 - -Dgosh.args=--noshutdown -c noop=true + -Dgosh.args=--nointeractive -Dfelix.fileinstall.dir=${com.sun.aas.installRoot}/modules/autostart/ -Dfelix.fileinstall.poll=5000 -Dfelix.fileinstall.log.level=3 @@ -1005,7 +1005,7 @@ -Dosgi.shell.telnet.port=${OSGI_SHELL_TELNET_PORT} -Dosgi.shell.telnet.maxconn=1 -Dosgi.shell.telnet.ip=127.0.0.1 - -Dgosh.args=--noshutdown -c noop=true + -Dgosh.args=--nointeractive -Dfelix.fileinstall.dir=${com.sun.aas.installRoot}/modules/autostart/ -Dfelix.fileinstall.poll=5000 -Dfelix.fileinstall.log.level=3 diff --git a/nucleus/common/common-util/src/test/resources/manysysprops.xml b/nucleus/common/common-util/src/test/resources/manysysprops.xml index f63d23ed193..de63f982b00 100644 --- a/nucleus/common/common-util/src/test/resources/manysysprops.xml +++ b/nucleus/common/common-util/src/test/resources/manysysprops.xml @@ -346,7 +346,7 @@ -Dosgi.shell.telnet.port=${OSGI_SHELL_TELNET_PORT} -Dosgi.shell.telnet.maxconn=1 -Dosgi.shell.telnet.ip=127.0.0.1 - -Dgosh.args=--noshutdown -c noop=true + -Dgosh.args=--nointeractive -Dfelix.fileinstall.dir=${com.sun.aas.installRoot}/modules/autostart/ -Dfelix.fileinstall.poll=5000 -Dfelix.fileinstall.log.level=3 @@ -515,7 +515,7 @@ -Dosgi.shell.telnet.port=${OSGI_SHELL_TELNET_PORT} -Dosgi.shell.telnet.maxconn=1 -Dosgi.shell.telnet.ip=127.0.0.1 - -Dgosh.args=--noshutdown -c noop=true + -Dgosh.args=--nointeractive -Dfelix.fileinstall.dir=${com.sun.aas.installRoot}/modules/autostart/ -Dfelix.fileinstall.poll=5000 -Dfelix.fileinstall.log.level=3 @@ -681,7 +681,7 @@ -Dosgi.shell.telnet.port=${OSGI_SHELL_TELNET_PORT} -Dosgi.shell.telnet.maxconn=1 -Dosgi.shell.telnet.ip=127.0.0.1 - -Dgosh.args=--noshutdown -c noop=true + -Dgosh.args=--nointeractive -Dfelix.fileinstall.dir=${com.sun.aas.installRoot}/modules/autostart/ -Dfelix.fileinstall.poll=5000 -Dfelix.fileinstall.log.level=3 @@ -847,7 +847,7 @@ -Dosgi.shell.telnet.port=${OSGI_SHELL_TELNET_PORT} -Dosgi.shell.telnet.maxconn=1 -Dosgi.shell.telnet.ip=127.0.0.1 - -Dgosh.args=--noshutdown -c noop=true + -Dgosh.args=--nointeractive -Dfelix.fileinstall.dir=${com.sun.aas.installRoot}/modules/autostart/ -Dfelix.fileinstall.poll=5000 -Dfelix.fileinstall.log.level=3 @@ -1013,7 +1013,7 @@ -Dosgi.shell.telnet.port=${OSGI_SHELL_TELNET_PORT} -Dosgi.shell.telnet.maxconn=1 -Dosgi.shell.telnet.ip=127.0.0.1 - -Dgosh.args=--noshutdown -c noop=true + -Dgosh.args=--nointeractive -Dfelix.fileinstall.dir=${com.sun.aas.installRoot}/modules/autostart/ -Dfelix.fileinstall.poll=5000 -Dfelix.fileinstall.log.level=3 diff --git a/nucleus/common/internal-api/src/main/java/org/glassfish/internal/api/RelativePathResolver.java b/nucleus/common/internal-api/src/main/java/org/glassfish/internal/api/RelativePathResolver.java index 5da7e56a354..19a4da8d7b4 100644 --- a/nucleus/common/internal-api/src/main/java/org/glassfish/internal/api/RelativePathResolver.java +++ b/nucleus/common/internal-api/src/main/java/org/glassfish/internal/api/RelativePathResolver.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2024, 2025 Contributors to the Eclipse Foundation * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -21,13 +22,8 @@ import com.sun.enterprise.util.i18n.StringManagerBase; import java.io.File; -import java.io.IOException; import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateException; import java.util.logging.Level; -import java.util.logging.Logger; import org.glassfish.api.admin.PasswordAliasStore; @@ -48,8 +44,6 @@ */ public class RelativePathResolver { - private static Logger _logger = null; - private static RelativePathResolver _instance = null; private static final String ALIAS_TOKEN = "ALIAS"; @@ -99,8 +93,8 @@ public String unresolve(String path, String[] propNames) { //assumption is that the File class can convert this to an OS //dependent path separator (e.g. \\ on windows). path = path.replace(File.separatorChar, '/'); - for (int i = 0; i < propNames.length; i++) { - propVal = getPropertyValue(propNames[i], true); + for (String propName : propNames) { + propVal = getPropertyValue(propName, true); if (propVal != null) { //All paths returned will contain / as the separator. This will allow //all comparison to be done using / as the separator @@ -108,13 +102,13 @@ public String unresolve(String path, String[] propNames) { startIdx = path.indexOf(propVal); if (startIdx >= 0) { path = path.substring(0, startIdx) + - "${" + propNames[i] + "}" + + "${" + propName + "}" + path.substring(startIdx + propVal.length()); } } else { InternalLoggerInfo.getLogger().log(Level.SEVERE, InternalLoggerInfo.unknownProperty, - new Object[] {propNames[i], path}); + new Object[] {propName, path}); } } } @@ -162,8 +156,9 @@ static public String getAlias(String propName) int lastIdx = propName.length() - 1; if (lastIdx > 1) { propName = propName.substring(0,lastIdx); - if (propName!=null) - aliasName = propName.trim(); + if (propName!=null) { + aliasName = propName.trim(); + } } } return aliasName; @@ -180,8 +175,9 @@ static public String getAlias(String propName) */ protected String getPropertyValue(String propName, boolean bIncludingEnvironmentVariables) { - if(!bIncludingEnvironmentVariables) - return null; + if(!bIncludingEnvironmentVariables) { + return null; + } // Try finding the property as a system property String result = System.getProperty(propName); @@ -308,8 +304,8 @@ public static void main(String[] args) { System.out.println(args[i] + " " + result + " " + resolvePath(result)); } } else { - for (int i = 0; i < args.length; i++) { - System.out.println(args[i] + " " + resolvePath(args[i])); + for (String arg : args) { + System.out.println(arg + " " + resolvePath(arg)); } } } @@ -333,9 +329,7 @@ public static void main(String[] args) { * UnrecoverableKeyException if there is an error is opening or * processing the password store */ - public static String getRealPasswordFromAlias(final String at) throws - KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException, - UnrecoverableKeyException { + public static String getRealPasswordFromAlias(final String at) throws KeyStoreException { try { if (at == null || RelativePathResolver.getAlias(at) == null) { return ( at ); diff --git a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/StartServerHook.java b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/StartServerHook.java index 34f9a5c73fc..7dfab0580c8 100644 --- a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/StartServerHook.java +++ b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/StartServerHook.java @@ -56,6 +56,8 @@ class StartServerShutdownHook extends Thread { private static final Logger LOG = System.getLogger(StartServerShutdownHook.class.getName()); private static final boolean LOG_RESTART = Boolean.parseBoolean(System.getenv("AS_RESTART_LOGFILES")); + private static final Path CFGDIR = new File(System.getProperty("com.sun.aas.instanceRoot"), "config").toPath() + .toAbsolutePath(); private static final Path LOGDIR = new File(System.getProperty("com.sun.aas.instanceRoot"), "logs").toPath() .toAbsolutePath(); private static final Predicate FILTER_OTHER_HOOKS = t -> t.getName().startsWith("GlassFish") diff --git a/nucleus/core/logging/src/main/java/com/sun/enterprise/server/logging/commands/CollectLogFiles.java b/nucleus/core/logging/src/main/java/com/sun/enterprise/server/logging/commands/CollectLogFiles.java index 77ea1cbf867..0b5f6c77d4c 100644 --- a/nucleus/core/logging/src/main/java/com/sun/enterprise/server/logging/commands/CollectLogFiles.java +++ b/nucleus/core/logging/src/main/java/com/sun/enterprise/server/logging/commands/CollectLogFiles.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -33,6 +33,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -111,9 +112,7 @@ public void execute(AdminCommandContext context) { if (targetServer != null && targetServer.isDas()) { // This loop if target instance is DAS - String logFileDetails = ""; - String zipFile = ""; - + final String logFileDetails; try { // getting log file values from logging.propertie file. logFileDetails = loggingConfig.getLoggingFileDetails(); @@ -129,12 +128,11 @@ public void execute(AdminCommandContext context) { File targetDir = makingDirectoryOnDas(targetServer.getName(), report); try { - - String sourceDir = ""; + final Path sourceDir; if (logFileDetails.contains("${com.sun.aas.instanceRoot}/logs")) { - sourceDir = env.getInstanceRoot() + File.separator + "logs"; + sourceDir = env.getInstanceRoot().toPath().resolve("logs"); } else { - sourceDir = logFileDetails.substring(0, logFileDetails.lastIndexOf(File.separator)); + sourceDir = new File(logFileDetails).toPath().getParent(); } copyLogFilesForLocalhost(sourceDir, targetDir.getAbsolutePath(), report, targetServer.getName()); @@ -148,6 +146,7 @@ public void execute(AdminCommandContext context) { } + String zipFile = null; try { String zipFilePath = getZipFilePath().getAbsolutePath(); zipFile = loggingConfig.createZipFile(zipFilePath); @@ -188,7 +187,7 @@ public void execute(AdminCommandContext context) { String zipFile = ""; File targetDir = null; - String logFileDetails = ""; + String logFileDetails; try { // getting log file values from logging.propertie file. logFileDetails = getInstanceLogFileDirectory(targetServer); @@ -205,16 +204,15 @@ public void execute(AdminCommandContext context) { try { if (node.isLocal()) { - String sourceDir = getLogDirForLocalNode(logFileDetails, node, serverNode, instanceName); + Path sourceDir = getLogDirForLocalNode(logFileDetails, node, serverNode, instanceName); copyLogFilesForLocalhost(sourceDir, targetDir.getAbsolutePath(), report, instanceName); } else { new LogFilterForInstance().downloadAllInstanceLogFiles(habitat, targetServer, - domain, LOGGER, instanceName, targetDir.getAbsolutePath(), logFileDetails); + domain, LOGGER, instanceName, targetDir.toPath(), logFileDetails); } - } - catch (Exception ex) { - final String errorMsg = localStrings.getLocalString( - "collectlogfiles.errInstanceDownloading", "Error while downloading log files from {0}.", instanceName); + } catch (Exception ex) { + final String errorMsg = localStrings.getLocalString("collectlogfiles.errInstanceDownloading", + "Error while downloading log files from {0}.", instanceName); report.setMessage(errorMsg); report.setFailureCause(ex); report.setActionExitCode(ActionReport.ExitCode.FAILURE); @@ -233,10 +231,9 @@ public void execute(AdminCommandContext context) { report.setActionExitCode(ActionReport.ExitCode.FAILURE); return; } - } - catch (Exception ex) { - final String errorMsg = localStrings.getLocalString( - "collectlogfiles.creatingZip", "Error while creating zip file {0}.", zipFile); + } catch (Exception ex) { + final String errorMsg = localStrings.getLocalString("collectlogfiles.creatingZip", + "Error while creating zip file {0}.", zipFile); report.setMessage(errorMsg); report.setActionExitCode(ActionReport.ExitCode.FAILURE); return; @@ -260,7 +257,7 @@ public void execute(AdminCommandContext context) { // code to download server.log file for DAS. Bug fix 16088 - String logFileDetails = ""; + final String logFileDetails; try { // getting log file values from logging.propertie file. logFileDetails = loggingConfig.getLoggingFileDetails(); @@ -276,15 +273,15 @@ public void execute(AdminCommandContext context) { targetDir = makingDirectoryOnDas(SystemPropertyConstants.DEFAULT_SERVER_INSTANCE_NAME, report); try { - String sourceDir = ""; + Path sourceDir; if (logFileDetails.contains("${com.sun.aas.instanceRoot}/logs")) { - sourceDir = env.getInstanceRoot() + File.separator + "logs"; + sourceDir = env.getInstanceRoot().toPath().resolve("logs"); } else { - sourceDir = logFileDetails.substring(0, logFileDetails.lastIndexOf(File.separator)); + sourceDir = new File(logFileDetails).toPath().getParent(); } copyLogFilesForLocalhost(sourceDir, targetDir.getAbsolutePath(), report, - SystemPropertyConstants.DEFAULT_SERVER_INSTANCE_NAME); + SystemPropertyConstants.DEFAULT_SERVER_INSTANCE_NAME); } catch (Exception ex) { final String errorMsg = localStrings.getLocalString( "collectlogfiles.errInstanceDownloading", "Error while downloading log files from {0}.", target); @@ -309,11 +306,10 @@ public void execute(AdminCommandContext context) { Node node = domain.getNodes().getNode(serverNode); boolean errorOccur = false; instanceCount++; - - logFileDetails = ""; + final String logFile; try { // getting log file values from logging.propertie file. - logFileDetails = getInstanceLogFileDirectory(domain.getServerNamed(instanceName)); + logFile = getInstanceLogFileDirectory(domain.getServerNamed(instanceName)); } catch (Exception ex) { final String errorMsg = localStrings.getLocalString( "collectlogfiles.errGettingLogFiles", "Error while getting log file attribute for {0}.", target); @@ -327,11 +323,11 @@ public void execute(AdminCommandContext context) { targetDir = makingDirectoryOnDas(instanceName, report); if (node.isLocal()) { - String sourceDir = getLogDirForLocalNode(logFileDetails, node, serverNode, instanceName); + Path sourceDir = getLogDirForLocalNode(logFile, node, serverNode, instanceName); copyLogFilesForLocalhost(sourceDir, targetDir.getAbsolutePath(), report, instanceName); } else { new LogFilterForInstance().downloadAllInstanceLogFiles(habitat, instance, - domain, LOGGER, instanceName, targetDir.getAbsolutePath(), logFileDetails); + domain, LOGGER, instanceName, targetDir.toPath(), logFile); } } catch (Exception ex) { @@ -354,7 +350,7 @@ public void execute(AdminCommandContext context) { String zipFilePath = getZipFilePath().getAbsolutePath(); // Creating zip file and returning zip file absolute path. zipFile = loggingConfig.createZipFile(zipFilePath); - if (zipFile == null || new File(zipFile) == null) { + if (zipFile == null) { // Failure during zip final String errorMsg = localStrings.getLocalString( "collectlogfiles.creatingZip", "Error while creating zip file {0}.", zipFilePath); @@ -362,8 +358,7 @@ public void execute(AdminCommandContext context) { report.setActionExitCode(ActionReport.ExitCode.FAILURE); return; } - } - catch (Exception ex) { + } catch (Exception ex) { final String errorMsg = localStrings.getLocalString( "collectlogfiles.creatingZip", "Error while creating zip file {0}.", zipFile); report.setMessage(errorMsg); @@ -395,9 +390,9 @@ public void execute(AdminCommandContext context) { deleteDir(new File(env.getInstanceRoot() + File.separator + "collected-logs" + File.separator + "logs")); } - private void copyLogFilesForLocalhost(String sourceDir, String targetDir, ActionReport report, String instanceName) throws IOException { + private void copyLogFilesForLocalhost(Path sourceDir, String targetDir, ActionReport report, String instanceName) throws IOException { // Getting all Log Files - File logsDir = new File(sourceDir); + File logsDir = sourceDir.toFile(); File allLogFileNames[] = logsDir.listFiles(); if (allLogFileNames == null) { throw new IOException(""); @@ -418,8 +413,7 @@ private void copyLogFilesForLocalhost(String sourceDir, String targetDir, Action byte[] buffer = new byte[4096]; int bytesRead; - while ((bytesRead = from.read(buffer)) != -1) - { + while ((bytesRead = from.read(buffer)) != -1) { to.write(buffer, 0, bytesRead); // write } } @@ -578,22 +572,20 @@ private File makingDirectory(File parent, String path, ActionReport report, Stri } } - private String getLogDirForLocalNode(String instanceLogFileName, Node node, String serverNode, String instanceName) { - String loggingDir; - loggingDir = new LogFilterForInstance().getLoggingDirectoryForNode(instanceLogFileName, node, serverNode, instanceName); - - File logsDir = new File(loggingDir); - File allLogFileNames[] = logsDir.listFiles(); - + private Path getLogDirForLocalNode(String instanceLogFileDirectory, Node node, String serverNode, String instanceName) { + Path loggingDir = new LogFilterForInstance().getLoggingDirectoryForNode(instanceLogFileDirectory, node, + serverNode, instanceName); + File allLogFileNames[] = loggingDir.toFile().listFiles(); boolean noFileFound = true; - if (allLogFileNames != null) { // This check for, if directory doesn't present or missing on machine. It happens due to bug 16451 + if (allLogFileNames != null) { // This check for, if directory doesn't present or missing on + // machine. It happens due to bug 16451 for (File file : allLogFileNames) { String fileName = file.getName(); // code to remove . and .. file which is return if (file.isFile() && !fileName.equals(".") && !fileName.equals("..") && fileName.contains(".log") - && !fileName.contains(".log.")) { + && !fileName.contains(".log.")) { noFileFound = false; break; } @@ -602,7 +594,8 @@ private String getLogDirForLocalNode(String instanceLogFileName, Node node, Stri if (noFileFound) { // this loop is used when user has changed value for server.log but not restarted the server. - loggingDir = new LogFilterForInstance().getLoggingDirectoryForNodeWhenNoFilesFound(instanceLogFileName, node, serverNode, instanceName); + loggingDir = new LogFilterForInstance().getLoggingDirectoryForNodeWhenNoFilesFound(instanceLogFileDirectory, + node, serverNode, instanceName); } diff --git a/nucleus/core/logging/src/main/java/com/sun/enterprise/server/logging/logviewer/backend/LogFilter.java b/nucleus/core/logging/src/main/java/com/sun/enterprise/server/logging/logviewer/backend/LogFilter.java index d42fda8c4a8..86ae70e04b9 100644 --- a/nucleus/core/logging/src/main/java/com/sun/enterprise/server/logging/logviewer/backend/LogFilter.java +++ b/nucleus/core/logging/src/main/java/com/sun/enterprise/server/logging/logviewer/backend/LogFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 2009, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -30,6 +30,7 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.time.Instant; import java.time.OffsetDateTime; import java.util.ArrayList; @@ -47,7 +48,6 @@ import org.glassfish.api.admin.CommandRunner; import org.glassfish.api.admin.ServerEnvironment; -import org.glassfish.api.logging.LogLevel; import org.glassfish.config.support.TranslatedConfigView; import org.glassfish.hk2.api.ServiceLocator; import org.jvnet.hk2.annotations.Service; @@ -68,12 +68,6 @@ public class LogFilter { // Admin front end. private static final String RESULTS_ATTRIBUTE = "Results"; - // Load the custom level class for query purpose. - private static final Level[] GF_CUSTOM_LEVELS = new Level[] { - LogLevel.ALERT, - LogLevel.EMERGENCY - }; - private static final String NV_SEPARATOR = ";"; static final String[] LOG_LEVELS = {"SEVERE", "WARNING", "INFO", "CONFIG", "FINE", "FINER", "FINEST"}; @@ -246,26 +240,20 @@ public List getInstanceLogFileNames(String instanceName) { } } - /* - This function is used to get log file details from logging.properties file for given target. - */ + /** + * This function is used to get log file details from logging.properties file for given target. + */ private String getInstanceLogFileDetails(Server targetServer) throws IOException { - - String logFileDetailsForServer = ""; - String targetConfigName = ""; - Cluster clusterForInstance = targetServer.getCluster(); - if (clusterForInstance != null) { - targetConfigName = clusterForInstance.getConfigRef(); - } else { + final String targetConfigName; + if (clusterForInstance == null) { targetConfigName = targetServer.getConfigRef(); + } else { + targetConfigName = clusterForInstance.getConfigRef(); } - logFileDetailsForServer = loggingConfig.getLoggingFileDetails(targetConfigName); - - return logFileDetailsForServer; - + return loggingConfig.getLoggingFileDetails(targetConfigName); } /* @@ -281,41 +269,38 @@ public String getLogFileForGivenTarget(String targetServerName) throws IOExcepti logFileDetailsForServer = TranslatedConfigView.getTranslatedValue(logFileDetailsForServer).toString(); logFileDetailsForServer = new File(logFileDetailsForServer).getAbsolutePath(); return logFileDetailsForServer; + } + // getting log file for instance from logging.properties + String logFileDetailsForInstance = getInstanceLogFileDetails(targetServer); + Node node = domain.getNodes().getNode(serverNode); + String loggingDir = ""; + String loggingFile = ""; + + // replacing instanceRoot value if it's there + if (logFileDetailsForInstance.contains("${com.sun.aas.instanceRoot}/logs") && node.getNodeDir() != null) { + // this code is used if no changes made in logging.properties file + loggingDir = node.getNodeDir() + File.separator + serverNode + + File.separator + targetServerName; + loggingFile = logFileDetailsForInstance.replace("${com.sun.aas.instanceRoot}", loggingDir); + } else if (logFileDetailsForInstance.contains("${com.sun.aas.instanceRoot}/logs") && node.getInstallDir() != null) { + loggingDir = node.getInstallDir() + File.separator + "glassfish" + File.separator + "nodes" + + File.separator + serverNode + File.separator + targetServerName; + loggingFile = logFileDetailsForInstance.replace("${com.sun.aas.instanceRoot}", loggingDir); } else { - // getting log file for instance from logging.properties - String logFileDetailsForInstance = getInstanceLogFileDetails(targetServer); - Node node = domain.getNodes().getNode(serverNode); - String loggingDir = ""; - String loggingFile = ""; - - // replacing instanceRoot value if it's there - if (logFileDetailsForInstance.contains("${com.sun.aas.instanceRoot}/logs") && node.getNodeDir() != null) { - // this code is used if no changes made in logging.properties file - loggingDir = node.getNodeDir() + File.separator + serverNode - + File.separator + targetServerName; - loggingFile = logFileDetailsForInstance.replace("${com.sun.aas.instanceRoot}", loggingDir); - } else if (logFileDetailsForInstance.contains("${com.sun.aas.instanceRoot}/logs") && node.getInstallDir() != null) { - loggingDir = node.getInstallDir() + File.separator + "glassfish" + File.separator + "nodes" - + File.separator + serverNode + File.separator + targetServerName; - loggingFile = logFileDetailsForInstance.replace("${com.sun.aas.instanceRoot}", loggingDir); - } else { - loggingFile = logFileDetailsForInstance; - } - - if (node.isLocal()) { - // if local just returning log file to view - return loggingFile; - } else { - // if remote then need to download log file on DAS and returning that log file for view - String logFileName = logFileDetailsForInstance.substring(logFileDetailsForInstance.lastIndexOf(File.separator) + 1, logFileDetailsForInstance.length()); - File instanceFile = null; - instanceFile = new LogFilterForInstance().downloadGivenInstanceLogFile(habitat, targetServer, domain, LOGGER, - targetServerName, env.getInstanceRoot().getAbsolutePath(), logFileName, logFileDetailsForInstance); - - return instanceFile.getAbsolutePath(); - } + loggingFile = logFileDetailsForInstance; + } + if (node.isLocal()) { + // if local just returning log file to view + return loggingFile; } + // if remote then need to download log file on DAS and returning that log file for view + String logFileName = logFileDetailsForInstance.substring(logFileDetailsForInstance.lastIndexOf(File.separator) + 1, logFileDetailsForInstance.length()); + File instanceFile = null; + instanceFile = new LogFilterForInstance().downloadGivenInstanceLogFile(habitat, targetServer, domain, + targetServerName, env.getInstanceRoot().getAbsolutePath(), logFileName, logFileDetailsForInstance); + + return instanceFile.getAbsolutePath(); } @@ -336,90 +321,84 @@ public AttributeList getLogRecordsUsingQuery( requestedCount, fromDate, toDate, logLevel, onlyLevel, listOfModules, nameValueMap, anySearch); - } else { - // for Instance it's going through this loop. This will use ssh utility to get file from instance machine(remote machine) and - // store under glassfish/domains/domain1/logs// directory which is used to get LogFile object. - // Right now user needs to go through this URL to setup and configure ssh http://wikis.sun.com/display/GlassFish/3.1SSHSetup - - String serverNode = targetServer.getNodeRef(); - Node node = domain.getNodes().getNode(serverNode); - String loggingDir = ""; - String instanceLogFileName = ""; - try { - // getting lof file details for given target. - instanceLogFileName = getInstanceLogFileDetails(targetServer); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, LogFacade.ERROR_EXECUTING_LOG_QUERY, e); - return new AttributeList(); - } + } + + String serverNode = targetServer.getNodeRef(); + Node node = domain.getNodes().getNode(serverNode); + Path loggingDir; + String instanceLogFileName; + try { + // getting lof file details for given target. + instanceLogFileName = getInstanceLogFileDetails(targetServer); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, LogFacade.ERROR_EXECUTING_LOG_QUERY, e); + return new AttributeList(); + } - if (node.isLocal()) { + if (node.isLocal()) { - loggingDir = new LogFilterForInstance().getLoggingDirectoryForNode(instanceLogFileName, node, serverNode, instanceName); + loggingDir = new LogFilterForInstance().getLoggingDirectoryForNode(instanceLogFileName, node, serverNode, instanceName); - File logsDir = new File(loggingDir); - File allLogFileNames[] = logsDir.listFiles(); + File logsDir = loggingDir.toFile(); + File allLogFileNames[] = logsDir.listFiles(); - boolean noFileFound = true; + boolean noFileFound = true; - if (allLogFileNames != null) { // This check for, if directory doesn't present or missing on machine. It happens due to bug 16451 - for (File file : allLogFileNames) { - String fileName = file.getName(); - // code to remove . and .. file which is return - if (file.isFile() && !fileName.equals(".") && !fileName.equals("..") && fileName.contains(".log") - && !fileName.contains(".log.")) { - noFileFound = false; - break; - } + if (allLogFileNames != null) { // This check for, if directory doesn't present or missing on machine. It happens due to bug 16451 + for (File file : allLogFileNames) { + String fileName = file.getName(); + // code to remove . and .. file which is return + if (file.isFile() && !fileName.equals(".") && !fileName.equals("..") && fileName.contains(".log") + && !fileName.contains(".log.")) { + noFileFound = false; + break; } } + } - if (noFileFound) { - // this loop is used when user has changed value for server.log but not restarted the server. - loggingDir = new LogFilterForInstance().getLoggingDirectoryForNodeWhenNoFilesFound(instanceLogFileName, node, serverNode, instanceName); + if (noFileFound) { + // this loop is used when user has changed value for server.log but not restarted the server. + loggingDir = new LogFilterForInstance().getLoggingDirectoryForNodeWhenNoFilesFound(instanceLogFileName, node, serverNode, instanceName); - } + } - instanceLogFile = new File(loggingDir + File.separator + logFileName); - - // verifying loggingFile presents or not if not then changing logFileName value to server.log. It means wrong name is coming - // from GUI to back end code. - if (!instanceLogFile.exists()) { - instanceLogFile = new File(loggingDir + File.separator + "server.log"); - } else if (!instanceLogFile.exists()) { - loggingDir = instanceLogFileName.substring(0, instanceLogFileName.lastIndexOf(File.separator)); - instanceLogFile = new File(loggingDir + File.separator + logFileName); - if (!instanceLogFile.exists()) { - instanceLogFile = new File(instanceLogFileName); - } - } + instanceLogFile = loggingDir.resolve(logFileName).toFile(); + // verifying loggingFile presents or not if not then changing logFileName value to server.log. It means wrong name is coming + // from GUI to back end code. + if (!instanceLogFile.exists()) { + instanceLogFile = loggingDir.resolve("server.log").toFile(); } else { - try { - // this code is used when the node is not local. - instanceLogFile = new LogFilterForInstance().downloadGivenInstanceLogFile(habitat, targetServer, - domain, LOGGER, instanceName, env.getInstanceRoot().getAbsolutePath(), logFileName, instanceLogFileName); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, LogFacade.ERROR_EXECUTING_LOG_QUERY, e); - return new AttributeList(); + loggingDir = new File(instanceLogFileName).toPath().getParent(); + instanceLogFile = new File(loggingDir + File.separator + logFileName); + if (!instanceLogFile.exists()) { + instanceLogFile = new File(instanceLogFileName); } + } + } else { + try { + // this code is used when the node is not local. + instanceLogFile = new LogFilterForInstance().downloadGivenInstanceLogFile(habitat, targetServer, + domain, instanceName, env.getInstanceRoot().getAbsolutePath(), logFileName, instanceLogFileName); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, LogFacade.ERROR_EXECUTING_LOG_QUERY, e); + return new AttributeList(); } + } - LogFile logFile = null; File loggingFileExists = new File(instanceLogFile.getAbsolutePath()); if (!loggingFileExists.exists()) { LOGGER.log(Level.WARNING, LogFacade.INSTANCE_LOG_FILE_NOT_FOUND, instanceLogFile.getAbsolutePath()); return new AttributeList(); } - logFile = getLogFile(instanceLogFile.getAbsolutePath()); + final LogFile logFile = getLogFile(instanceLogFile.getAbsolutePath()); boolean forwd = (forward == null) ? true : forward.booleanValue(); boolean nxt = (next == null) ? true : next.booleanValue(); - long reqCount = (requestedCount == null) ? - logFile.getIndexSize() : requestedCount.intValue(); + long reqCount = (requestedCount == null) ? logFile.getIndexSize() : requestedCount.intValue(); long startingRecord; if (fromRecord == -1) { // In this case next/previous (before/after) don't mean much since @@ -669,7 +648,7 @@ public LogFile getLogFile(String fileName) { protected boolean allChecks(LogFile.LogEntry entry, Instant fromDate, Instant toDate, String queryLevel, boolean onlyLevel, List listOfModules, Properties nameValueMap, String anySearch) { if (DEBUG) { - StringBuffer buf = new StringBuffer(); + StringBuilder buf = new StringBuilder(); buf.append(dateTimeCheck(entry.getLoggedDateTime(), fromDate, toDate)); buf.append(","); buf.append(levelCheck(entry.getLoggedLevel(), queryLevel, onlyLevel)); diff --git a/nucleus/core/logging/src/main/java/com/sun/enterprise/server/logging/logviewer/backend/LogFilterForInstance.java b/nucleus/core/logging/src/main/java/com/sun/enterprise/server/logging/logviewer/backend/LogFilterForInstance.java index ee464be34cf..95f91b87657 100644 --- a/nucleus/core/logging/src/main/java/com/sun/enterprise/server/logging/logviewer/backend/LogFilterForInstance.java +++ b/nucleus/core/logging/src/main/java/com/sun/enterprise/server/logging/logviewer/backend/LogFilterForInstance.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -18,28 +18,22 @@ package com.sun.enterprise.server.logging.logviewer.backend; import com.jcraft.jsch.ChannelSftp.LsEntry; -import com.jcraft.jsch.JSchException; import com.jcraft.jsch.SftpATTRS; -import com.jcraft.jsch.SftpException; import com.sun.enterprise.config.serverbeans.Domain; import com.sun.enterprise.config.serverbeans.Node; import com.sun.enterprise.config.serverbeans.Nodes; import com.sun.enterprise.config.serverbeans.Server; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Paths; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import java.util.Vector; import java.util.logging.Logger; +import org.glassfish.cluster.ssh.launcher.SSHException; import org.glassfish.cluster.ssh.launcher.SSHLauncher; +import org.glassfish.cluster.ssh.launcher.SSHSession; import org.glassfish.cluster.ssh.sftp.SFTPClient; import org.glassfish.hk2.api.ServiceLocator; @@ -50,155 +44,122 @@ */ public class LogFilterForInstance { - public File downloadGivenInstanceLogFile(ServiceLocator habitat, Server targetServer, Domain domain, Logger logger, - String instanceName, String domainRoot, String logFileName, String instanceLogFileName) - throws IOException { - - File instanceLogFile = null; + public File downloadGivenInstanceLogFile(ServiceLocator habitat, Server targetServer, Domain domain, + String instanceName, String domainRoot, String logFileName, String instanceLogFileName) throws IOException { // method is used from logviewer back end code logfilter. // for Instance it's going through this loop. This will use ssh utility to get file from instance machine(remote machine) and // store in domains/domain1/logs/ which is used to get LogFile object. // Right now user needs to go through this URL to setup and configure ssh http://wikis.sun.com/display/GlassFish/3.1SSHSetup - SSHLauncher sshL = getSSHL(habitat); String sNode = targetServer.getNodeRef(); Nodes nodes = domain.getNodes(); Node node = nodes.getNode(sNode); + if (!node.getType().equals("SSH")) { + return null; + } - if (node.getType().equals("SSH")) { - + final SSHLauncher sshL = new SSHLauncher(node); + try (SSHSession session = sshL.openSession(); SFTPClient sftpClient = session.createSFTPClient()) { + File logFileDirectoryOnServer = makingDirectory(Path.of(domainRoot, "logs", instanceName)); + boolean noFileFound = true; + Path loggingDir = getLoggingDirectoryForNode(instanceLogFileName, node, sNode, instanceName); try { - sshL.init(node, logger); + List instanceLogFileNames = sftpClient.ls(loggingDir, this::isAcceptable); + if (!instanceLogFileNames.isEmpty()) { + noFileFound = false; + } + } catch (Exception e) { + // if directory doesn't present or missing on remote machine. It happens due + // to bug 16451 + noFileFound = true; + } - try (SFTPClient sftpClient = sshL.getSFTPClient()) { - File logFileDirectoryOnServer = makingDirectory( - domainRoot + File.separator + "logs" + File.separator + instanceName); - boolean noFileFound = true; - String loggingDir = getLoggingDirectoryForNode(instanceLogFileName, node, sNode, instanceName); - try { - @SuppressWarnings("unchecked") - Vector instanceLogFileNames = sftpClient.getSftpChannel().ls(loggingDir); - for (LsEntry file : instanceLogFileNames) { - // code to remove . and .. file which is return from sftpclient ls - // method - if (isAcceptable(file)) { - noFileFound = false; - break; - } - } - } catch (Exception e) { - // if directory doesn't present or missing on remote machine. It happens due - // to bug 16451 - noFileFound = true; - } + if (noFileFound) { + // this loop is used when user has changed value for server.log but not + // restarted the server. + loggingDir = getLoggingDirectoryForNodeWhenNoFilesFound(instanceLogFileName, node, sNode, + instanceName); + } - if (noFileFound) { - // this loop is used when user has changed value for server.log but not - // restarted the server. - loggingDir = getLoggingDirectoryForNodeWhenNoFilesFound(instanceLogFileName, node, sNode, instanceName); - } + Path loggingFile = loggingDir.resolve(logFileName); + if (!sftpClient.exists(loggingFile)) { + loggingFile = loggingDir.resolve("server.log"); + } else if (!sftpClient.exists(loggingFile)) { + loggingFile = Path.of(instanceLogFileName); + } - String loggingFile = loggingDir + File.separator + logFileName; - if (!sftpClient.exists(loggingFile)) { - loggingFile = loggingDir + File.separator + "server.log"; - } else if (!sftpClient.exists(loggingFile)) { - loggingFile = instanceLogFileName; - } + // creating local file name on DAS + long instanceLogFileSize = 0; + File instanceLogFile = logFileDirectoryOnServer.toPath().resolve(loggingFile.getFileName()).toFile(); - // creating local file name on DAS - long instanceLogFileSize = 0; - instanceLogFile = new File(logFileDirectoryOnServer.getAbsolutePath() + File.separator - + loggingFile.substring(loggingFile.lastIndexOf(File.separator), loggingFile.length())); + // getting size of the file on DAS + if (instanceLogFile.exists()) { + instanceLogFileSize = instanceLogFile.length(); + } - // getting size of the file on DAS - if (instanceLogFile.exists()) { - instanceLogFileSize = instanceLogFile.length(); - } + // getting size of the file on instance machine + SftpATTRS sftpFileAttributes = sftpClient.stat(loggingFile); + long fileSizeOnNode = sftpFileAttributes.getSize(); - SftpATTRS sftpFileAttributes = sftpClient._stat(loggingFile); - - // getting size of the file on instance machine - long fileSizeOnNode = sftpFileAttributes.getSize(); - - // if differ both size then downloading - if (instanceLogFileSize != fileSizeOnNode) { - try (InputStream inputStream = sftpClient.getSftpChannel().get(loggingFile); - BufferedInputStream in = new BufferedInputStream(inputStream); - FileOutputStream file = new FileOutputStream(instanceLogFile); - BufferedOutputStream out = new BufferedOutputStream(file)) { - int i; - while ((i = in.read()) != -1) { - out.write(i); - } - out.flush(); - } - } - } - } catch (JSchException ex) { - throw new IOException("Unable to download instance log file from SSH Node", ex); - } catch (SftpException ex) { - throw new IOException("Unable to download instance log file from SSH Node", ex); + // if differ both size then downloading + if (instanceLogFileSize != fileSizeOnNode) { + sftpClient.download(loggingFile, instanceLogFile.toPath()); } + return instanceLogFile; + } catch (SSHException ex) { + throw new SSHException( + "Unable to download log file of instance " + instanceName + " from SSH Node " + node.getName() + '.', + ex); } - - return instanceLogFile; - } + + /** + * Download log files of all instances of the node. + */ public void downloadAllInstanceLogFiles(ServiceLocator habitat, Server targetServer, Domain domain, Logger logger, - String instanceName, String tempDirectoryOnServer, String instanceLogFileDirectory) + String instanceName, Path tempDirectoryOnServer, String instanceLogFileDirectory) throws IOException { - // method is used from collect-log-files command - // for Instance it's going through this loop. This will use ssh utility to get file from instance machine(remote machine) and - // store in tempDirectoryOnServer which is used to create zip file. - // Right now user needs to go through this URL to setup and configure ssh http://wikis.sun.com/display/GlassFish/3.1SSHSetup - SSHLauncher sshL = getSSHL(habitat); String sNode = targetServer.getNodeRef(); Nodes nodes = domain.getNodes(); Node node = nodes.getNode(sNode); if (node.getType().equals("SSH")) { try { - sshL.init(node, logger); - - List allInstanceLogFileName = getInstanceLogFileNames(habitat, targetServer, domain, logger, + List allInstanceLogFileNames = getInstanceLogFileNames(habitat, targetServer, domain, logger, instanceName, instanceLogFileDirectory); boolean noFileFound = true; - String sourceDir = getLoggingDirectoryForNode(instanceLogFileDirectory, node, sNode, instanceName); - SFTPClient sftpClient = sshL.getSFTPClient(); + Path sourceDir = getLoggingDirectoryForNode(instanceLogFileDirectory, node, sNode, instanceName); + final SSHLauncher sshL = new SSHLauncher(node); + try (SSHSession session = sshL.openSession(); SFTPClient sftpClient = session.createSFTPClient()) { - try { - @SuppressWarnings("unchecked") - List instanceLogFileNames = sftpClient.getSftpChannel().ls(sourceDir); - for (LsEntry file : instanceLogFileNames) { - // code to remove . and .. file which is return from sftpclient ls method - if (isAcceptable(file)) { + try { + List instanceLogFileNames = sftpClient.ls(sourceDir, this::isAcceptable); + if (!instanceLogFileNames.isEmpty()) { noFileFound = false; - break; } + } catch (Exception e) { + // if directory doesn't present or missing on remote machine. + // It happens due to bug 16451 + noFileFound = true; } - } catch (Exception e) { - // if directory doesn't present or missing on remote machine. It happens due to bug 16451 - noFileFound = true; - } - if (noFileFound) { - // this loop is used when user has changed value for server.log but not restarted the server. - sourceDir = getLoggingDirectoryForNodeWhenNoFilesFound(instanceLogFileDirectory, node, sNode, instanceName); - } + if (noFileFound) { + // this loop is used when user has changed value for server.log but not + // restarted the server. + sourceDir = getLoggingDirectoryForNodeWhenNoFilesFound(instanceLogFileDirectory, node, sNode, + instanceName); + } - for (Object element : allInstanceLogFileName) { - String remoteFileName = sourceDir + File.separator + element; - InputStream inputStream = sftpClient.getSftpChannel().get(remoteFileName); - Files.copy(inputStream, Paths.get(tempDirectoryOnServer)); + for (String fileName : allInstanceLogFileNames) { + sftpClient.download(sourceDir.resolve(fileName), tempDirectoryOnServer.resolve(fileName)); + } } - sftpClient.close(); - } catch (JSchException ex) { - throw new IOException("Unable to download instance log file from SSH Node", ex); - } catch (SftpException ex) { - throw new IOException("Unable to download instance log file from SSH Node", ex); + } catch (Exception e) { + throw new SSHException("Unable to download log file of instance " + instanceName + " from SSH Node " + + node.getName() + '.', e); } } } @@ -213,9 +174,9 @@ public List getInstanceLogFileNames(ServiceLocator habitat, Server targe // this code is used when DAS and instances are running on the same machine if (node.isLocal()) { - String loggingDir = getLoggingDirectoryForNode(instanceLogFileDetails, node, sNode, instanceName); + Path loggingDir = getLoggingDirectoryForNode(instanceLogFileDetails, node, sNode, instanceName); - File logsDir = new File(loggingDir); + File logsDir = loggingDir.toFile(); File allLogFileNames[] = logsDir.listFiles(); boolean noFileFound = true; @@ -235,7 +196,7 @@ public List getInstanceLogFileNames(ServiceLocator habitat, Server targe if (noFileFound) { // this loop is used when user has changed value for server.log but not restarted the server. loggingDir = getLoggingDirectoryForNodeWhenNoFilesFound(instanceLogFileDetails, node, sNode, instanceName); - logsDir = new File(loggingDir); + logsDir = loggingDir.toFile(); allLogFileNames = logsDir.listFiles(); for (File file : allLogFileNames) { @@ -248,111 +209,74 @@ public List getInstanceLogFileNames(ServiceLocator habitat, Server targe } } } else if (node.getType().equals("SSH")) { - try { // this code is used if DAS and instance are running on different machine - SSHLauncher sshL = getSSHL(habitat); - sshL.init(node, logger); - try (SFTPClient sftpClient = sshL.getSFTPClient()) { - boolean noFileFound = true; - String loggingDir = getLoggingDirectoryForNode(instanceLogFileDetails, node, sNode, instanceName); - try { - @SuppressWarnings("unchecked") - Vector instanceLogFileNames = sftpClient.getSftpChannel().ls(loggingDir); - for (LsEntry file : instanceLogFileNames) { - // code to remove . and .. file which is return from sftpclient ls method - if (isAcceptable(file)) { - instanceLogFileNamesAsString.add(file.getFilename()); - noFileFound = false; - } - } - } catch (Exception ex) { - // if directory doesn't present or missing on remote machine. It happens due - // to bug 16451 - noFileFound = true; - } - - if (noFileFound) { - // this loop is used when user has changed value for server.log but not - // restarted the server. - loggingDir = getLoggingDirectoryForNodeWhenNoFilesFound(instanceLogFileDetails, node, sNode, - instanceName); - @SuppressWarnings("unchecked") - Vector instanceLogFileNames = sftpClient.getSftpChannel().ls(loggingDir); - for (LsEntry file : instanceLogFileNames) { - // code to remove . and .. file which is return from sftpclient ls - // method - if (isAcceptable(file)) { - instanceLogFileNamesAsString.add(file.getFilename()); - } - } + SSHLauncher sshL = new SSHLauncher(node); + try (SSHSession session = sshL.openSession(); SFTPClient sftpClient = session.createSFTPClient()) { + boolean noFileFound = true; + Path loggingDir = getLoggingDirectoryForNode(instanceLogFileDetails, node, sNode, instanceName); + try { + List instanceLogFileNames = sftpClient.ls(loggingDir, this::isAcceptable); + for (String file : instanceLogFileNames) { + instanceLogFileNamesAsString.add(file); + noFileFound = false; } + } catch (Exception ex) { + // if directory doesn't present or missing on remote machine. It happens due + // to bug 16451 + noFileFound = true; + } + if (noFileFound) { + // this loop is used when user has changed value for server.log but not + // restarted the server. + loggingDir = getLoggingDirectoryForNodeWhenNoFilesFound(instanceLogFileDetails, node, sNode, + instanceName); + List instanceLogFileNames = sftpClient.ls(loggingDir, this::isAcceptable); + instanceLogFileNamesAsString.addAll(instanceLogFileNames); } - } catch (JSchException ex) { - throw new IOException("Unable to download instance log file from SSH Node", ex); - } catch (SftpException ex) { - throw new IOException("Unable to download instance log file from SSH Node", ex); + } catch (SSHException e) { + throw new SSHException("Unable to download log file of instance " + instanceName + " from SSH Node " + + node.getName() + '.', e); } } return instanceLogFileNamesAsString; } - private SSHLauncher getSSHL(ServiceLocator habitat) { - return habitat.getService(SSHLauncher.class); - } - - private File makingDirectory(String path) { - File targetDir = new File(path); - boolean created = false; - boolean deleted = false; + private File makingDirectory(Path path) { + File targetDir = path.toFile(); if (targetDir.exists()) { - deleted = targetDir.delete(); - if (!deleted) { + if (!targetDir.delete()) { return targetDir; } - } - created = targetDir.mkdir(); - if (created) { + if (targetDir.mkdir()) { return targetDir; } return null; - } - public String getLoggingDirectoryForNode(String instanceLogFileDirectory, Node node, String sNode, String instanceName) { - String loggingDir = ""; - + public Path getLoggingDirectoryForNode(String instanceLogFileDirectory, Node node, String sNode, String instanceName) { if (instanceLogFileDirectory.contains("${com.sun.aas.instanceRoot}/logs") && node.getNodeDir() != null) { // this code is used if no changes made in logging.properties file - loggingDir = node.getNodeDir() + File.separator + sNode - + File.separator + instanceName + File.separator + "logs"; - } else if (instanceLogFileDirectory.contains("${com.sun.aas.instanceRoot}/logs") && node.getInstallDir() != null) { - loggingDir = node.getInstallDir() + File.separator + "glassfish" + File.separator + "nodes" - + File.separator + sNode + File.separator + instanceName + File.separator + "logs"; + return new File(node.getNodeDir()).toPath().resolve(Path.of(sNode, instanceName, "logs")); + } else if (instanceLogFileDirectory.contains("${com.sun.aas.instanceRoot}/logs") + && node.getInstallDir() != null) { + return new File(node.getInstallDir()).toPath() + .resolve(Path.of("glassfish", "nodes", sNode, instanceName, "logs")); } else { - loggingDir = instanceLogFileDirectory.substring(0, instanceLogFileDirectory.lastIndexOf(File.separator)); + return new File(instanceLogFileDirectory).toPath(); } - - return loggingDir; } - public String getLoggingDirectoryForNodeWhenNoFilesFound(String instanceLogFileDirectory, Node node, String sNode, String instanceName) { - String loggingDir = ""; - + public Path getLoggingDirectoryForNodeWhenNoFilesFound(String instanceLogFileDirectory, Node node, String sNode, String instanceName) { if (node.getNodeDir() != null) { // this code is used if no changes made in logging.properties file - loggingDir = node.getNodeDir() + File.separator + sNode - + File.separator + instanceName + File.separator + "logs"; + return new File(node.getNodeDir()).toPath().resolve(Path.of(sNode, instanceName, "logs")); } else if (node.getInstallDir() != null) { - loggingDir = node.getInstallDir() + File.separator + "glassfish" + File.separator + "nodes" - + File.separator + sNode + File.separator + instanceName + File.separator + "logs"; + return new File(node.getInstallDir()).toPath().resolve(Path.of("glassfish", "nodes", sNode, instanceName, "logs")); } else { - loggingDir = instanceLogFileDirectory.substring(0, instanceLogFileDirectory.lastIndexOf(File.separator)); + return new File(instanceLogFileDirectory).toPath(); } - - return loggingDir; - }