/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.component.sshremoteaccess; import java.io.File; import java.io.IOException; import java.io.InputStream; import org.apache.commons.io.IOUtils; import org.apache.commons.io.LineIterator; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; import de.rcenvironment.core.communication.sshconnection.SshConnectionService; import de.rcenvironment.core.communication.sshconnection.api.SshConnectionSetup; import de.rcenvironment.core.component.api.ComponentException; import de.rcenvironment.core.component.datamanagement.api.ComponentDataManagementService; import de.rcenvironment.core.component.execution.api.ComponentContext; import de.rcenvironment.core.component.execution.api.ComponentLog; import de.rcenvironment.core.component.model.spi.DefaultComponent; import de.rcenvironment.core.component.workflow.execution.api.WorkflowState; import de.rcenvironment.core.datamodel.api.DataType; import de.rcenvironment.core.datamodel.api.FinalWorkflowState; import de.rcenvironment.core.datamodel.api.TypedDatum; import de.rcenvironment.core.datamodel.types.api.DirectoryReferenceTD; import de.rcenvironment.core.datamodel.types.api.ShortTextTD; import de.rcenvironment.core.utils.common.StringUtils; import de.rcenvironment.core.utils.common.TempFileService; import de.rcenvironment.core.utils.common.TempFileServiceAccess; import de.rcenvironment.core.utils.ssh.jsch.JschFileTransfer; import de.rcenvironment.core.utils.ssh.jsch.executor.JSchRCECommandLineExecutor; /** * Component which runs a remote tool over a SSH connection. * * @author Brigitte Boden */ public class SshRemoteAccessClientComponent extends DefaultComponent { private static final String COLON = ":"; private static final Log LOG = LogFactory.getLog(SshRemoteAccessClientComponent.class); private static final String QUOT = "\""; private SshConnectionService sshService; private SshConnectionSetup connection; private TempFileService tempFileService; private ComponentDataManagementService datamanagementService; private ComponentContext componentContext; private String toolName; private String toolVersion; private String hostId; private String connectionId; private ComponentLog componentLog; private boolean isWorkflow; @Override public void setComponentContext(ComponentContext componentContext) { this.componentContext = componentContext; componentLog = componentContext.getLog(); } @Override public void start() throws ComponentException { tempFileService = TempFileServiceAccess.getInstance(); datamanagementService = componentContext.getService(ComponentDataManagementService.class); sshService = componentContext.getService(SshConnectionService.class); toolName = componentContext.getConfigurationValue(SshRemoteAccessConstants.KEY_TOOL_NAME); hostId = componentContext.getConfigurationValue(SshRemoteAccessConstants.KEY_HOST_ID); toolVersion = componentContext.getConfigurationValue(SshRemoteAccessConstants.KEY_TOOL_VERSION); connectionId = componentContext.getConfigurationValue(SshRemoteAccessConstants.KEY_CONNECTION); isWorkflow = Boolean.parseBoolean(componentContext.getConfigurationValue(SshRemoteAccessConstants.KEY_IS_WORKFLOW)); if (toolName == null || toolVersion == null || connectionId == null) { throw new ComponentException("Configuration for remote tool is not valid."); } connection = sshService.getConnectionSetup(connectionId); if (connection == null) { throw new ComponentException("The SSH connection for this tool does not exist."); } } @Override public void processInputs() throws ComponentException { // Create temp directory File tempRootDir; File inputDir; String inputShortText = null; try { tempRootDir = tempFileService.createManagedTempDir(); // Read the inputs (Currently, the inputs are fixed, a short text and a directory) inputDir = new File(tempRootDir, "input"); if (componentContext != null && componentContext.getInputsWithDatum() != null) { for (String inputName : componentContext.getInputsWithDatum()) { if (inputName.equals(SshRemoteAccessConstants.INPUT_NAME_DIRECTORY) && componentContext.getInputDataType(inputName).equals(DataType.DirectoryReference)) { datamanagementService.copyDirectoryReferenceTDToLocalDirectory(componentContext, (DirectoryReferenceTD) componentContext.readInput(inputName), inputDir); } else if (inputName.equals(SshRemoteAccessConstants.INPUT_NAME_SHORT_TEXT) && componentContext.getInputDataType(inputName).equals(DataType.ShortText)) { inputShortText = ((ShortTextTD) componentContext.readInput(inputName)).toString(); } else { // Should never happen as the component has only static inputs. LOG.warn("Invalid input " + inputName); } } } if (inputShortText == null) { throw new ComponentException("Short text input is missing"); } } catch (IOException e1) { throw new ComponentException("Temp directory for output could not be created.", e1); } // (Currently, the output of ssh tools is always a directory). File outputDir = new File(tempRootDir, "output"); // Get SSH session Session session; session = sshService.getAvtiveSshSession(connectionId); // Initialize scp context JSchRCECommandLineExecutor rceExecutor = new JSchRCECommandLineExecutor(session); String sessionToken; try { rceExecutor.start("ra init --compact"); try (InputStream stdoutStream = rceExecutor.getStdout(); InputStream stderrStream = rceExecutor.getStderr();) { rceExecutor.waitForTermination(); sessionToken = IOUtils.toString(stdoutStream).trim(); LOG.info("Received session token " + sessionToken); // Currently, nothing is written to stderr by the server side. Just in case, log error messages here. String errStream = IOUtils.toString(stderrStream); if (!errStream.isEmpty()) { LOG.error(errStream); } } } catch (IOException | InterruptedException e1) { throw new ComponentException("Executing SSH command failed", e1); } // Upload input directory try { JschFileTransfer.uploadDirectoryToRCEInstance(session, inputDir, StringUtils.format("/ra/%s/input", sessionToken)); } catch (IOException | JSchException | InterruptedException e2) { throw new ComponentException("Uploading input directory via SCP failed", e2); } // Format Strings for SSH command, set them in Quotes and replace inner quotes by double quotes. String formattedToolName = QUOT + toolName.replace(QUOT, QUOT + QUOT) + QUOT; String formattedVersion = QUOT + toolVersion.replace(QUOT, QUOT + QUOT) + QUOT; String command; if (isWorkflow) { command = StringUtils.format("ra run-wf %s --show-output %s %s %s", sessionToken, formattedToolName, formattedVersion, inputShortText, inputDir.getName(), outputDir.getName()); } else { command = StringUtils.format("ra run-tool %s --show-output -n %s %s %s %s", sessionToken, hostId, formattedToolName, formattedVersion, inputShortText, inputDir.getName(), outputDir.getName()); } // Parse final state of component String state = ""; // Run the tool try { rceExecutor.start(command); try (InputStream stdoutStream = rceExecutor.getStdout(); InputStream stderrStream = rceExecutor.getStderr();) { LineIterator it = IOUtils.lineIterator(stdoutStream, (String) null); while (it.hasNext()) { String line = it.nextLine(); state = parseLogLine(sessionToken, state, line); } rceExecutor.waitForTermination(); // Currently, nothing is written to stderr by the server side. Just in case, log error messages here. String errStream = IOUtils.toString(stderrStream); if (!errStream.isEmpty()) { LOG.error(errStream); } } } catch (IOException | InterruptedException e1) { throw new ComponentException("Executing SSH command failed", e1); } // Check if final state was "FINISHED" if (!state.equals(FinalWorkflowState.FINISHED.toString())) { throw new ComponentException("Remote component or workflow ended in state: " + state); } // Download output directory try { JschFileTransfer.downloadDirectory(session, StringUtils.format("/ra/%s/output", sessionToken), outputDir.getParentFile()); } catch (IOException | JSchException e1) { throw new ComponentException("Downloading outputput directory via SCP failed", e1); } // Write the component output TypedDatum output; try { output = datamanagementService.createDirectoryReferenceTDFromLocalDirectory(componentContext, outputDir, outputDir.getName()); componentContext.writeOutput(SshRemoteAccessConstants.OUTPUT_NAME, output); } catch (IOException e) { throw new ComponentException("Output directory reference could not be created. ", e); } finally { try { tempFileService.disposeManagedTempDirOrFile(tempRootDir); } catch (IOException e) { LOG.warn("Could not dispose managed temp dir " + tempRootDir); } } } private String parseLogLine(String sessionToken, String state, String line) { if (line.startsWith(StringUtils.format("[%s] StdOut: ", sessionToken))) { componentLog.toolStdout(line.substring(line.indexOf(COLON) + 2)); } else if (line.startsWith(StringUtils.format("[%s] State: ", sessionToken))) { // Parse state from line state = line.substring(line.indexOf(COLON) + 2); if (isWorkflow) { componentLog.toolStdout("Workflow state changed, new state: " + WorkflowState.valueOf(state).getDisplayName()); } else { componentLog.toolStdout("Tool state changed, new state: " + WorkflowState.valueOf(state).getDisplayName()); } } else if (line.startsWith(StringUtils.format("[%s] StdErr: ", sessionToken))) { componentLog.toolStderr(line.substring(line.indexOf(COLON) + 2)); } else { LOG.error(line); } return state; } }