/*
* 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.ignite.internal.util.nodestart;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.cluster.ClusterStartNodeResult;
import org.apache.ignite.internal.IgniteInterruptedCheckedException;
import org.apache.ignite.internal.cluster.ClusterStartNodeResultImpl;
import org.apache.ignite.internal.util.typedef.X;
import org.apache.ignite.internal.util.typedef.internal.SB;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.resources.LoggerResource;
import static org.apache.ignite.IgniteSystemProperties.IGNITE_SSH_HOST;
import static org.apache.ignite.IgniteSystemProperties.IGNITE_SSH_USER_NAME;
/**
* SSH-based node starter.
*/
public class StartNodeCallableImpl implements StartNodeCallable {
/** Default Ignite home path for Windows (taken from environment variable). */
private static final String DFLT_IGNITE_HOME_WIN = "%IGNITE_HOME%";
/** Default Ignite home path for Linux (taken from environment variable). */
private static final String DFLT_IGNITE_HOME_LINUX = "$IGNITE_HOME";
/** Default start script path for Linux. */
private static final String DFLT_SCRIPT_LINUX = "bin/ignite.sh -v";
/** Date format for log file name. */
private static final SimpleDateFormat FILE_NAME_DATE_FORMAT = new SimpleDateFormat("MM-dd-yyyy--HH-mm-ss");
/** Specification. */
private final IgniteRemoteStartSpecification spec;
/** Connection timeout. */
private final int timeout;
/** Logger. */
@LoggerResource
private IgniteLogger log;
/**
* Required by Externalizable.
*/
public StartNodeCallableImpl() {
spec = null;
timeout = 0;
assert false;
}
/**
* Constructor.
*
* @param spec Specification.
* @param timeout Connection timeout.
*/
public StartNodeCallableImpl(IgniteRemoteStartSpecification spec, int timeout) {
assert spec != null;
this.spec = spec;
this.timeout = timeout;
}
/** {@inheritDoc} */
@Override public ClusterStartNodeResult call() {
JSch ssh = new JSch();
Session ses = null;
try {
if (spec.key() != null)
ssh.addIdentity(spec.key().getAbsolutePath());
ses = ssh.getSession(spec.username(), spec.host(), spec.port());
if (spec.password() != null)
ses.setPassword(spec.password());
ses.setConfig("StrictHostKeyChecking", "no");
ses.connect(timeout);
boolean win = isWindows(ses);
char separator = win ? '\\' : '/';
spec.fixPaths(separator);
String igniteHome = spec.igniteHome();
if (igniteHome == null)
igniteHome = win ? DFLT_IGNITE_HOME_WIN : DFLT_IGNITE_HOME_LINUX;
String script = spec.script();
if (script == null)
script = DFLT_SCRIPT_LINUX;
String cfg = spec.configuration();
if (cfg == null)
cfg = "";
String startNodeCmd;
String scriptOutputFileName = FILE_NAME_DATE_FORMAT.format(new Date()) + '-'
+ UUID.randomUUID().toString().substring(0, 8) + ".log";
if (win)
throw new UnsupportedOperationException("Apache Ignite cannot be auto-started on Windows from IgniteCluster.startNodes(…) API.");
else { // Assume Unix.
int spaceIdx = script.indexOf(' ');
String scriptPath = spaceIdx > -1 ? script.substring(0, spaceIdx) : script;
String scriptArgs = spaceIdx > -1 ? script.substring(spaceIdx + 1) : "";
String rmtLogArgs = buildRemoteLogArguments(spec.username(), spec.host());
String tmpDir = env(ses, "$TMPDIR", "/tmp/");
String scriptOutputDir = tmpDir + "ignite-startNodes";
shell(ses, "mkdir " + scriptOutputDir);
// Mac os don't support ~ in double quotes. Trying get home path from remote system.
if (igniteHome.startsWith("~")) {
String homeDir = env(ses, "$HOME", "~");
igniteHome = igniteHome.replaceFirst("~", homeDir);
}
startNodeCmd = new SB().
// Console output is consumed, started nodes must use Ignite file appenders for log.
a("nohup ").
a("\"").a(igniteHome).a('/').a(scriptPath).a("\"").
a(" ").a(scriptArgs).
a(!cfg.isEmpty() ? " \"" : "").a(cfg).a(!cfg.isEmpty() ? "\"" : "").
a(rmtLogArgs).
a(" > ").a(scriptOutputDir).a("/").a(scriptOutputFileName).a(" 2>& 1 &").
toString();
}
info("Starting remote node with SSH command: " + startNodeCmd, spec.logger(), log);
shell(ses, startNodeCmd);
return new ClusterStartNodeResultImpl(spec.host(), true, null);
}
catch (IgniteInterruptedCheckedException e) {
return new ClusterStartNodeResultImpl(spec.host(), false, e.getMessage());
}
catch (Exception e) {
return new ClusterStartNodeResultImpl(spec.host(), false, X.getFullStackTrace(e));
}
finally {
if (ses != null && ses.isConnected())
ses.disconnect();
}
}
/**
* Executes command using {@code shell} channel.
*
* @param ses SSH session.
* @param cmd Command.
* @throws JSchException In case of SSH error.
* @throws IOException If IO error occurs.
* @throws IgniteInterruptedCheckedException If thread was interrupted while waiting.
*/
private void shell(Session ses, String cmd) throws JSchException, IOException, IgniteInterruptedCheckedException {
ChannelShell ch = null;
try {
ch = (ChannelShell)ses.openChannel("shell");
ch.connect();
try (PrintStream out = new PrintStream(ch.getOutputStream(), true)) {
out.println(cmd);
U.sleep(1000);
}
}
finally {
if (ch != null && ch.isConnected())
ch.disconnect();
}
}
/**
* Checks whether host is running Windows OS.
*
* @param ses SSH session.
* @return Whether host is running Windows OS.
* @throws JSchException In case of SSH error.
*/
private boolean isWindows(Session ses) throws JSchException {
try {
return exec(ses, "cmd.exe") != null;
}
catch (IOException ignored) {
return false;
}
}
/**
* Gets the value of the specified environment variable.
*
* @param ses SSH session.
* @param name environment variable name.
* @param dflt default value.
* @return environment variable value.
* @throws JSchException In case of SSH error.
*/
private String env(Session ses, String name, String dflt) throws JSchException {
try {
return exec(ses, "echo " + name);
}
catch (IOException ignored) {
return dflt;
}
}
/**
* Gets the value of the specified environment variable.
*
* @param ses SSH session.
* @param cmd environment variable name.
* @return environment variable value.
* @throws JSchException In case of SSH error.
* @throws IOException If failed.
*/
private String exec(Session ses, String cmd) throws JSchException, IOException {
ChannelExec ch = null;
try {
ch = (ChannelExec)ses.openChannel("exec");
ch.setCommand(cmd);
ch.connect();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(ch.getInputStream()))) {
return reader.readLine();
}
}
finally {
if (ch != null && ch.isConnected())
ch.disconnect();
}
}
/**
* Builds ignite.sh attributes to set up SSH username and password and log directory for started node.
*
* @param username SSH user name.
* @param host Host.
* @return {@code ignite.sh} script arguments.
*/
private String buildRemoteLogArguments(String username, String host) {
assert username != null;
assert host != null;
SB sb = new SB();
sb.a(" -J-D").a(IGNITE_SSH_HOST).a("=\"").a(host).a("\"").
a(" -J-D").a(IGNITE_SSH_USER_NAME).a("=\"").a(username).a("\"");
return sb.toString();
}
/**
* @param log Logger.
* @return This callable for chaining method calls.
*/
public StartNodeCallable setLogger(IgniteLogger log) {
this.log = log;
return this;
}
/**
* Log info message to loggers.
*
* @param msg Message text.
* @param loggers Loggers.
*/
private void info(String msg, IgniteLogger... loggers) {
for (IgniteLogger logger : loggers)
if (logger != null && logger.isInfoEnabled())
logger.info(msg);
}
}