/******************************************************************************* * Copyright 2006 - 2014 Vienna University of Technology, * Department of Software Technology and Interactive Systems, IFS * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package eu.scape_project.planning.services.taverna.executor; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.TimeUnit; import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.common.IOUtils; import net.schmizz.sshj.connection.channel.direct.Session; import net.schmizz.sshj.connection.channel.direct.Session.Command; import net.schmizz.sshj.userauth.keyprovider.KeyProvider; import net.schmizz.sshj.xfer.FileSystemFile; import net.schmizz.sshj.xfer.InMemoryDestFile; import net.schmizz.sshj.xfer.InMemorySourceFile; import net.sf.taverna.t2.baclava.DataThing; import net.sf.taverna.t2.baclava.factory.DataThingFactory; import net.sf.taverna.t2.baclava.factory.DataThingXMLFactory; import org.apache.commons.configuration.Configuration; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.Namespace; import org.jdom.input.SAXBuilder; import org.jdom.output.Format; import org.jdom.output.XMLOutputter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import eu.scape_project.planning.utils.ConfigurationLoader; /** * Class to execute Taverna workflows on a remote server via SSH. */ public class SSHTavernaExecutor implements TavernaExecutor { private static final Logger LOG = LoggerFactory.getLogger(SSHTavernaExecutor.class); /** * Filename of input data document. */ private static final String INPUT_DOC_FILENAME = "input_data.xml"; /** * Filename of output data document. */ private static final String OUTPUT_DOC_FILENAME = "output_data.xml"; /** * Baclava XML namespace. */ private static final Namespace NAMESPACE = Namespace.getNamespace("b", "http://org.embl.ebi.escience/baclava/0.1alpha"); /** * SSH properties. */ private Configuration sshConfig; /** * Timeout for remote commands. */ private Integer commandTimeout; /** * Taverna command to call. */ private String tavernaCommand; /* * Executor parameters */ private Object workflow; private Map<String, Object> inputData = new HashMap<String, Object>(); private Set<String> outputPorts = new HashSet<String>(); private HashMap<String, ?> outputFiles = new HashMap<String, Object>(); private Map<String, Object> outputData = new HashMap<String, Object>();; private String outputDoc; /* * Cache of created directories on the server */ private HashSet<String> createdDirsCache = new HashSet<String>(); /* * Cache of temp files */ private HashMap<SSHTempFile, String> tempFilePaths = new HashMap<SSHTempFile, String>(); /* * Taverna call stuff */ private SSHClient ssh; private String workingDir; /* * Taverna command line arguments */ private String inputDocPath; private String outputDocPath; private String workflowPath; /** * Initializes the Executor. */ public void init() { ConfigurationLoader configurationLoader = new ConfigurationLoader(); sshConfig = configurationLoader.load(); commandTimeout = sshConfig.getInt("tavernaserver.ssh.command.timeout"); tavernaCommand = sshConfig.getString("tavernaserver.ssh.command"); clear(); } /* * (non-Javadoc) * * @see * eu.scape_project.planning.services.taverna.executor.TavernaExecutor#execute * () */ @Override public void execute() throws IOException, TavernaExecutorException { clear(); prepareClient(); try { if (sshConfig.getInteger("tavernaserver.ssh.port", null) != null) { ssh.connect(sshConfig.getString("tavernaserver.ssh.host"), sshConfig.getInt("tavernaserver.ssh.port")); } else { ssh.connect(sshConfig.getString("tavernaserver.ssh.host")); } if (sshConfig.getString("tavernaserver.ssh.privatekey.location") != null && !"".equals(sshConfig.getString("tavernaserver.ssh.privatekey.location"))) { KeyProvider kp = ssh.loadKeys(sshConfig.getString("tavernaserver.ssh.privatekey.location"), sshConfig.getString("tavernaserver.ssh.privatekey.password")); ssh.authPublickey(sshConfig.getString("tavernaserver.ssh.user"), kp); } else if (sshConfig.getString("tavernaserver.ssh.password") != null && !"".equals(sshConfig.getString("tavernaserver.ssh.password"))) { ssh.authPassword(sshConfig.getString("tavernaserver.ssh.user"), sshConfig.getString("tavernaserver.ssh.password")); } else { ssh.authPublickey(sshConfig.getString("tavernaserver.ssh.user")); } workingDir = createWorkingDir(); prepareServer(); executeWorkflow(); getResults(); if (sshConfig.getBoolean("tavernaserver.ssh.server.cleanup")) { cleanupServer(); } } finally { ssh.disconnect(); } } /** * Clears temporary data used for each execute. */ private void clear() { createdDirsCache.clear(); tempFilePaths.clear(); workingDir = null; ssh = null; inputDocPath = null; outputDocPath = null; workflowPath = null; outputData.clear(); } /** * Prepares the SSH client. * * @throws IOException * if an error occurred during setting up the client */ private void prepareClient() throws IOException { ssh = new SSHClient(); if (sshConfig.getString("tavernaserver.ssh.fingerprint") != null && !"".equals(sshConfig.getString("tavernaserver.ssh.fingerprint"))) { ssh.addHostKeyVerifier(sshConfig.getString("tavernaserver.ssh.fingerprint")); } ssh.useCompression(); } /** * Prepares the server for execution. * * @throws IOException * if the server communication failed * @throws TavernaExecutorException * if the server cannot be prepared */ private void prepareServer() throws IOException, TavernaExecutorException { outputDocPath = workingDir + File.separator + OUTPUT_DOC_FILENAME; inputDocPath = prepareInputs(); workflowPath = prepareWorkflow(); } /** * Prepares the inputs of the workflow run. * * @return the server path of the input document * @throws IOException * if the server communication failed * @throws TavernaExecutorException * if the inputs cannot be prepared */ private String prepareInputs() throws IOException, TavernaExecutorException { Element rootElement = new Element("dataThingMap", NAMESPACE); Document document = new Document(rootElement); for (Entry<String, Object> entry : inputData.entrySet()) { String portName = entry.getKey(); Object value = entry.getValue(); Object dereferencedInput = dereferenceInput(portName, value); DataThing thing = DataThingFactory.bake(dereferencedInput); Element dataThingElement = new Element("dataThing", NAMESPACE); dataThingElement.setAttribute("key", portName); dataThingElement.addContent(thing.getElement()); rootElement.addContent(dataThingElement); } XMLOutputter xo = new XMLOutputter(Format.getPrettyFormat()); // PrintWriter out = new PrintWriter(new FileWriter(inputFile)); ByteArrayOutputStream out = new ByteArrayOutputStream(); try { xo.output(document, out); return uploadFile(new ByteArraySourceFile(INPUT_DOC_FILENAME, out.toByteArray()), ""); } finally { out.close(); } } /** * Dereferences an input object of the provided port recursively. * * @param portName * the port name * @param value * input object * @return a dereferenced object * @throws IOException * if the file cannot be read * @throws TavernaExecutorException * if the file cannot be dereferenced */ private Object dereferenceInput(String portName, Object value) throws IOException, TavernaExecutorException { return dereferenceObject(portName, value); } /** * Dereferences an input object of the provided port recursively. * * @param prefix * the prefix for the dereferenced object * @param object * input object * @return a dereferenced object * @throws IOException * if the file cannot be read * @throws TavernaExecutorException * if the file cannot be dereferenced */ private Object dereferenceObject(String prefix, Object object) throws IOException, TavernaExecutorException { if (object instanceof Collection<?>) { ArrayList<Object> results = new ArrayList<Object>(((Collection<?>) object).size()); for (Object o : (Collection<?>) object) { results.add(dereferenceInput(prefix, o)); } return results; } else if (object instanceof File) { return uploadFile((File) object, prefix); } else if (object instanceof ByteArraySourceFile) { return uploadFile((ByteArraySourceFile) object, prefix); } else if (object instanceof SSHTempFile) { return registerTempPath((SSHTempFile) object, prefix); } else { return object; } } /** * Uploads a file to the provided target directory. * * @param file * the file to upload * @param targetDir * the target directory name * @return the path of the file on the server * @throws IOException * if the file cannot be read * @throws TavernaExecutorException * if the file cannot be uploaded */ private String uploadFile(File file, String targetDir) throws IOException, TavernaExecutorException { String targetPath; if (targetDir.equals("")) { targetPath = workingDir + File.separator + file.getName(); } else { targetPath = workingDir + File.separator + targetDir + File.separator + file.getName(); createDir(workingDir + File.separator + targetDir); } if (file.canRead()) { ssh.newSCPFileTransfer().upload(new FileSystemFile(file), targetPath); LOG.debug("Uploaded file " + file.getAbsolutePath() + " to " + targetPath); } else { LOG.error("Cannot load file " + file.getAbsolutePath() + " for upload"); throw new TavernaExecutorException("Cannot load file " + file.getAbsolutePath() + " for upload"); } return targetPath; } /** * Uploads an in-memory-source-file to the provided target directory. * * @param file * the file to upload * @param targetDir * the target directory name * @return the path of the file on the server * @throws IOException * if the server communication failed * @throws TavernaExecutorException * if the file cannot be uploaded */ private String uploadFile(InMemorySourceFile file, String targetDir) throws IOException, TavernaExecutorException { String targetPath; if (targetDir.equals("")) { targetPath = workingDir + File.separator + file.getName(); } else { targetPath = workingDir + File.separator + targetDir + File.separator + file.getName(); createDir(workingDir + File.separator + targetDir); } ssh.newSCPFileTransfer().upload(file, targetPath); LOG.debug("Uploaded file " + file.getName() + " to " + targetPath); return targetPath; } /** * Registers the temporary file in the provided target directory and returns * the server path to it. * * @param file * the file * @param targetDir * the target directory name * @return the path of the file on the server * @throws IOException * if the server communication failed * @throws TavernaExecutorException * if the path cannot be registered */ private String registerTempPath(SSHTempFile file, String targetDir) throws IOException, TavernaExecutorException { String targetPath; if (targetDir.equals("")) { targetPath = workingDir + File.separator + file.getName(); } else { targetPath = workingDir + File.separator + targetDir + File.separator + file.getName(); createDir(workingDir + File.separator + targetDir); } tempFilePaths.put(file, targetPath); LOG.debug("Added temporary file " + file.getName() + " to " + targetPath); return targetPath; } /** * Creates the working directory on the server. * * @return the directory * @throws IOException * if the server communication failed * @throws TavernaExecutorException * if the directory cannot be created */ private String createWorkingDir() throws IOException, TavernaExecutorException { final Session session = ssh.startSession(); try { final Command cmd = session.exec("mktemp -d -t plato.XXXXXXXXXXXXXXXXXXXX"); String tempDir = IOUtils.readFully(cmd.getInputStream()).toString(); cmd.join(commandTimeout, TimeUnit.SECONDS); if (cmd.getExitStatus().equals(0)) { tempDir = tempDir.trim(); LOG.debug("Created working directory " + tempDir); return tempDir; } else { String stderr = IOUtils.readFully(cmd.getErrorStream()).toString(); LOG.error("Error creating working directory " + stderr); throw new TavernaExecutorException("Error creating working directory " + stderr); } } finally { session.close(); } } /** * Creates a directory on the server if it does not already exist. * * @param dir * name of the directory to create * @throws IOException * if the server communication failed * @throws TavernaExecutorException * if the directory cannot be created */ private void createDir(String dir) throws IOException, TavernaExecutorException { if (!createdDirsCache.contains(dir)) { final Session session = ssh.startSession(); try { final Command cmd = session.exec("mkdir -p \"" + dir + "\""); cmd.join(commandTimeout, TimeUnit.SECONDS); if (cmd.getExitStatus().equals(0)) { LOG.debug("Created directory " + dir); createdDirsCache.add(dir); } else { String stderr = IOUtils.readFully(cmd.getErrorStream()).toString(); LOG.error("Error creating directory " + dir + ": " + stderr); throw new TavernaExecutorException("Error creating directory " + dir + ": " + stderr); } } finally { session.close(); } } } /** * Executes a prepared workflow. * * @throws IOException * if the server communication failed * @throws TavernaExecutorException * if the workflow cannot be executed */ private void executeWorkflow() throws IOException, TavernaExecutorException { final Session session = ssh.startSession(); try { String command = tavernaCommand.replace("%%inputdoc%%", inputDocPath) .replace("%%outputdoc%%", outputDocPath).replace("%%workflow%%", workflowPath) .replace("%%working_dir%%", workingDir); final Command cmd = session.exec(command); cmd.join(commandTimeout, TimeUnit.SECONDS); if (!cmd.getExitStatus().equals(0)) { String stderr = IOUtils.readFully(cmd.getErrorStream()).toString(); LOG.error("Error executing workflow: " + stderr); throw new TavernaExecutorException("Error executing workflow: " + stderr); } LOG.debug("Executed workflow with command " + command); } finally { session.close(); } } /** * Prepares a workflow for execution. * * @return the workflow identifier for execution * @throws IOException * if the workflow cannot be uploaded * @throws TavernaExecutorException * if the workflow was not specified */ private String prepareWorkflow() throws IOException, TavernaExecutorException { if (workflow == null) { throw new TavernaExecutorException("No workflow specified"); } return (String) dereferenceObject("", workflow); } /** * Reads the results of ports specified in outputPorts or of all ports if no * output ports is null. * * @throws IOException * if the results cannot be retrieved * @throws TavernaExecutorException * if the results cannot be read */ private void getResults() throws IOException, TavernaExecutorException { // Download data File outputDocFile = File.createTempFile("ssh-taverna-executor-", ".xml"); try { downloadFile(OUTPUT_DOC_FILENAME, outputDocFile); SAXBuilder builder = new SAXBuilder(); FileInputStream is = new FileInputStream(outputDocFile); try { Document outputDocument = builder.build(is); XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat()); outputDoc = outputter.outputString(outputDocument); Map<String, DataThing> outputDataThings = DataThingXMLFactory.parseDataDocument(outputDocument); if (outputPorts == null) { for (Entry<String, DataThing> outputDataThing : outputDataThings.entrySet()) { outputData.put(outputDataThing.getKey(), outputDataThing.getValue().getDataObject()); } } else { for (String portName : outputPorts) { DataThing outputDataThing = outputDataThings.get(portName); if (outputDataThing == null) { outputData.put(portName, null); } else { outputData.put(portName, outputDataThing.getDataObject()); } } } } catch (JDOMException e) { throw new TavernaExecutorException("Error reading output document", e); } finally { is.close(); } } finally { outputDocFile.delete(); } // Download files for (Entry<String, ?> entry : outputFiles.entrySet()) { getResultFiles(entry.getKey(), entry.getValue()); } } /** * Reads the results files of the provided port. * * @param portName * the port name * @param value * a file or nested collection of files * @return a file or nested collection of files * @throws IOException * if the file cannot be downloaded * @throws TavernaExecutorException * if the file cannot be processed */ private Object getResultFiles(String portName, Object value) throws IOException, TavernaExecutorException { if (value instanceof Collection<?>) { ArrayList<Object> results = new ArrayList<Object>(((Collection<?>) value).size()); for (Object object : (Collection<?>) value) { results.add(dereferenceInput(portName, object)); } return results; } else if (value instanceof File) { String path = portName + File.separator + ((File) value).getName(); downloadFile(path, (File) value); return value; } else if (value instanceof SSHInMemoryTempFile) { // Check either registered tmp file or try path from output port String path = tempFilePaths.get(value); if (path == null) { path = (String) outputData.get(portName); } downloadFile(path, (SSHInMemoryTempFile) value); return value; } else { return value; } } /** * Downloads a path to a local file. * * @param path * the server path * @param localFile * the local file * @throws IOException * if the file could not be downloaded */ private void downloadFile(String path, File localFile) throws IOException { String sourcePath = workingDir + File.separator + path; ssh.newSCPFileTransfer().download(sourcePath, new FileSystemFile(localFile)); LOG.debug("Downloaded file " + path + " to " + localFile.getPath()); } /** * Downloads a registered tmp file. * * @param path * path to the file to download * @param tempFile * the tmp file * @throws IOException * if the file could not be downloaded * @throws TavernaExecutorException * if the file is not registered */ private void downloadFile(String path, SSHInMemoryTempFile tempFile) throws IOException, TavernaExecutorException { ByteArrayDestFile destFile = new ByteArrayDestFile(); ssh.newSCPFileTransfer().download(path, destFile); tempFile.setData(destFile.getData()); LOG.debug("Downloaded file " + path + " to " + tempFile.getName()); } /** * Cleans up created resources on the server. * * @throws IOException * if a communication error occurred * @throws TavernaExecutorException * if the cleanup was not successful */ private void cleanupServer() throws IOException, TavernaExecutorException { final Session session = ssh.startSession(); try { final Command cmd = session.exec("rm -rf " + workingDir); cmd.join(commandTimeout, TimeUnit.SECONDS); if (!cmd.getExitStatus().equals(0)) { String stderr = IOUtils.readFully(cmd.getErrorStream()).toString(); LOG.error("Error deleting working directory " + stderr); throw new TavernaExecutorException("Error deleting working directory " + stderr); } LOG.debug("Deleted working directory " + workingDir); } finally { session.close(); } } // --------------- getter/setter --------------- public Object getWorkflow() { return workflow; } public void setWorkflow(Object workflow) { this.workflow = workflow; } public Map<String, Object> getInputData() { return inputData; } public void setInputData(Map<String, Object> inputData) { this.inputData = inputData; } /* * (non-Javadoc) * * @see eu.scape_project.planning.services.taverna.executor.TavernaExecutor# * getOutputData () */ @Override public Map<String, ?> getOutputData() { return outputData; } public void setOutputData(Map<String, Object> outputData) { this.outputData = outputData; } /* * (non-Javadoc) * * @see eu.scape_project.planning.services.taverna.executor.TavernaExecutor# * getOutputFiles () */ @Override public HashMap<String, ?> getOutputFiles() { return outputFiles; } public void setOutputFiles(HashMap<String, ?> outputFiles) { this.outputFiles = outputFiles; } public Set<String> getOutputPorts() { return outputPorts; } public void setOutputPorts(Set<String> outputPorts) { this.outputPorts = outputPorts; } public String getOutputDoc() { return outputDoc; } public void setOutputDoc(String outputDoc) { this.outputDoc = outputDoc; } /** * Implementation of in-memory-source-file that reads the data from a byte * array. */ public class ByteArraySourceFile extends InMemorySourceFile { private byte[] data; private String name; /** * Creates a new byte array source file. * * @param name * name of the file * @param data * data */ public ByteArraySourceFile(String name, byte[] data) { this.name = name; this.data = data; } @Override public String getName() { return name; } @Override public long getLength() { return data.length; } @Override public InputStream getInputStream() throws IOException { return new ByteArrayInputStream(data); } } /** * Implementation of in-memory-destination-file that writes to a byte array. */ public class ByteArrayDestFile extends InMemoryDestFile { private ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); @Override public OutputStream getOutputStream() throws IOException { return outputStream; } public byte[] getData() { return outputStream.toByteArray(); } } }