/***************************************************************************
* Copyright (c) 2012-2015 VMware, Inc. All Rights Reserved.
* Licensed 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 com.vmware.bdd.service.sp;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.concurrent.Callable;
import com.jcraft.jsch.JSchException;
import com.vmware.bdd.entity.NodeEntity;
import com.vmware.bdd.utils.ShellCommandExecutor;
import org.apache.log4j.Logger;
import com.vmware.aurora.global.Configuration;
import com.vmware.bdd.utils.CommonUtil;
import com.vmware.bdd.utils.Constants;
import com.vmware.bdd.utils.SSHUtil;
import com.vmware.bdd.exception.SetPasswordException;
/**
* Store Procedure of setting password for a vm
*/
public class SetVMPasswordSP implements Callable<Void> {
private static final Logger logger = Logger.getLogger(SetVMPasswordSP.class);
private NodeEntity node;
private String nodeIP;
private String password;
private String privateKeyFile;
private String sshUser;
private int sshPort;
private String sudoCmd = CommonUtil.getCustomizedSudoCmd();
private final static int SETUP_PASSWORDLESS_LOGIN_TIMEOUT = 120; //in seconds
private int setupPasswordLessLoginTimeout;
public SetVMPasswordSP(NodeEntity node, String password) {
this.node = node;
this.nodeIP = node.getPrimaryMgtIpV4();
this.password = password;
this.sshUser = Configuration.getString(Constants.SSH_USER_CONFIG_NAME, Constants.DEFAULT_SSH_USER_NAME);
this.sshPort = Configuration.getInt(Constants.SSH_PORT_CONFIG_NAME, Constants.DEFAULT_SSH_PORT);
String keyFileName= Configuration.getString(Constants.SSH_PRIVATE_KEY_CONFIG_NAME, Constants.SSH_PRIVATE_KEY_FILE_NAME);
String serengetiHome = Configuration.getString(Constants.SERENGETI_HOME, Constants.DEFAULT_SERENGETI_HOME);
this.privateKeyFile = serengetiHome + "/.ssh/" + keyFileName;
this.setupPasswordLessLoginTimeout = Configuration.getInt(Constants.PASSWORDLESS_LOGIN_TIMEOUT, SETUP_PASSWORDLESS_LOGIN_TIMEOUT);
}
@Override
public Void call() throws Exception {
setPasswordForNode();
return null;
}
public boolean setPasswordForNode() throws Exception {
logger.info("Setting password for node " + nodeIP);
setupPasswordLessLogin(nodeIP);
boolean useDefaultPassword = Configuration.getBoolean(Constants.SERENGETI_USE_DEFAULT_PASSWORD, false);
if (!useDefaultPassword) {
// if user set customized password, set the customized password for it
// if user didn't set customized password, set random password for it
if (this.password == null) {
setRandomPassword();
} else {
if (!CommonUtil.validateClusterPassword(password)) {
logger.error("Set customized password for " + nodeIP + " failed. Password contains invalid characters");
throw SetPasswordException.INVALID_PASSWORD(Constants.PASSWORD_REQUIREMENT);
}
setCustomizedPassword(password);
}
} else {
logger.info("use default password for node " + nodeIP);
}
removeSSHLimit();
return true;
}
private boolean removeSSHLimit() throws Exception {
String scriptFileName = Configuration.getString(Constants.REMOVE_SSH_LIMIT_SCRIPT, Constants.DEFAULT_REMOVE_SSH_LIMIT_SCRIPT);
String script = getScriptName(scriptFileName);
String cmd = sudoCmd + " " + script;
boolean succeed = false;
SSHUtil sshUtil = new SSHUtil();
for (int i = 0; i < Constants.SET_PASSWORD_MAX_RETRY_TIMES; i++) {
try {
succeed = sshUtil.execCmd(sshUser, privateKeyFile, nodeIP, sshPort, cmd, null, null);
if (succeed) {
logger.info("Remove ssh limit for " + nodeIP + " succceed");
return true;
}
} catch (JSchException e) {
logger.warn("Caught exception when remove ssh limit for " + nodeIP);
}
}
throw SetPasswordException.FAIL_TO_REMOVE_SSH_LIMIT(nodeIP);
}
private boolean setRandomPassword() throws Exception {
logger.info("Setting random password for " + nodeIP);
String scriptFileName = Configuration.getString(Constants.SET_PASSWORD_SCRIPT_CONFIG_NAME, Constants.DEFAULT_SET_PASSWORD_SCRIPT);
String script = getScriptName(scriptFileName);
String cmd = sudoCmd + " " + script + " -a";
return setPassword(cmd, null);
}
private boolean setCustomizedPassword(String password) throws Exception {
logger.info("Setting customized password for " + nodeIP);
String cmd = generateSetPasswdCommand(Constants.SET_PASSWORD_SCRIPT_CONFIG_NAME);
InputStream in = null;
try {
in = parseInputStream(new String(password + Constants.NEW_LINE + password + Constants.NEW_LINE));
return setPassword(cmd, in);
} finally {
if (in != null) {
in.close();
}
}
}
private boolean setPassword(String cmd, InputStream in) throws Exception {
boolean setPasswordSucceed = false;
int jschExceptionCount = 0;
for (int i = 0; i < Constants.SET_PASSWORD_MAX_RETRY_TIMES; i++) {
try {
SSHUtil sshUtil = new SSHUtil();
setPasswordSucceed = sshUtil.execCmd(sshUser, privateKeyFile, nodeIP, sshPort, cmd, in, null);
} catch (JSchException e) {
if (++jschExceptionCount == Constants.SET_PASSWORD_MAX_RETRY_TIMES) {
throw SetPasswordException.GOT_JSCH_EXCEPTION_WHEN_SET_PASSWORD(e, nodeIP);
}
}
if (setPasswordSucceed) {
handleTty();
break;
} else {
logger.info("Set password for " + nodeIP + " failed for " + (i + 1)
+ " times. Retrying after 2 seconds....");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
logger.info("Sleep interrupted, retrying immediately");
}
}
}
if (setPasswordSucceed) {
logger.info("Set password for " + nodeIP + " succeed");
return true;
} else {
logger.error("set password for " + nodeIP + " failed");
throw SetPasswordException.FAIL_TO_SET_PASSWORD(nodeIP, null);
}
}
private boolean setupPasswordLessLogin(String hostIP) throws Exception {
String scriptName = Configuration.getString(Constants.PASSWORDLESS_LOGIN_SCRIPT, Constants.DEFAULT_PASSWORDLESS_LOGIN_SCRIPT);
String script = getScriptName(scriptName);
String user = Configuration.getString(Constants.SSH_USER_CONFIG_NAME, Constants.DEFAULT_SSH_USER_NAME);
String password = Configuration.getString(Constants.SERENGETI_DEFAULT_PASSWORD);
String cmd = script + " " + hostIP + " " + user + " " + password;
int sleepTime = Configuration.getInt(Constants.SSH_SLEEP_TIME_BEFORE_RETRY, Constants.DEFAULT_SSH_SLEEP_TIME_BEFORE_RETRY);
int maxRetryTimes = Configuration.getInt(Constants.SSH_MAX_RETRY_TIMES, Constants.SET_PASSWORD_MAX_RETRY_TIMES);
int timeoutCount = 0;
for (int i = 0; i < maxRetryTimes; i++) {
try {
ShellCommandExecutor.execCmd(cmd, null, null, this.setupPasswordLessLoginTimeout, Constants.MSG_SETTING_UP_PASSWORDLESS_LOGIN + hostIP + ".");
logger.info("Set passwordless login successfully for " + hostIP);
return true;
} catch (Exception e) {
if (e.getMessage().contains(Constants.EXEC_COMMAND_TIMEOUT)) {
timeoutCount++;
}
logger.warn("Set passwordless login no. " + i + " and got exception: " + e.getMessage(), e);
//sometimes the sshd daemon may not ready, add sleep can avoid the race. Now, the longest wait time is 150s
try {
Thread.sleep(sleepTime);
} catch (InterruptedException ie) {
logger.warn("Interrupted when waiting for setupPasswordlessLogin, retry immediately.", ie);
}
}
}
logger.error("Failed to set passwordless login for " + hostIP);
if (timeoutCount == Constants.SET_PASSWORD_MAX_RETRY_TIMES) {
throw SetPasswordException.SETUP_PASSWORDLESS_LOGIN_TIMEOUT(null, hostIP);
}
throw SetPasswordException.FAIL_TO_SETUP_PASSWORDLESS_LOGIN(hostIP);
}
private void handleTty() throws Exception {
setupLoginTty();
refreshTty();
}
private void refreshTty() {
String ttyName = Configuration.getString(Constants.SERENGETI_TTY_NAME, Constants.SERENGETI_DEFAULT_TTY_NAME);
String cmd = "ps aux | grep " + ttyName + " | grep -v \"grep\" | awk '{print $2}' | " + sudoCmd + " xargs kill -9";
SSHUtil sshUtil = new SSHUtil();
//if refresh failed, user still can manually refresh tty by Ctrl+C, so don't need to check whether
//it succeed or not
try {
boolean refreshTtySucceed = sshUtil.execCmd(sshUser, privateKeyFile, nodeIP, sshPort, cmd, null, null);
logger.info("Refresh " + ttyName + " on " + nodeIP + (refreshTtySucceed ? "succeed" : "failed") + ".");
} catch (JSchException e) {
logger.error("Got exception when refresh tty on " + nodeIP, e);
}
}
private void setupLoginTty() throws Exception {
String setupTtyScriptName = Configuration.getString(Constants.SERENGETI_SETUP_LOGIN_TTY_SCRIPT, Constants.SERENGETI_DEFAULT_SETUP_LOGIN_TTY_SCRIPT);
String setupTtyScript = getScriptName(setupTtyScriptName);
String cmd = sudoCmd + " " + setupTtyScript;
String action = "Setup login tty for " + nodeIP;
logger.info(action + " command is: " + cmd);
SSHUtil sshUtil = new SSHUtil();
String errMsg = null;
for (int i = 0; i < Constants.SET_PASSWORD_MAX_RETRY_TIMES; i++) {
try {
if (sshUtil.execCmd(sshUser, privateKeyFile, nodeIP, sshPort, cmd, null, null)) {
logger.info(action + " succeed.");
return;
}
} catch (JSchException e) {
logger.warn("Got exception when " + action, e);
if (errMsg == null) {
errMsg = e.getMessage();
}
}
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
logger.info("Interrupted when waiting for setup login tty, retry immediately...");
}
}
logger.info(action + " failed");
throw SetPasswordException.FAIL_TO_SETUP_LOGIN_TTY(nodeIP, errMsg);
}
public ByteArrayInputStream parseInputStream(String in) throws Exception {
ByteArrayInputStream input=new ByteArrayInputStream(in.getBytes());
return input;
}
private String generateSetPasswdCommand(String setPasswdScriptConfig) {
String scriptFileName = Configuration.getString(setPasswdScriptConfig, Constants.DEFAULT_SET_PASSWORD_SCRIPT);
String script = getScriptName(scriptFileName);
return sudoCmd + " " + script + " -u";
}
private String getScriptName(String scriptFileName) {
String serengetiSbinDir = Configuration.getString(Constants.SERENGETI_SBIN_DIR, Constants.DEFAULT_SERENGETI_SBIN_DIR);
return serengetiSbinDir + "/" + scriptFileName;
}
public String getNodeIP() {
return nodeIP;
}
public void setNodeIP(String nodeIP) {
this.nodeIP = nodeIP;
}
public NodeEntity getNodeEntity() {
return node;
}
}