package org.zstack.core.salt; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.DirectoryWalker; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.zstack.core.CoreGlobalProperty; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.MessageSafe; import org.zstack.header.AbstractService; import org.zstack.header.exception.CloudRuntimeException; import org.zstack.header.message.Message; import org.zstack.utils.DebugUtils; import org.zstack.utils.IptablesUtils; import org.zstack.utils.ShellUtils; import org.zstack.utils.Utils; import org.zstack.utils.data.StringTemplate; import org.zstack.utils.logging.CLogger; import org.zstack.utils.path.PathUtil; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.*; /** */ public class SaltFacadeImpl extends AbstractService implements SaltFacade { private static final CLogger logger = Utils.getLogger(SaltFacadeImpl.class); @Autowired private CloudBus bus; private int masterMaxOpenFiles = 1024; private int masterWorkerThreads = 10; private String masterFileRoots = "/srv/salt"; private String saltBootstrapScriptPath; private String saltMinionConfPath; private Map<String, Boolean> moduleChanges = new HashMap<String, Boolean>(); @Override @MessageSafe public void handleMessage(Message msg) { bus.dealWithUnknownMessage(msg); } @Override public String getId() { return bus.makeLocalServiceId(SaltConstant.SERVICE_ID); } private boolean deployFile(File src, File dst) throws IOException { if (dst.exists() && PathUtil.compareFileByMd5(src, dst)) { logger.debug(String.format("MD5 of src file[%s] and dst file[%s] are the same, no need to deploy", src.getAbsolutePath(), dst.getAbsolutePath())); return false; } FileUtils.copyFile(src, dst); logger.debug(String.format("deployed src file[%s] to dst file[%s]", src.getAbsolutePath(), dst.getAbsolutePath())); return true; } private File rewriteMasterConfFile() throws IOException { File masterConfTmpt = PathUtil.findFileOnClassPath(PathUtil.join("salt", SaltConstant.MASTER_CONF_NAME), true); Map<String, String> map = new HashMap<String, String>(); map.put("maxOpenFiles", String.valueOf(masterMaxOpenFiles)); map.put("workerThreads", String.valueOf(masterWorkerThreads)); String srcConf = FileUtils.readFileToString(masterConfTmpt); String conf = StringTemplate.substitute(srcConf, map); File masterConf = File.createTempFile("zstack-salt", "master"); FileUtils.write(masterConf, conf); return masterConf; } private void prepareSaltMaster() throws IOException { File srcMasterConf = rewriteMasterConfFile(); try { IptablesUtils.insertRuleToFilterTable("-A INPUT -p tcp -m state --state NEW -m tcp --dport 4505 -j ACCEPT"); IptablesUtils.insertRuleToFilterTable("-A INPUT -p tcp -m state --state NEW -m tcp --dport 4506 -j ACCEPT"); File dstMasterConf = new File(PathUtil.join(SaltConstant.SALT_CONF_HOME, SaltConstant.MASTER_CONF_NAME)); if (deployFile(srcMasterConf, dstMasterConf)) { ShellUtils.run("service salt-master restart"); } } finally { srcMasterConf.delete(); } } private void deployCommonStates() { if (CoreGlobalProperty.UNIT_TEST_ON) { return; } deployModule("salt/zstacklib"); } private void prepareJinjaVariables() throws IOException { Properties props = System.getProperties(); Map<String, String> envVars = new HashMap<String, String>(); Map<String, String> vars = new HashMap<String, String>(); for (String key : props.stringPropertyNames()) { String val = props.getProperty(key); if (key.startsWith("salt.env.")) { String[] tuples = key.split("\\."); if (tuples.length != 3) { throw new IllegalArgumentException(String.format("invalid salt environment variable[%s], it must be in form of 'salt.env.variableName'", key)); } String envName = tuples[2]; envVars.put(envName, val); } else if (key.startsWith("salt.var.")) { String[] tuples = key.split("\\."); if (tuples.length != 3) { throw new IllegalArgumentException(String.format("invalid salt variable[%s], it must be in form of 'salt.var.variableName'", key)); } String varName = tuples[2]; vars.put(varName, val); } } String varModulePath = PathUtil.join(masterFileRoots, "variables"); File varModule = new File(varModulePath); if (!varModule.exists()) { varModule.mkdirs(); } StringBuilder sb = new StringBuilder(); if (!envVars.isEmpty()) { List<String> dicts = new ArrayList<String>(); for (Map.Entry<String, String> e : envVars.entrySet()) { dicts.add(String.format("'%s':'%s'", e.getKey(), e.getValue())); } sb.append(String.format("{%% set cmd_env = {%s} %%}\n", StringUtils.join(dicts, ","))); } if (!vars.isEmpty()) { for (Map.Entry<String, String> e : vars.entrySet()) { sb.append(String.format("{%% set %s = '%s' %%}\n", e.getKey(), e.getValue())); } } File varFile = new File(PathUtil.join(varModulePath, "var.sls")); FileUtils.writeStringToFile(varFile, sb.toString()); } @Override public boolean start() { try { prepareSaltMaster(); prepareJinjaVariables(); } catch (IOException e) { throw new CloudRuntimeException(e); } saltBootstrapScriptPath = PathUtil.findFileOnClassPath("salt/salt-bootstrap.sh", true).getAbsolutePath(); saltMinionConfPath = PathUtil.findFileOnClassPath(String.format("salt/%s", SaltConstant.MINION_CONF_NAME), true).getAbsolutePath(); deployCommonStates(); return true; } @Override public boolean stop() { return true; } public void setMasterMaxOpenFiles(int masterMaxOpenFiles) { this.masterMaxOpenFiles = masterMaxOpenFiles; } public void setMasterWorkerThreads(int masterWorkerThreads) { this.masterWorkerThreads = masterWorkerThreads; } @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(this.masterFileRoots, moduleName); File dest = new File(destModulePath); if (!dest.exists()) { logger.debug(String.format("%s is not existing, need to deploy salt 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 salt 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 salt 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 salt module[%s]", name, destMd5, destModulePath, srcEntry.getValue(), modulePath, moduleName)); needed = true; return needed; } } logger.debug(String.format("no file changed in puppet module[%s], no need to deploy", moduleName)); needed = false; return needed; } finally { moduleChanges.put(moduleName, needed); } } @Override public void deployModule(String modulePath) { File src = PathUtil.findFolderOnClassPath(modulePath, true); if (!src.isDirectory()) { throw new CloudRuntimeException(String.format("Cannot find salt module[%s], it's either not existing or not a directory", modulePath)); } String moduleName = src.getName(); if (moduleName == null) { throw new CloudRuntimeException(String.format("Cannot get salt module name from path[%s]", modulePath)); } try { if (!isNeedToDeploy(moduleName, src.getAbsolutePath())) { return; } String destModulePath = PathUtil.join(masterFileRoots, moduleName); File dest = new File(destModulePath); if (dest.exists()) { FileUtils.forceDelete(dest); FileUtils.forceMkdir(dest); } FileUtils.copyDirectory(src, dest); } catch (Exception e) { String err = String.format("Unable to deploy salt module[%s] from %s", moduleName, modulePath); throw new CloudRuntimeException(err, e); } } @Override public SaltRunner createSaltRunner() { return new SaltRunner(saltBootstrapScriptPath, saltMinionConfPath); } @Override public boolean isModuleChanged(String moduleName) { Boolean ret = moduleChanges.get(moduleName); DebugUtils.Assert(ret != null, String.format("cannot find salt module name[%s]", moduleName)); return ret; } public void setMasterFileRoots(String masterFileRoots) { this.masterFileRoots = masterFileRoots; } }