Package eu.scape_project.planning.services.taverna.executor

Source Code of eu.scape_project.planning.services.taverna.executor.SSHTavernaExecutor

/*******************************************************************************
* 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();
        }
    }

}
TOP

Related Classes of eu.scape_project.planning.services.taverna.executor.SSHTavernaExecutor

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.