package org.zstack.core.ansible;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.zstack.core.Platform;
import org.zstack.core.cloudbus.CloudBus;
import org.zstack.core.cloudbus.CloudBusCallBack;
import org.zstack.core.defer.Defer;
import org.zstack.core.defer.Deferred;
import org.zstack.header.core.Completion;
import org.zstack.header.exception.CloudRuntimeException;
import org.zstack.header.message.MessageReply;
import org.zstack.utils.ShellUtils;
import org.zstack.utils.Utils;
import org.zstack.utils.logging.CLogger;
import org.zstack.utils.network.NetworkUtils;
import org.zstack.utils.path.PathUtil;
import org.zstack.utils.ssh.SshResult;
import org.zstack.utils.ssh.SshShell;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.zstack.utils.CollectionDSL.e;
import static org.zstack.utils.CollectionDSL.map;
import static org.zstack.utils.StringDSL.ln;
/**
*/
@Configurable(preConstruction = true, autowire = Autowire.BY_TYPE)
public class AnsibleRunner {
private static final CLogger logger = Utils.getLogger(AnsibleRunner.class);
@Autowired
private AnsibleFacade asf;
@Autowired
private CloudBus bus;
private static String privKeyFile;
private List<AnsibleChecker> checkers = new ArrayList<AnsibleChecker>();
static {
privKeyFile = PathUtil.findFileOnClassPath(AnsibleConstant.RSA_PRIVATE_KEY).getAbsolutePath();
}
{
fullDeploy = AnsibleGlobalProperty.FULL_DEPLOY;
}
private String targetIp;
private String username;
private String password;
private String privateKey;
private int sshPort = 22;
private String playBookName;
private String playBookPath;
private Map<String, Object> arguments = new HashMap<String, Object>();
private int agentPort;
private boolean fullDeploy;
private boolean localPublicKey;
private boolean runOnLocal;
private AnsibleNeedRun ansibleNeedRun;
private String ansibleExecutable;
public String getAnsibleExecutable() {
return ansibleExecutable;
}
public void setAnsibleExecutable(String ansibleExecutable) {
this.ansibleExecutable = ansibleExecutable;
}
public String getPlayBookPath() {
return playBookPath;
}
public void setPlayBookPath(String playBookPath) {
this.playBookPath = playBookPath;
}
public AnsibleNeedRun getAnsibleNeedRun() {
return ansibleNeedRun;
}
public void setAnsibleNeedRun(AnsibleNeedRun ansibleNeedRun) {
this.ansibleNeedRun = ansibleNeedRun;
}
public boolean isRunOnLocal() {
return runOnLocal;
}
public void setRunOnLocal(boolean runOnLocal) {
this.runOnLocal = runOnLocal;
}
public boolean isLocalPublicKey() {
return localPublicKey;
}
public void setLocalPublicKey(boolean localPublicKey) {
this.localPublicKey = localPublicKey;
}
public void putArgument(String key, Object value) {
arguments.put(key, value);
}
public String getTargetIp() {
return targetIp;
}
public void setTargetIp(String targetIp) {
this.targetIp = targetIp;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPrivateKey() {
return privateKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
public int getSshPort() {
return sshPort;
}
public void setSshPort(int sshPort) {
this.sshPort = sshPort;
}
public String getPlayBookName() {
return playBookName;
}
public void setPlayBookName(String playBookName) {
this.playBookName = playBookName;
}
public Map<String, Object> getArguments() {
return arguments;
}
public void setArguments(Map<String, Object> arguments) {
this.arguments = arguments;
}
public int getAgentPort() {
return agentPort;
}
public void setAgentPort(int agentPort) {
this.agentPort = agentPort;
}
public boolean isFullDeploy() {
return fullDeploy;
}
public void setFullDeploy(boolean fullDeploy) {
this.fullDeploy = fullDeploy;
}
private void setupPublicKey() throws IOException {
File pubKeyFile = PathUtil.findFileOnClassPath(AnsibleConstant.RSA_PUBLIC_KEY);
String script = PathUtil.findFileOnClassPath(AnsibleConstant.IMPORT_PUBLIC_KEY_SCRIPT_PATH, true).getAbsolutePath();
if (localPublicKey) {
ShellUtils.run(String.format("sh %s %s", script, pubKeyFile.getAbsolutePath()));
} else {
setupPublicKeyOnRemote();
}
}
@Deferred
private void setupPublicKeyOnRemote() {
String script = ln(
"#!/bin/sh",
"if [ ! -d ~/.ssh ]; then",
"mkdir -p ~/.ssh",
"chmod 700 ~/.ssh",
"fi",
"if [ ! -f ~/.ssh/authorized_keys ]; then",
"touch ~/.ssh/authorized_keys",
"chmod 600 ~/.ssh/authorized_keys",
"fi",
"pub_key='{pubkey}'",
"grep \"$pub_key\" ~/.ssh/authorized_keys > /dev/null",
"if [ $? -eq 1 ]; then",
"echo \"$pub_key\" >> ~/.ssh/authorized_keys",
"fi",
"if [ -x /sbin/restorecon ]; then",
"/sbin/restorecon ~/.ssh ~/.ssh/authorized_keys",
"fi",
"exit 0"
).formatByMap(map(
e("pubkey", asf.getPublicKey())
));
SshShell ssh = new SshShell();
ssh.setHostname(targetIp);
ssh.setPassword(password);
ssh.setPort(sshPort);
ssh.setUsername(username);
if (privateKey != null) {
try {
final File tempKeyFile = File.createTempFile("zstack", "tmp");
FileUtils.writeStringToFile(tempKeyFile, privateKey);
Defer.defer(new Runnable() {
@Override
public void run() {
tempKeyFile.delete();
}
});
ssh.setPrivateKeyFile(tempKeyFile.getAbsolutePath());
} catch (IOException e) {
throw new CloudRuntimeException(e);
}
}
SshResult res = ssh.runScript(script);
res.raiseExceptionIfFailed();
}
private void callAnsible(final Completion completion) {
RunAnsibleMsg msg = new RunAnsibleMsg();
msg.setTargetIp(targetIp);
msg.setPrivateKeyFile(privKeyFile);
msg.setArguments(arguments);
msg.setAnsibleExecutable(ansibleExecutable);
if (playBookPath != null) {
msg.setPlayBookPath(playBookPath);
} else {
msg.setPlayBookPath(PathUtil.join(AnsibleConstant.ROOT_DIR, playBookName));
}
if (runOnLocal) {
bus.makeLocalServiceId(msg, AnsibleConstant.SERVICE_ID);
} else {
bus.makeTargetServiceIdByResourceUuid(msg, AnsibleConstant.SERVICE_ID, targetIp);
}
bus.send(msg, new CloudBusCallBack(completion) {
@Override
public void run(MessageReply reply) {
if (reply.isSuccess()) {
completion.success();
} else {
cleanup();
completion.fail(reply.getError());
}
}
});
}
private boolean runChecker() {
for (AnsibleChecker checker : checkers) {
if (checker.needDeploy()) {
logger.debug(String.format("checker[%s] reports deploy is needed", checker.getClass()));
return true;
}
}
return false;
}
private boolean isNeedRun() {
List<String> ignoreAgentPortModule = new ArrayList<String>();
ignoreAgentPortModule.add("imagestorebackupstorage.py");
if (isFullDeploy()) {
logger.debug("Ansible.fullDeploy is set, run ansible anyway");
return true;
}
if (ansibleNeedRun != null) {
return ansibleNeedRun.isRunNeed();
}
boolean changed = asf.isModuleChanged(playBookName);
if (changed) {
logger.debug(String.format("ansible module[%s] changed, run ansible", playBookName));
return true;
}
if (agentPort != 0) {
boolean opened = NetworkUtils.isRemotePortOpen(targetIp, agentPort, (int) TimeUnit.SECONDS.toMillis(5));
if (!opened) {
logger.debug(String.format("agent port[%s] on target ip[%s] is not opened, run ansible[%s]", agentPort, targetIp, playBookName));
return true;
}
if (runChecker()) {
return true;
}
logger.debug(String.format("agent port[%s] on target ip[%s] is opened, ansible module[%s] is not changed, skip to run ansible", agentPort, targetIp, playBookName));
return false;
} else if ( ignoreAgentPortModule.contains(playBookName) ) {
logger.debug(String.format("module %s will not check agent port, only check md5sum", playBookName));
if (runChecker()) {
logger.debug(String.format("module %s md5sum changed, run ansible", playBookName));
return true;
} else {
logger.debug(String.format("module %s md5sum not change, skip to run ansible", playBookName));
return false;
}
}
logger.debug("agent port is not set, run ansible anyway");
return true;
}
private void cleanup() {
// deleting source files. Then next time ansible is called, AnsibleChecker returns false that lets ansible run
for (AnsibleChecker checker : checkers) {
checker.deleteDestFile();
}
}
public void run(Completion completion) {
try {
if (!isNeedRun()) {
completion.success();
return;
}
putArgument("pip_url", String.format("http://%s:8080/zstack/static/pypi/simple", Platform.getManagementServerIp()));
putArgument("trusted_host", Platform.getManagementServerIp());
putArgument("yum_server", String.format("%s:8080", Platform.getManagementServerIp()));
putArgument("remote_user", username);
if (password != null && !password.isEmpty()) {
putArgument("remote_pass", password);
}
putArgument("remote_port", Integer.toString(sshPort));
logger.debug(String.format("starts to run ansible[%s]", playBookPath == null ? playBookName : playBookPath));
new PrepareAnsible().setTargetIp(targetIp).prepare();
setupPublicKey();
callAnsible(completion);
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
}
public List<AnsibleChecker> getCheckers() {
return checkers;
}
public void installChecker(AnsibleChecker checker) {
checkers.add(checker);
}
}