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

Reply via email to