package io.fathom.auto.openstack.horizon; import io.fathom.auto.HostsFile; import io.fathom.auto.TimeSpan; import io.fathom.auto.openstack.horizon.config.LocalSettingsTemplate; import io.fathom.auto.processes.Pid; import io.fathom.auto.processes.ProcFs; import io.fathom.auto.processes.ProcessExecution; import io.fathom.auto.processes.Processes; import io.fathom.http.HttpClient; import io.fathom.http.HttpMethod; import io.fathom.http.HttpRequest; import io.fathom.http.HttpResponse; import io.fathom.http.jre.JreHttpClient; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.net.URI; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.io.Files; public class HorizonProcess { private static final Logger log = LoggerFactory.getLogger(HorizonProcess.class); private final Pid pid; final File instanceDir; private final HorizonConfig config; final File installDir; public HorizonProcess(HorizonConfig config, Pid pid) { this.instanceDir = config.getInstanceDir(); this.installDir = config.getInstallDir(); this.config = config; this.pid = pid; } static File getPidFile(File instanceDir) { return new File(instanceDir, "horizon.pid"); } static File getLogFile(File instanceDir) { return new File(instanceDir, "horizon.log"); } static File getConfigFile(File installDir) { return new File(installDir, "openstack_dashboard/local/local_settings.py"); } public static HorizonProcess start(HorizonConfig config) throws IOException { File instanceDir = config.getInstanceDir(); configureInstance(config); ProcessBuilder pb = buildLauncherProcess(config); ProcessExecution execution = Processes.run(pb, TimeSpan.minutes(1)); if (!execution.didExit()) { throw new IOException("Timeout while starting Process"); } else { if (execution.getExitCode() == 0) { log.info("Process started OK"); // TODO: Poll loop with timeout? TimeSpan.seconds(2).sleep(); File pidFile = getPidFile(instanceDir); Pid pid = Pid.read(pidFile); if (pid == null) { throw new IOException("Process started, but could not read pid from file"); } return new HorizonProcess(config, pid); } else { throw new IOException("Error starting Process process"); } } } private static void configureInstance(HorizonConfig config) throws IOException { File instanceDir = config.getInstanceDir(); File installDir = config.getInstallDir(); log.info("Installing new config file"); LocalSettingsTemplate template = new LocalSettingsTemplate(); File configFile = getConfigFile(installDir); template.write(configFile, config); HostsFile.setHosts(config.getHosts()); } private static ProcessBuilder buildLauncherProcess(HorizonConfig config) throws IOException { File instanceDir = config.getInstanceDir(); File logFile = getLogFile(instanceDir); File pidFile = getPidFile(instanceDir); File tmpDir = new File("/tmp"); // Be sure we don't end up as the process owner... File trampolineScript = new File(tmpDir, "run-horizon.sh"); { StringWriter stringWriter = new StringWriter(); try (PrintWriter s = new PrintWriter(stringWriter)) { s.println("#!/bin/bash"); s.println(""); List<String> args = Lists.newArrayList(); args.add("/opt/horizon/tools/with_venv.sh"); args.add("/opt/horizon/manage.py"); args.add("runserver"); args.add("[::]:8080"); args.add(">"); args.add(logFile.getAbsolutePath()); args.add("2>&1"); args.add("&"); s.println(Joiner.on(" ").join(args)); s.println("pid=$!"); s.println("echo ${pid} > " + pidFile.getAbsolutePath()); } Files.write(stringWriter.toString(), trampolineScript, Charsets.UTF_8); } List<String> args = Lists.newArrayList(); args.add("/bin/bash"); // Avoids need to chmod args.add(trampolineScript.getAbsolutePath()); ProcessBuilder pb = new ProcessBuilder(args); return pb; } public static HorizonProcess find(HorizonConfig config) throws IOException { File pidFile = getPidFile(config.getInstanceDir()); Pid pid = Pid.read(pidFile); if (pid == null) { return null; } HorizonProcess process = new HorizonProcess(config, pid); if (!process.isRunning()) { log.info("Found process in pid file, but was not our process: {}", pid); pidFile.delete(); process = null; } else { log.info("Found existing process: {}", pid); } return process; } public boolean isRunning() throws IOException { ProcFs.Process process = ProcFs.findProcess(pid); if (process == null) { return false; } List<String> cmdline = process.getCmdline(); if (!isOurProcess(cmdline)) { return false; } // Fetch the root page; this warms-up the app try { HttpClient httpClient = JreHttpClient.create(); URI uri = URI.create("http://127.0.0.1:8080/"); HttpRequest httpRequest = httpClient.buildRequest(HttpMethod.GET, uri); try (HttpResponse response = httpRequest.doRequest()) { if (response.getHttpResponseCode() != 200) { throw new IllegalStateException("Bad response code from page: " + response.getHttpResponseCode()); } } } catch (Exception e) { // TODO: Should we restart horizon if this failed? log.warn("Error while fetching warm-up page", e); } return true; } private boolean isOurProcess(List<String> cmdline) { if (cmdline == null) { // Process stopped return false; } if (cmdline.isEmpty()) { return false; } if (cmdline.get(2).equals("/opt/horizon/manage.py")) { return true; } return false; } }