/*
* 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.
*/
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 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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
/**
* For ssh and scp commands, delegating to system calls.
*/
public class SshCliTool extends SshAbstractTool implements SshTool {
// TODO No retry support, with backoffLimitedRetryHandler
private static final Logger LOG = LoggerFactory.getLogger(SshCliTool.class);
public static final ConfigKey<String> PROP_SSH_EXECUTABLE = ConfigKeys.newStringConfigKey("sshExecutable", "command to execute for ssh (defaults to \"ssh\", but could be overridden to sshg3 for Tectia for example)", "ssh");
public static final ConfigKey<String> PROP_SSH_FLAGS = ConfigKeys.newStringConfigKey("sshFlags", "flags to pass to ssh, as a space separated list", "");
public static final ConfigKey<String> PROP_SCP_EXECUTABLE = ConfigKeys.newStringConfigKey("scpExecutable", "command to execute for scp (defaults to \"scp\", but could be overridden to scpg3 for Tectia for example)", "scp");
public static Builder<SshCliTool,?> builder() {
return new ConcreteBuilder();
}
private static class ConcreteBuilder extends Builder<SshCliTool, ConcreteBuilder> {
}
public static class Builder<T extends SshCliTool, B extends Builder<T,B>> extends AbstractSshToolBuilder<T,B> {
private String sshExecutable;
private String sshFlags;
private String scpExecutable;
@Override
public B from(Map<String,?> props) {
super.from(props);
sshExecutable = getOptionalVal(props, PROP_SSH_EXECUTABLE);
sshFlags = getOptionalVal(props, PROP_SSH_FLAGS);
scpExecutable = getOptionalVal(props, PROP_SCP_EXECUTABLE);
return self();
}
public B sshExecutable(String val) {
this.sshExecutable = val; return self();
}
public B scpExecutable(String val) {
this.scpExecutable = val; return self();
}
@SuppressWarnings("unchecked")
public T build() {
return (T) new SshCliTool(this);
}
}
private final String sshExecutable;
private final String sshFlags;
private final String scpExecutable;
public SshCliTool(Map<String,?> map) {
this(builder().from(map));
}
private SshCliTool(Builder<?,?> builder) {
super(builder);
sshExecutable = checkNotNull(builder.sshExecutable);
sshFlags = checkNotNull(builder.sshFlags);
scpExecutable = checkNotNull(builder.scpExecutable);
if (LOG.isTraceEnabled()) LOG.trace("Created SshCliTool {} ({})", this, System.identityHashCode(this));
}
@Override
public void connect() {
// no-op
}
@Override
public void connect(int maxAttempts) {
// no-op
}
@Override
public void disconnect() {
if (LOG.isTraceEnabled()) LOG.trace("Disconnecting SshCliTool {} ({}) - no-op", this, System.identityHashCode(this));
// no-op
}
@Override
public boolean isConnected() {
// TODO Always pretends to be connected
return true;
}
@Override
public int copyToServer(java.util.Map<String,?> props, byte[] contents, String pathAndFileOnRemoteServer) {
return copyTempFileToServer(props, writeTempFile(contents), pathAndFileOnRemoteServer);
}
@Override
public int copyToServer(java.util.Map<String,?> props, InputStream contents, String pathAndFileOnRemoteServer) {
return copyTempFileToServer(props, writeTempFile(contents), pathAndFileOnRemoteServer);
}
@Override
public int copyToServer(Map<String,?> props, File f, String pathAndFileOnRemoteServer) {
if (hasVal(props, PROP_LAST_MODIFICATION_DATE)) {
LOG.warn("Unsupported ssh feature, setting lastModificationDate for {}:{}", this, pathAndFileOnRemoteServer);
}
if (hasVal(props, PROP_LAST_ACCESS_DATE)) {
LOG.warn("Unsupported ssh feature, setting lastAccessDate for {}:{}", this, pathAndFileOnRemoteServer);
}
String permissions = getOptionalVal(props, PROP_PERMISSIONS);
int uid = getOptionalVal(props, PROP_OWNER_UID);
int result = scpToServer(props, f, pathAndFileOnRemoteServer);
if (result == 0) {
result = chmodOnServer(props, permissions, pathAndFileOnRemoteServer);
if (result == 0) {
if (uid != -1) {
result = chownOnServer(props, uid, pathAndFileOnRemoteServer);
if (result != 0) {
LOG.warn("Error setting file owner to {}, after copying file {} to {}:{}; exit code {}", new Object[] { uid, pathAndFileOnRemoteServer, this, f, result });
}
}
} else {
LOG.warn("Error setting file permissions to {}, after copying file {} to {}:{}; exit code {}", new Object[] { permissions, pathAndFileOnRemoteServer, this, f, result });
}
} else {
LOG.warn("Error copying file {} to {}:{}; exit code {}", new Object[] {pathAndFileOnRemoteServer, this, f, result});
}
return result;
}
private int chownOnServer(Map<String,?> props, int uid, String remote) {
return sshExec(props, "chown "+uid+" "+remote);
}
private int copyTempFileToServer(Map<String,?> props, File f, String pathAndFileOnRemoteServer) {
try {
return copyToServer(props, f, pathAndFileOnRemoteServer);
} finally {
f.delete();
}
}
@Override
public int copyFromServer(Map<String,?> props, String pathAndFileOnRemoteServer, File localFile) {
return scpFromServer(props, pathAndFileOnRemoteServer, localFile);
}
@Override
public int execScript(final Map<String,?> props, final List<String> commands, final Map<String,?> env) {
return new ToolAbstractExecScript(props) {
public int run() {
String scriptContents = toScript(props, commands, env);
if (LOG.isTraceEnabled()) LOG.trace("Running shell command at {} as script: {}", host, scriptContents);
copyTempFileToServer(ImmutableMap.of("permissions", "0700"), writeTempFile(scriptContents), scriptPath);
String cmd = Strings.join(buildRunScriptCommand(), separator);
return asInt(sshExec(props, cmd), -1);
}
}.run();
}
@Override
public int execCommands(Map<String,?> props, List<String> commands, Map<String,?> env) {
Map<String,Object> props2 = new MutableMap<String,Object>();
if (props!=null) props2.putAll(props);
props2.put(SshTool.PROP_NO_EXTRA_OUTPUT.getName(), true);
return execScript(props2, commands, env);
}
private int scpToServer(Map<String,?> props, File local, String remote) {
String to = (Strings.isEmpty(getUsername()) ? "" : getUsername()+"@")+getHostAddress()+":"+remote;
return scpExec(props, local.getAbsolutePath(), to);
}
private int scpFromServer(Map<String,?> props, String remote, File local) {
String from = (Strings.isEmpty(getUsername()) ? "" : getUsername()+"@")+getHostAddress()+":"+remote;
return scpExec(props, from, local.getAbsolutePath());
}
private int chmodOnServer(Map<String,?> props, String permissions, String remote) {
return sshExec(props, "chmod "+permissions+" "+remote);
}
private int scpExec(Map<String,?> props, String from, String to) {
File tempFile = null;
try {
List<String> cmd = Lists.newArrayList();
cmd.add(getOptionalVal(props, PROP_SCP_EXECUTABLE, scpExecutable));
if (privateKeyFile != null) {
cmd.add("-i");
cmd.add(privateKeyFile.getAbsolutePath());
} else if (privateKeyData != null) {
tempFile = writeTempFile(privateKeyData);
cmd.add("-i");
cmd.add(tempFile.getAbsolutePath());
}
if (!strictHostKeyChecking) {
cmd.add("-o");
cmd.add("StrictHostKeyChecking=no");
}
if (port != 22) {
cmd.add("-P");
cmd.add(""+port);
}
cmd.add(from);
cmd.add(to);
if (LOG.isTraceEnabled()) LOG.trace("Executing with command: {}", cmd);
int result = execProcess(props, cmd);
if (LOG.isTraceEnabled()) LOG.trace("Executed command: {}; exit code {}", cmd, result);
return result;
} finally {
if (tempFile != null) tempFile.delete();
}
}
private int sshExec(Map<String,?> props, String command) {
File tempKeyFile = null;
try {
List<String> cmd = Lists.newArrayList();
cmd.add(getOptionalVal(props, PROP_SSH_EXECUTABLE, sshExecutable));
String propsFlags = getOptionalVal(props, PROP_SSH_FLAGS, sshFlags);
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);
cmd.add("-i");
cmd.add(tempKeyFile.getAbsolutePath());
}
if (!strictHostKeyChecking) {
cmd.add("-o");
cmd.add("StrictHostKeyChecking=no");
}
if (port != 22) {
cmd.add("-P");
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);
// hover that doesn't seem to be a problem the way we use it from brooklyn
// (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));
// previously we tried these approaches:
//cmd.add("$(<"+tempCmdFile.getAbsolutePath()+")");
// only pays attention to the first word; the "; echo Executing ..." get treated as arguments
// to the script in the first word, when invoked from java (when invoked from prompt the behaviour is as desired)
//cmd.add("\""+command+"\"");
// 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);
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) {
OutputStream out = getOptionalVal(props, PROP_OUT_STREAM);
OutputStream err = getOptionalVal(props, PROP_ERR_STREAM);
return ProcessTool.execSingleProcess(cmdWords, null, (File)null, out, err, this);
}
}