package org.zstack.core.ansible; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.DirectoryWalker; import org.apache.commons.io.FileUtils; import org.ini4j.Wini; import org.springframework.beans.factory.annotation.Autowired; import org.zstack.core.CoreGlobalProperty; import org.zstack.core.Platform; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.MessageSafe; import org.zstack.core.errorcode.ErrorFacade; import org.zstack.core.thread.SyncTask; import org.zstack.core.thread.ThreadFacade; import org.zstack.header.AbstractService; import org.zstack.header.core.Completion; import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.errorcode.OperationFailureException; import org.zstack.header.exception.CloudRuntimeException; import org.zstack.header.message.Message; import org.zstack.utils.DebugUtils; import org.zstack.utils.ShellUtils; import org.zstack.utils.ShellUtils.ShellException; import org.zstack.utils.StringDSL; import org.zstack.utils.Utils; import org.zstack.utils.gson.JSONObjectUtil; import org.zstack.utils.logging.CLogger; import org.zstack.utils.path.PathUtil; import static org.zstack.core.Platform.operr; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.*; /** */ public class AnsibleFacadeImpl extends AbstractService implements AnsibleFacade { private static final CLogger logger = Utils.getLogger(AnsibleFacadeImpl.class); private int maxForks = 100; private String filesDir = PathUtil.join(AnsibleConstant.ROOT_DIR, "files"); private Map<String, Boolean> moduleChanges = new HashMap<String, Boolean>(); private Map<String, String> variables = new HashMap<String, String>(); @Autowired private CloudBus bus; @Autowired private ErrorFacade errf; @Autowired private ThreadFacade thdf; private String publicKey; private String privateKey; private void placePip703() { File pip = PathUtil.findFileOnClassPath("tools/pip-7.0.3.tar.gz"); if (pip == null) { throw new CloudRuntimeException(String.format("cannot find tools/pip-7.0.3.tar.gz on classpath")); } File root = new File(filesDir); if (!root.exists()) { root.mkdirs(); } ShellUtils.run(String.format("yes | cp %s %s", pip.getAbsolutePath(), filesDir)); } private void placeAnsible196() { File ansible = PathUtil.findFileOnClassPath("tools/ansible-1.9.6.tar.gz"); if (ansible == null) { throw new CloudRuntimeException(String.format("cannot find tools/ansible-1.9.6.tar.gz on classpath")); } File root = new File(filesDir); if (!root.exists()) { root.mkdirs(); } ShellUtils.run(String.format("yes | cp %s %s", ansible.getAbsolutePath(), filesDir)); } void init() { if (CoreGlobalProperty.UNIT_TEST_ON) { logger.debug("skip AnsibleFacade init as it's unittest"); return; } File privKeyFile = PathUtil.findFileOnClassPath(AnsibleConstant.RSA_PRIVATE_KEY, true); ShellUtils.run(String.format("chmod 600 %s", privKeyFile.getAbsolutePath())); File pubKeyFile = PathUtil.findFileOnClassPath(AnsibleConstant.RSA_PUBLIC_KEY); try { publicKey = FileUtils.readFileToString(pubKeyFile); publicKey = publicKey.trim(); publicKey = StringDSL.stripEnd(publicKey, "\n"); privateKey = FileUtils.readFileToString(privKeyFile); privateKey = privateKey.trim(); privateKey = StringDSL.stripEnd(privateKey, "\n"); File invFile = new File(AnsibleConstant.CONFIGURATION_FILE); File invDir = new File(invFile.getParent()); if (!invDir.exists()) { invDir.mkdirs(); } if (!invFile.exists()) { invFile.createNewFile(); } Wini ini = new Wini(invFile); Map<String, String> cfgs = Platform.getGlobalPropertiesStartWith("Ansible.cfg."); ini.put("defaults", "forks", maxForks); ini.put("defaults", "inventory", AnsibleConstant.INVENTORY_FILE); for (Map.Entry<String, String> e : cfgs.entrySet()) { String key = StringDSL.stripStart(e.getKey(), "Ansible.cfg."); if (!key.contains(".")) { ini.put("defaults", key, e.getValue()); } else { String[] pair = key.split("\\.", 2); ini.put(pair[0], pair[1], e.getValue()); } logger.debug(String.format("added ansible cfg[%s=%s] to %s", key, e.getValue(), AnsibleConstant.CONFIGURATION_FILE)); } ini.store(); Map<String, String> vars = Platform.getGlobalPropertiesStartWith("Ansible.var."); for (Map.Entry<String, String> e : vars.entrySet()) { String key = StringDSL.stripStart(e.getKey(), "Ansible.var."); variables.put(key, e.getValue()); logger.debug(String.format("discovered ansible variable[%s=%s]", key, e.getValue())); } placePip703(); placeAnsible196(); ShellUtils.run(String.format("if ! ansible --version | grep -q 1.9.6; then " + "if grep -i -s centos /etc/system-release; then " + "sudo yum remove -y ansible; " + "elif grep -i -s ubuntu /etc/issue; then " + "sudo apt-get --assume-yes remove ansible; " + "else echo \"Warning: can't remove ansible from unknown platform\"; " + "fi; " + "sudo pip install -i file://%s --trusted-host localhost -I ansible==1.9.6; " + "fi", AnsibleConstant.PYPI_REPO), false); deployModule("ansible/zstacklib", "zstacklib.py"); } catch (IOException e) { throw new CloudRuntimeException(e); } } @Override @MessageSafe public void handleMessage(Message msg) { if (msg instanceof RunAnsibleMsg) { handle((RunAnsibleMsg) msg); } else { bus.dealWithUnknownMessage(msg); } } private void handle(final RunAnsibleMsg msg) { thdf.syncSubmit(new SyncTask<Object>() { @Override public String getSyncSignature() { return String.format("run-anisble-for-host-%s", msg.getTargetIp()); } @Override public int getSyncLevel() { return 1; } @Override public String getName() { return getSyncSignature(); } private void run(Completion completion) { new PrepareAnsible().setTargetIp(msg.getTargetIp()).prepare(); logger.debug(String.format("start running ansible for playbook[%s]", msg.getPlayBookPath())); Map<String, Object> arguments = new HashMap<String, Object>(); if (msg.getArguments() != null) { arguments.putAll(msg.getArguments()); } arguments.put("host", msg.getTargetIp()); arguments.put("zstack_root", AnsibleGlobalProperty.ZSTACK_ROOT); arguments.put("pkg_zstacklib", AnsibleGlobalProperty.ZSTACKLIB_PACKAGE_NAME); arguments.putAll(getVariables()); String playBookPath = msg.getPlayBookPath(); if ( ! playBookPath.contains("py")) { arguments.put("ansible_ssh_user", arguments.get("remote_user")); arguments.put("ansible_ssh_port", arguments.get("remote_port")); arguments.put("ansible_ssh_pass", arguments.get("remote_pass")); arguments.remove("remote_user"); arguments.remove("remote_pass"); arguments.remove("remote_port"); if ( ! arguments.get("ansible_ssh_user").equals("root")) { arguments.put("ansible_become", "yes"); arguments.put("become_user", "root"); arguments.put("ansible_become_pass", arguments.get("ansible_ssh_pass")); } } String executable = msg.getAnsibleExecutable() == null ? AnsibleGlobalProperty.EXECUTABLE : msg.getAnsibleExecutable(); try { String output; if (AnsibleGlobalProperty.DEBUG_MODE2) { output = ShellUtils.run(String.format("PYTHONPATH=%s %s %s -i %s -vvvv --private-key %s -e '%s' | tee -a %s", AnsibleConstant.ZSTACKLIB_ROOT, executable, playBookPath, AnsibleConstant.INVENTORY_FILE, msg.getPrivateKeyFile(), JSONObjectUtil.toJsonString(arguments), AnsibleConstant.LOG_PATH), AnsibleConstant.ROOT_DIR); } else if (AnsibleGlobalProperty.DEBUG_MODE) { output = ShellUtils.run(String.format("PYTHONPATH=%s %s %s -i %s -vvvv --private-key %s -e '%s'", AnsibleConstant.ZSTACKLIB_ROOT, executable, playBookPath, AnsibleConstant.INVENTORY_FILE, msg.getPrivateKeyFile(), JSONObjectUtil.toJsonString(arguments)), AnsibleConstant.ROOT_DIR); } else { output = ShellUtils.run(String.format("PYTHONPATH=%s %s %s -i %s --private-key %s -e '%s'", AnsibleConstant.ZSTACKLIB_ROOT, executable, playBookPath, AnsibleConstant.INVENTORY_FILE, msg.getPrivateKeyFile(), JSONObjectUtil.toJsonString(arguments)), AnsibleConstant.ROOT_DIR); } if (output.contains("skipping: no hosts matched")) { throw new OperationFailureException(operr(output)); } } catch (ShellException se) { logger.warn(se.getMessage(), se); throw new OperationFailureException(operr(se.getMessage())); } completion.success(); } @Override public Object call() throws Exception { final RunAnsibleReply reply = new RunAnsibleReply(); run(new Completion(msg) { @Override public void success() { bus.reply(msg, reply); } @Override public void fail(ErrorCode errorCode) { reply.setError(errorCode); bus.reply(msg, reply); } }); return null; } }); } @Override public String getId() { return bus.makeLocalServiceId(AnsibleConstant.SERVICE_ID); } @Override public boolean start() { return true; } @Override public boolean stop() { return true; } @SuppressWarnings("rawtypes") public class ModuleWalker extends DirectoryWalker { @Override protected void handleFile(File file, int depth, Collection results) { results.add(file); } public void doWalk(File start, Collection result) throws IOException { walk(start, result); } } private boolean isNeedToDeploy(String moduleName, String modulePath) throws IOException { boolean needed = false; try { String destModulePath = PathUtil.join(filesDir, moduleName); File dest = new File(destModulePath); if (!dest.exists()) { logger.debug(String.format("%s is not existing, need to deploy ansible module[%s]", destModulePath, moduleName)); needed = true; return needed; } List<File> destFiles = new ArrayList<File>(20); ModuleWalker walker = new ModuleWalker(); walker.doWalk(dest, destFiles); List<File> srcFiles = new ArrayList<File>(20); walker = new ModuleWalker(); File src = new File(modulePath); walker.doWalk(src, srcFiles); if (srcFiles.size() != destFiles.size()) { logger.debug(String.format("%s has %s files, %s has %s files, need to deploy ansible module[%s]", destModulePath, destFiles.size(), modulePath, srcFiles.size(), moduleName)); needed = true; return needed; } Map<String, String> srcMd5sum = new HashMap<String, String>(srcFiles.size()); for (File f : srcFiles) { FileInputStream fis = new FileInputStream(f); String md5 = DigestUtils.md5Hex(fis); srcMd5sum.put(f.getName(), md5); } Map<String, String> destMd5sum = new HashMap<String, String>(destFiles.size()); for (File f : destFiles) { FileInputStream fis = new FileInputStream(f); String md5 = DigestUtils.md5Hex(fis); destMd5sum.put(f.getName(), md5); } for (Map.Entry<String, String> srcEntry : srcMd5sum.entrySet()) { String name = srcEntry.getKey(); String destMd5 = destMd5sum.get(name); if (destMd5 == null) { logger.debug(String.format("%s is not existing in %s, need to deploy ansible module[%s]", name, destModulePath, moduleName)); needed = true; return needed; } if (!destMd5.equals(srcEntry.getValue())) { logger.debug(String.format("%s's md5 changed[{%s} in %s, {%s} in %s], need to deploy ansible module[%s]", name, destMd5, destModulePath, srcEntry.getValue(), modulePath, moduleName)); needed = true; return needed; } } logger.debug(String.format("no file changed in ansible module[%s], no need to deploy", moduleName)); needed = false; return needed; } finally { moduleChanges.put(moduleName, needed); } } @Override public void deployModule(String modulePath, String playBookName) { File src = PathUtil.findFolderOnClassPath(modulePath, true); if (!src.isDirectory()) { throw new CloudRuntimeException(String.format("Cannot find ansible module[%s], it's either not existing or not a directory", modulePath)); } String moduleName = src.getName(); File root = new File(AnsibleConstant.ROOT_DIR); if (!root.exists()) { root.mkdirs(); } File filesRoot = new File(filesDir); if (!filesRoot.exists()) { filesRoot.mkdirs(); } try { if (!isNeedToDeploy(moduleName, src.getAbsolutePath())) { return; } String destModulePath = PathUtil.join(filesRoot.getAbsolutePath(), moduleName); File dest = new File(destModulePath); if (dest.exists()) { FileUtils.forceDelete(dest); FileUtils.forceMkdir(dest); } FileUtils.copyDirectory(src, dest); boolean isPlaybookLinked = false; for (File f : dest.listFiles()) { if (f.getName().equals(playBookName)) { String lnPath = PathUtil.join(AnsibleConstant.ROOT_DIR, playBookName); File lnFile = new File(lnPath); if (lnFile.exists()) { lnFile.delete(); } Files.createSymbolicLink(Paths.get(lnPath), Paths.get(f.getAbsolutePath())); isPlaybookLinked = true; } } // if playBookName=null, skip the deploy steps because the deploy is not independent if (playBookName != null && !isPlaybookLinked) { throw new IllegalArgumentException(String.format("cannot find playbook[%s] in module[%s], module files are%s", playBookName, modulePath, Arrays.asList(src.list()))); } logger.debug(String.format("successfully deployed ansible module[%s]", modulePath)); } catch (Exception e) { String err = String.format("Unable to deploy ansible module[%s] from %s", moduleName, modulePath); throw new CloudRuntimeException(err, e); } } @Override public boolean isModuleChanged(String playbookName) { String moduleName = StringDSL.stripEnd(playbookName, ".py"); Boolean ret = moduleChanges.get(moduleName); DebugUtils.Assert(ret != null, String.format("cannot find ansible module name[%s]", moduleName)); if (ret) { // we only need to deploy once moduleChanges.put(moduleName, false); } return ret; } @Override public Map<String, String> getVariables() { return variables; } @Override public String getPublicKey() { return publicKey; } @Override public String getPrivateKey() { return privateKey; } }