package io.urmia.jail.docker;
/**
*
* Copyright 2014 by Amin Abbaspour
*
* This file is part of Urmia.io
*
* Urmia.io is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Urmia.io is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Urmia.io. If not, see <http://www.gnu.org/licenses/>.
*/
import io.urmia.job.JobExecutor;
import io.urmia.job.JobExecutorFactory;
import io.urmia.md.model.job.JobDefinition;
import io.urmia.md.model.job.JobExec;
import io.urmia.md.model.job.JobInput;
import io.urmia.md.model.storage.ObjectName;
import io.urmia.util.UnixPathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.*;
import static io.urmia.job.JobExecutor.Status.*;
import static io.urmia.md.model.job.JobInput.END;
public class DockerCommandJobExecutor implements JobExecutor {
private static final Logger log = LoggerFactory.getLogger(DockerCommandJobExecutor.class);
private final static String[] envp = new String[]{
"PATH=/bin:/usr/bin:/usr/local/bin",
"DOCKER_HOST=tcp://192.168.59.103:2376",
"DOCKER_TLS_VERIFY=0"
};
private Status status = INIT;
private Process process = null;
private final String[] commands;
private final String hostId;
private final String directory;
private final String inputDirectory;
private final String dockerStorMountPoint = "/urmia";
private final JobDefinition.Phase.Type type;
private RandomAccessFile jobInput = null;
private final boolean boot2docker;
public DockerCommandJobExecutor(String hostId, JobDefinition jobDef, String jobId, String mountPoint,
JobDefinition.Phase.Type type) {
this.directory = UnixPathUtils.appendTrailingSlash(mountPoint) + jobDef.getOwner() + '/' + ObjectName.Namespace.JOBS.path + '/' + jobId;
this.inputDirectory = directory + "/input";
this.hostId = hostId;
this.type = type;
List<String> commands = new LinkedList<String>();
String dockerBin = "/usr/bin/docker";
if("Mac OS X".equalsIgnoreCase(System.getProperty("os.name"))) {
dockerBin = "/usr/local/bin/docker";
boot2docker = true;
} else {
boot2docker = false;
}
commands.add(dockerBin);
commands.add("--debug=true");
commands.add("run");
commands.add("-v");
commands.add(getAbsoluteScriptFolderPath() + ":/opt/urmia/bin:ro");
commands.add("-v");
commands.add(UnixPathUtils.trimSlash(mountPoint) + "/" + jobDef.getOwner() + ":/urmia/" + jobDef.getOwner());
//commands.add("urmia/debian");
commands.add("debian:7.6"); // <-- todo: read this from jobDef
commands.add("/opt/urmia/bin/job-runner.sh");
commands.add("-p");
commands.add(dockerStorMountPoint + "/" + jobDef.getOwner());
commands.add("-i");
commands.add(hostId);
commands.add("-j");
commands.add(jobId);
log.info("number of phases in job {} is: {}", jobId, jobDef.getPhases().size());
for (JobDefinition.Phase p : jobDef.getPhases()) {
log.info("number of exec in phase {} is: {}", p, p.exec.size());
for (JobExec exec : p.exec) {
commands.add("-c");
commands.add(exec.getCommand());
}
}
this.commands = commands.toArray(new String[commands.size()]);
boolean mkDirResult = new File(directory).mkdirs();
log.info("mkdirs to {} was: {}", directory, mkDirResult);
}
private static final String RUNNER_BASE = "src/main/bash";
private static final String MODULE_NAME = "jail-docker";
protected static String getAbsoluteScriptFolderPath() {
String pwd = System.getProperty("user.dir");
String scriptPath = pwd + File.separatorChar + RUNNER_BASE;
File f = new File(scriptPath);
if (f.exists() && f.isDirectory())
return f.getAbsolutePath();
String scriptPath2 = pwd + File.separatorChar + MODULE_NAME + File.separatorChar + RUNNER_BASE;
File f2 = new File(scriptPath2);
if (f2.exists() && f2.isDirectory())
return f2.getAbsolutePath();
String scriptPath21 = pwd + "/source/" + MODULE_NAME + File.separatorChar + RUNNER_BASE;
File f21 = new File(scriptPath21);
if (f21.exists() && f21.isDirectory())
return f21.getAbsolutePath();
String scriptPath3 = pwd + File.separatorChar + ".." + File.separatorChar + MODULE_NAME + File.separatorChar + RUNNER_BASE;
File f3 = new File(scriptPath3);
if (f3.exists() && f3.isDirectory())
return f3.getAbsolutePath();
String scriptPath4 = pwd + File.separatorChar + "../.." + File.separatorChar + MODULE_NAME + File.separatorChar + RUNNER_BASE;
File f4 = new File(scriptPath4);
if (f4.exists() && f4.isDirectory())
return f4.getAbsolutePath();
throw new RuntimeException("script folder not found in:\n" + scriptPath + "\n" + scriptPath2 + "\n" + scriptPath3 + "\n" + scriptPath4);
}
@Override
public void addInput(JobInput input) throws IOException {
if (jobInput == null)
throw new RuntimeException("stream not available yet");
if (status != SUCCESSFUL)
throw new IllegalArgumentException("job not in successful mode: " + status);
if (input.isEod()) {
log.info("closing input");
jobInput.close();
jobInput = null;
//File f = new File(inputDirectory);
//f.delete();
return;
}
for (String on : input) {
String line = dockerStorMountPoint + on;
log.info("flushing input: {}", line);
jobInput.writeBytes(line + "\n");
}
}
@Override
public void terminateInput() throws IOException {
addInput(END);
}
@Override
public String getOutputPath() {
return directory + "/" + hostId;
}
@Override
public int exitCode() throws InterruptedException {
log.info("waiting for process to exit" );
return process.waitFor();
}
@SuppressWarnings("UnusedDeclaration")
private Runnable pipe(final InputStream in) {
return new Runnable() {
public void run() {
Scanner s = new Scanner(in);
while (!Thread.interrupted() && s.hasNext())
log.error(s.next());
s.close();
}
};
}
@Override
public void run() {
log.info("running command: {}", Arrays.toString(commands));
ProcessBuilder pb = new ProcessBuilder(commands)
.directory(new File(directory))
.redirectErrorStream(true);
if(boot2docker) {
Map<String, String> env = pb.environment();
// todo: get these values from boot2docker shellinit output.
env.put("PATH", "/bin:/usr/bin:/usr/local/bin");
env.put("DOCKER_HOST", "tcp://192.168.59.103:2376");
env.put("DOCKER_CERT_PATH", "/Users/amin/.boot2docker/certs/boot2docker-vm");
env.put("DOCKER_TLS_VERIFY", "1");
//log.info("boot2docker running env: {}", env);
}
try {
//process = Runtime.getRuntime().exec(commands, envp, new File(directory));
process = pb.start();
} catch (IOException e) {
log.error("error running job.", e);
status = FAILED;
return;
}
new Thread(pipe(process.getErrorStream())).start();
new Thread(pipe(process.getInputStream())).start();
try {
Thread.sleep(100); // todo: why? give the process time to create input
// todo: don't do this. move jobInput stream creation to receive on first input.
// todo: check if enough time is past and file is there, then create it
} catch (InterruptedException ignored) {
}
try {
log.info("opening up jobInput to path: {}", inputDirectory);
File f = new File(inputDirectory);
if(!f.exists())
log.warn("input file does not exist: {}", inputDirectory);
else
log.warn("input file exist: {}", inputDirectory);
jobInput = new RandomAccessFile(inputDirectory, "rwd");
} catch (FileNotFoundException e) {
log.error("error opening input job.", e);
status = FAILED;
process.destroy();
return;
}
status = SUCCESSFUL;
log.info("opened up jobInput to path: {}", inputDirectory);
}
public static class Factory extends JobExecutorFactory {
@Override
public JobExecutor newInstance(String hostId, JobDefinition jobDef, String jobId, String mountPoint,
JobDefinition.Phase.Type type) {
return new DockerCommandJobExecutor(hostId, jobDef, jobId, mountPoint, type);
}
}
}