This is an automated email from the ASF dual-hosted git repository. heneveld pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git
commit 81c2dafa31e4d549f04ec0db5d81dd7aab689224 Author: Mykola Mandra <[email protected]> AuthorDate: Tue Aug 23 16:17:00 2022 +0100 SSH and SCP executables to get params via env vars This enables configuring a custom SSH adn SCP CLI tools allowing processing applying password instead of private key for identification, which is not allowed in SshCliTool by default. Signed-off-by: Mykola Mandra <[email protected]> --- .../util/core/internal/ssh/cli/SshCliTool.java | 128 +++++++++++++-------- .../ssh/cli/SshCliToolIntegrationTest.java | 110 +++++++++++++++--- core/src/test/resources/scp-executable.sh | 27 +++++ core/src/test/resources/ssh-executable.sh | 28 +++++ 4 files changed, 231 insertions(+), 62 deletions(-) diff --git a/core/src/main/java/org/apache/brooklyn/util/core/internal/ssh/cli/SshCliTool.java b/core/src/main/java/org/apache/brooklyn/util/core/internal/ssh/cli/SshCliTool.java index 19dccdcb43..ab1adf4c01 100644 --- a/core/src/main/java/org/apache/brooklyn/util/core/internal/ssh/cli/SshCliTool.java +++ b/core/src/main/java/org/apache/brooklyn/util/core/internal/ssh/cli/SshCliTool.java @@ -18,29 +18,28 @@ */ package org.apache.brooklyn.util.core.internal.ssh.cli; -import static com.google.common.base.Preconditions.checkNotNull; - -import java.io.File; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.ConfigKeys; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.internal.ssh.SshAbstractTool; import org.apache.brooklyn.util.core.internal.ssh.SshTool; -import org.apache.brooklyn.util.core.internal.ssh.cli.SshCliTool; import org.apache.brooklyn.util.core.internal.ssh.process.ProcessTool; -import org.apache.brooklyn.util.text.Strings; import org.apache.brooklyn.util.text.StringEscapes.BashStringEscapes; +import org.apache.brooklyn.util.text.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; /** * For ssh and scp commands, delegating to system calls. @@ -217,67 +216,92 @@ public class SshCliTool extends SshAbstractTool implements SshTool { } private int scpExec(Map<String,?> props, String from, String to) { - File tempFile = null; + File tempKeyFile = Objects.isNull(privateKeyData) ? null : writeTempFile(privateKeyData); + try { + + final String scpPassword = Strings.isEmpty(password) ? "" : password; + final File scpTempKeyFile = Objects.isNull(privateKeyFile) ? tempKeyFile : null; + List<String> cmd = Lists.newArrayList(); cmd.add(getOptionalVal(props, PROP_SCP_EXECUTABLE, scpExecutable)); + + // set batch mode cmd.add("-B"); - if (privateKeyFile != null) { - cmd.add("-i"); - cmd.add(privateKeyFile.getAbsolutePath()); - } else if (privateKeyData != null) { - tempFile = writeTempFile(privateKeyData); + + if (Objects.nonNull(scpTempKeyFile)) { cmd.add("-i"); - cmd.add(tempFile.getAbsolutePath()); + cmd.add(scpTempKeyFile.getAbsolutePath()); } + if (!strictHostKeyChecking) { cmd.add("-o"); cmd.add("StrictHostKeyChecking=no"); } + if (port != 22) { cmd.add("-P"); - cmd.add(""+port); + cmd.add("" + port); } + cmd.add(from); cmd.add(to); - - if (LOG.isTraceEnabled()) LOG.trace("Executing with command: {}", cmd); - int result = execProcess(props, cmd); - + + Map<String, String> env = MutableMap.of(); + env.put("SCP_TEMP_KEY_FILE", Objects.isNull(scpTempKeyFile) ? "" : scpTempKeyFile.getAbsolutePath()); + env.put("SCP_PASSWORD", scpPassword); + env.put("SCP_FROM", from); + env.put("SCP_TO", to); + + + if (LOG.isTraceEnabled()) LOG.trace("Executing command: {}; with env: {}", cmd, env); + int result = execProcess(props, cmd, env); if (LOG.isTraceEnabled()) LOG.trace("Executed command: {}; exit code {}", cmd, result); + return result; } finally { - if (tempFile != null) tempFile.delete(); + if (tempKeyFile != null) tempKeyFile.delete(); } } - + private int sshExec(Map<String,?> props, String command) { - File tempKeyFile = null; + File tempKeyFile = Objects.isNull(privateKeyData) ? null : writeTempFile(privateKeyData); + try { + + final String sshUser = Strings.isEmpty(getUsername()) ? "" : getUsername(); + final String sshHost = getHostAddress(); + final String sshPassword = Strings.isEmpty(password) ? "" : password; + final File sshKeyFile = Objects.isNull(privateKeyFile) ? tempKeyFile : null; + List<String> cmd = Lists.newArrayList(); cmd.add(getOptionalVal(props, PROP_SSH_EXECUTABLE, sshExecutable)); - String propsFlags = getOptionalVal(props, PROP_SSH_FLAGS, sshFlags); + + String propFlags = getOptionalVal(props, PROP_SSH_FLAGS, this.sshFlags); + if (Objects.nonNull(propFlags) && propFlags.trim().length() > 0) { + cmd.addAll(Arrays.asList(propFlags.trim().split(" "))); + } + + // set batch mode cmd.add("-o"); cmd.add("BatchMode=yes"); - if (propsFlags!=null && propsFlags.trim().length()>0) - cmd.addAll(Arrays.asList(propsFlags.trim().split(" "))); - if (privateKeyFile != null) { - cmd.add("-i"); - cmd.add(privateKeyFile.getAbsolutePath()); - } else if (privateKeyData != null) { - tempKeyFile = writeTempFile(privateKeyData); + + if (Objects.nonNull(sshKeyFile)) { cmd.add("-i"); - cmd.add(tempKeyFile.getAbsolutePath()); + cmd.add(sshKeyFile.getAbsolutePath()); } + if (!strictHostKeyChecking) { cmd.add("-o"); cmd.add("StrictHostKeyChecking=no"); } + if (port != 22) { cmd.add("-P"); - cmd.add(""+port); + cmd.add("" + port); } + if (allocatePTY) { // have to be careful with double -tt as it can leave a shell session active // when done from bash (ie ssh -tt localhost < /tmp/myscript.sh); @@ -285,9 +309,10 @@ public class SshCliTool extends SshAbstractTool implements SshTool { // (and note single -t doesn't work _programmatically_ since the input isn't a terminal) cmd.add("-tt"); } - cmd.add((Strings.isEmpty(getUsername()) ? "" : getUsername()+"@")+getHostAddress()); - - cmd.add("bash -c "+BashStringEscapes.wrapBash(command)); + + cmd.add((Strings.isEmpty(sshUser) ? "" : sshUser + "@") + sshHost); + + cmd.add("bash -c " + BashStringEscapes.wrapBash(command)); // previously we tried these approaches: //cmd.add("$(<"+tempCmdFile.getAbsolutePath()+")"); // only pays attention to the first word; the "; echo Executing ..." get treated as arguments @@ -296,21 +321,28 @@ public class SshCliTool extends SshAbstractTool implements SshTool { // only works if command is a single word //cmd.add(tempCmdFile.getAbsolutePath()); // above of course only works if the metafile is copied across - - if (LOG.isTraceEnabled()) LOG.trace("Executing ssh with command: {} (with {})", command, cmd); - int result = execProcess(props, cmd); - + + Map<String, String> env = MutableMap.of(); + env.put("SSH_HOST", sshHost); + env.put("SSH_USER", sshUser); + env.put("SSH_PASSWORD", sshPassword); + env.put("SSH_COMMAND_BODY", command); + env.put("SSH_TEMP_KEY_FILE", Objects.isNull(sshKeyFile) ? "" : sshKeyFile.getAbsolutePath()); + + if (LOG.isTraceEnabled()) LOG.trace("Executing command: {}; with env: {}", cmd, env); + int result = execProcess(props, cmd, env); if (LOG.isTraceEnabled()) LOG.trace("Executed command: {}; exit code {}", cmd, result); + return result; - + } finally { if (tempKeyFile != null) tempKeyFile.delete(); } } - private int execProcess(Map<String,?> props, List<String> cmdWords) { + private int execProcess(Map<String,?> props, List<String> cmdWords, Map<String,?> env) { OutputStream out = getOptionalVal(props, PROP_OUT_STREAM); OutputStream err = getOptionalVal(props, PROP_ERR_STREAM); - return ProcessTool.execSingleProcess(cmdWords, null, (File)null, out, err, this); + return ProcessTool.execSingleProcess(cmdWords, env, (File)null, out, err, this); } } diff --git a/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/cli/SshCliToolIntegrationTest.java b/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/cli/SshCliToolIntegrationTest.java index 31df773bc4..26998f5526 100644 --- a/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/cli/SshCliToolIntegrationTest.java +++ b/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/cli/SshCliToolIntegrationTest.java @@ -18,31 +18,29 @@ */ package org.apache.brooklyn.util.core.internal.ssh.cli; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.fail; - -import java.io.ByteArrayOutputStream; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.Map; - +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.internal.ssh.SshException; import org.apache.brooklyn.util.core.internal.ssh.SshTool; import org.apache.brooklyn.util.core.internal.ssh.SshToolAbstractIntegrationTest; -import org.apache.brooklyn.util.core.internal.ssh.cli.SshCliTool; +import org.apache.brooklyn.util.os.Os; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; import org.testng.annotations.Test; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermission; +import java.util.*; + +import static org.testng.Assert.*; /** - * Test the operation of the {@link SshJschTool} utility class. + * Test the operation of the {@link SshCliTool} utility class. */ public class SshCliToolIntegrationTest extends SshToolAbstractIntegrationTest { @@ -119,4 +117,88 @@ public class SshCliToolIntegrationTest extends SshToolAbstractIntegrationTest { assertEquals(exitcode, 123); } + @Test(groups = {"Integration"}) + public void testSshExecutable() throws IOException { + + String path = Objects.requireNonNull(getClass().getClassLoader().getResource("ssh-executable.sh")).getPath(); + Set<PosixFilePermission> perms = new HashSet<>(); + perms.add(PosixFilePermission.OWNER_EXECUTE); + perms.add(PosixFilePermission.OWNER_READ); + Files.setPosixFilePermissions(Paths.get(path), perms); + + final SshTool localTool = newTool(ImmutableMap.of( + "sshExecutable", path, + "user", Os.user(), + "host", "localhost", + "privateKeyData", "myKeyData", + "password", "testPassword")); + tools.add(localTool); + + try { + localTool.connect(); + Map<String,Object> props = new LinkedHashMap<>(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + props.put("out", out); + props.put("err", err); + int exitcode = localTool.execScript(props, Arrays.asList("echo hello err > /dev/stderr"), null); + Assert.assertEquals(0, exitcode, "exitCode=" + exitcode + ", but expected 0"); + log.debug("OUT from ssh -vvv command is: " + out); + log.debug("ERR from ssh -vvv command is: " + err); + + // Look for the rest of env vars to confirm we got them passed to sshExecutable. + String stdout = out.toString(); + assertTrue(stdout.contains("SSH_USER=" + Os.user()), "no SSH_USER in stdout: " + out); + assertTrue(stdout.contains("SSH_HOST=localhost"), "no SSH_HOST in stdout: " + out); + assertTrue(stdout.contains("SSH_PASSWORD=testPassword"), "no SSH_PASSWORD in stdout: " + out); + assertTrue(stdout.contains("SSH_COMMAND_BODY=/tmp/brooklyn-"), "no SSH_COMMAND_BODY in stdout: " + out); + assertTrue(stdout.contains("SSH_TEMP_KEY_FILE=/tmp/sshcopy-"), "no SSH_TEMP_KEY_FILE in stdout: " + out); + assertTrue(stdout.contains("myKeyData"), "no SSH_TEMP_KEY_FILE content in stdout: " + out); + + } catch (SshException e) { + if (!e.toString().contains("failed to connect")) throw e; + } + } + + @Test(groups = {"Integration"}) + public void testScpExecutable() throws IOException { + + String path = Objects.requireNonNull(getClass().getClassLoader().getResource("scp-executable.sh")).getPath(); + Set<PosixFilePermission> perms = new HashSet<>(); + perms.add(PosixFilePermission.OWNER_EXECUTE); + perms.add(PosixFilePermission.OWNER_READ); + Files.setPosixFilePermissions(Paths.get(path), perms); + + final SshTool localTool = newTool(ImmutableMap.of( + "scpExecutable", path, + "user", Os.user(), + "host", "localhost", +// "privateKeyData", "myKeyData", // TODO: loops to itself to copy the key file, skip in the test. + "password", "testPassword")); + tools.add(localTool); + + try { + localTool.connect(); + Map<String,Object> props = new LinkedHashMap<>(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + props.put("out", out); + props.put("err", err); + int exitcode = localTool.copyToServer(props, "echo hello world!\n".getBytes(), remoteFilePath); + + Assert.assertEquals(0, exitcode, "exitCode=" + exitcode + ", but expected 0"); + + String copiedFileContent = new String(Files.readAllBytes(Paths.get(remoteFilePath))); + log.info("Contents of copied file with custom scpExecutable: " + copiedFileContent); + + // Look for the rest of env vars to confirm we got them passed to scpExecutable. + assertTrue(copiedFileContent.contains("echo hello world!"), "no command in the remote file: " + out); + assertTrue(copiedFileContent.contains("SCP_PASSWORD=testPassword"), "no SCP_PASSWORD in the remote file: " + out); +// assertTrue(copiedFileContent.contains("SCP_TEMP_KEY_FILE="), "no SCP_TEMP_KEY_FILE in the remote file: " + out); +// assertTrue(copiedFileContent.contains("myKeyData"), "no SSH_TEMP_KEY_FILE content in stdout: " + out); + + } catch (SshException e) { + if (!e.toString().contains("failed to connect")) throw e; + } + } } diff --git a/core/src/test/resources/scp-executable.sh b/core/src/test/resources/scp-executable.sh new file mode 100644 index 0000000000..22cb6fa6b4 --- /dev/null +++ b/core/src/test/resources/scp-executable.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +chmod +w $SCP_FROM +echo SCP_TEMP_KEY_FILE=$SCP_TEMP_KEY_FILE >> $SCP_FROM +echo SCP_PASSWORD=$SCP_PASSWORD >> $SCP_FROM +echo SCP_FROM=$SCP_FROM >> $SCP_FROM +echo SCP_TO=$SCP_TO >> $SCP_FROM + +scp $SCP_FROM $SCP_TO diff --git a/core/src/test/resources/ssh-executable.sh b/core/src/test/resources/ssh-executable.sh new file mode 100644 index 0000000000..c13dea0809 --- /dev/null +++ b/core/src/test/resources/ssh-executable.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +echo SSH_USER=$SSH_USER +echo SSH_HOST=$SSH_HOST +echo SSH_PASSWORD=$SSH_PASSWORD +echo SSH_COMMAND_BODY=$SSH_COMMAND_BODY +echo SSH_TEMP_KEY_FILE=$SSH_TEMP_KEY_FILE + +# print contents of the key file +cat $SSH_TEMP_KEY_FILE \ No newline at end of file
