/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.component.sshremoteaccess.internal; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.StringReader; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVRecord; import org.apache.commons.csv.QuoteMode; import org.apache.commons.io.IOUtils; import org.apache.commons.io.LineIterator; import com.jcraft.jsch.Session; import de.rcenvironment.core.communication.api.PlatformService; import de.rcenvironment.core.communication.common.IdentifierException; import de.rcenvironment.core.communication.common.LogicalNodeId; import de.rcenvironment.core.communication.common.NodeIdentifierUtils; import de.rcenvironment.core.communication.sshconnection.SshConnectionService; import de.rcenvironment.core.component.api.ComponentConstants; import de.rcenvironment.core.component.model.api.ComponentInstallation; import de.rcenvironment.core.component.model.api.ComponentInstallationBuilder; import de.rcenvironment.core.component.model.api.ComponentInterface; import de.rcenvironment.core.component.model.api.ComponentInterfaceBuilder; import de.rcenvironment.core.component.model.api.ComponentRevisionBuilder; import de.rcenvironment.core.component.model.configuration.api.ComponentConfigurationModelFactory; import de.rcenvironment.core.component.model.configuration.api.ConfigurationDefinition; import de.rcenvironment.core.component.model.configuration.api.ConfigurationExtensionDefinition; import de.rcenvironment.core.component.model.endpoint.api.ComponentEndpointModelFactory; import de.rcenvironment.core.component.model.endpoint.api.EndpointDefinition; import de.rcenvironment.core.component.model.endpoint.api.EndpointDefinitionsProvider; import de.rcenvironment.core.component.registration.api.ComponentRegistry; import de.rcenvironment.core.component.sshremoteaccess.SshRemoteAccessConstants; import de.rcenvironment.core.component.sshremoteaccess.SshRemoteAccessClientService; import de.rcenvironment.core.datamodel.api.DataType; import de.rcenvironment.core.datamodel.api.EndpointType; import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils; import de.rcenvironment.core.utils.common.ServiceUtils; import de.rcenvironment.core.utils.common.StringUtils; import de.rcenvironment.core.utils.ssh.jsch.executor.JSchRCECommandLineExecutor; import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription; /** * Default implementation of {@link SshRemoteAccessClientService}. * * @author Brigitte Boden */ public class SshRemoteAccessClientServiceImpl implements SshRemoteAccessClientService { private static final int SIZE_32 = 32; private static final int SIZE_16 = 16; private static final Log LOG = LogFactory.getLog(SshRemoteAccessClientService.class); private static final int UPDATE_TOOLS_INTERVAL_SECS = 10; private static ComponentRegistry registry; private PlatformService platformService; private SshConnectionService sshService; private Map<String, Map<String, ComponentInstallation>> registeredComponentsPerConnection; private ScheduledFuture<?> taskFuture; private volatile boolean started; private Map<String, LogicalNodeId> logicalNodeMap; public SshRemoteAccessClientServiceImpl() { registeredComponentsPerConnection = new HashMap<String, Map<String, ComponentInstallation>>(); logicalNodeMap = new HashMap<String, LogicalNodeId>(); } @Override public void updateSshRemoteAccessComponents(String connectionId) { Map<String, ComponentInstallation> registeredComponents = registeredComponentsPerConnection.get(connectionId); if (registeredComponents == null) { registeredComponents = new HashMap<String, ComponentInstallation>(); registeredComponentsPerConnection.put(connectionId, registeredComponents); } Session session = null; session = sshService.getAvtiveSshSession(connectionId); if (session != null) { JSchRCECommandLineExecutor executor = new JSchRCECommandLineExecutor(session); List<String> componentIdsReceived = new ArrayList<String>(); // Get remote tools String command = StringUtils.format("ra list-tools"); String toolDescriptionsString = ""; try { executor.start(command); try (InputStream stdoutStream = executor.getStdout(); InputStream stderrStream = executor.getStderr();) { executor.waitForTermination(); toolDescriptionsString = IOUtils.toString(stdoutStream); } } catch (IOException | InterruptedException e1) { LOG.error("Executing SSH command (ra list-tools) failed", e1); } // Parse output string and register/unregister components final CSVFormat csvFormat = CSVFormat.newFormat(' ').withQuote('"').withQuoteMode(QuoteMode.ALL); CSVParser parser = null; try (final Reader toolDescriptionReader = new StringReader(toolDescriptionsString);) { parser = csvFormat.parse(toolDescriptionReader); for (CSVRecord record : parser.getRecords()) { String toolName; String toolVersion; String hostName; String hostId; toolName = record.get(0); toolVersion = record.get(1); hostId = record.get(2); hostName = record.get(3); // TODO Review, for now just use the toolName as ID String toolId = toolName; // Id containing tool id and host id; used as unique key for hashmap because the same tool can be available on different // remote nodes String toolAndHostId = createUniqueToolAndHostId(toolId, hostId, connectionId); // If this component was not registered before, register it now. if (!registeredComponents.containsKey(toolAndHostId)) { LOG.info(StringUtils.format("Detected new SSH tool %s (version %s) on host %s.", toolName, toolVersion, hostName)); registerToolAccessComponent(toolId, toolName, toolVersion, hostName, hostId, connectionId, false); } else { // If this is a new version of a component, replace the old installation by the new one. if (!registeredComponents.get(toolAndHostId).getComponentRevision().getComponentInterface().getVersion() .equals(toolVersion)) { removeToolAccessComponent(toolAndHostId, connectionId); registerToolAccessComponent(toolId, toolName, toolVersion, hostName, hostId, connectionId, false); LOG.info(StringUtils.format("SSH tool %s changed to version %s on host %s.", toolName, toolVersion, hostName)); } } componentIdsReceived.add(toolAndHostId); } } catch (IOException e) { LOG.error("Could not parse tool descriptions" + e.toString()); } // Get remote workflows command = StringUtils.format("ra list-wfs"); try { executor.start(command); try (InputStream stdoutStream = executor.getStdout(); InputStream stderrStream = executor.getStderr();) { LineIterator it = IOUtils.lineIterator(stdoutStream, (String) null); Integer numberOfWorkflows = null; Integer tokensPerWorkflow = null; if (it.hasNext()) { numberOfWorkflows = Integer.parseInt(it.nextLine()); } if (it.hasNext()) { tokensPerWorkflow = Integer.parseInt(it.nextLine()); } if (numberOfWorkflows != null && tokensPerWorkflow != null) { if (tokensPerWorkflow != 4) { LOG.error("Unkown format of workflow descriptions"); } else { for (int i = 0; i < numberOfWorkflows; i++) { String wfName = it.nextLine(); String wfVersion = it.nextLine(); // Not used yet String hostId = it.nextLine(); String hostName = it.nextLine(); String componentId = wfName + "_wf_" + hostId; String toolAndHostId = createUniqueToolAndHostId(componentId, hostId, connectionId); if (!registeredComponents.containsKey(toolAndHostId)) { LOG.info(StringUtils.format("Detected new remote workflow %s (version %s) on host %s.", wfName, wfVersion, hostName)); registerToolAccessComponent(componentId, wfName, wfVersion, hostName, hostId, connectionId, true); } componentIdsReceived.add(toolAndHostId); } } } executor.waitForTermination(); } } catch (IOException | InterruptedException e1) { LOG.error("Executing SSH command (ra list-wfs) failed", e1); } // Check if there are "old" components from this connection that are not available any more. for (Iterator<String> it = registeredComponents.keySet().iterator(); it.hasNext();) { String regCompName = it.next(); if (!componentIdsReceived.contains(regCompName)) { removeToolAccessComponent(regCompName, connectionId); it.remove(); } } } } private String createUniqueToolAndHostId(String toolId, String hostId, String connectionId) { return toolId + "/" + connectionId + "/" + hostId; } protected void registerToolAccessComponent(String componentId, String toolName, String toolVersion, String hostName, String hostId, String connectionId, boolean isWorkflow) { EndpointDefinitionsProvider inputProvider; EndpointDefinitionsProvider outputProvider; ConfigurationDefinition configuration; Set<EndpointDefinition> inputs = createInputs(); inputProvider = ComponentEndpointModelFactory.createEndpointDefinitionsProvider(inputs); Set<EndpointDefinition> outputs = createOutputs(); outputProvider = ComponentEndpointModelFactory.createEndpointDefinitionsProvider(outputs); configuration = generateConfiguration(toolName, toolVersion, hostName, hostId, connectionId, isWorkflow); ComponentInterface componentInterface; if (isWorkflow) { componentInterface = new ComponentInterfaceBuilder() .setIdentifier(SshRemoteAccessConstants.COMPONENT_ID + "." + componentId) // Add "WORKFLOW" to display name .setDisplayName(StringUtils.format("%s (%s) [workflow on %s]", toolName, toolVersion, hostName)) .setIcon16(readDefaultToolIcon(SIZE_16)) .setIcon32(readDefaultToolIcon(SIZE_32)) .setGroupName(SshRemoteAccessConstants.GROUP_NAME_WFS) .setVersion(toolVersion) .setInputDefinitionsProvider(inputProvider).setOutputDefinitionsProvider(outputProvider) .setConfigurationDefinition(configuration) .setConfigurationExtensionDefinitions(new HashSet<ConfigurationExtensionDefinition>()) .setColor(ComponentConstants.COMPONENT_COLOR_STANDARD) .setShape(ComponentConstants.COMPONENT_SHAPE_STANDARD) .setSize(ComponentConstants.COMPONENT_SIZE_STANDARD) .build(); } else { componentInterface = new ComponentInterfaceBuilder() .setIdentifier(SshRemoteAccessConstants.COMPONENT_ID + "." + componentId) .setDisplayName(StringUtils.format("%s [SSH forwarded]", toolName)) .setIcon16(readDefaultToolIcon(SIZE_16)) .setIcon32(readDefaultToolIcon(SIZE_32)) .setGroupName(SshRemoteAccessConstants.GROUP_NAME_TOOLS) .setVersion(toolVersion) .setInputDefinitionsProvider(inputProvider).setOutputDefinitionsProvider(outputProvider) .setConfigurationDefinition(configuration) .setConfigurationExtensionDefinitions(new HashSet<ConfigurationExtensionDefinition>()) .setColor(ComponentConstants.COMPONENT_COLOR_STANDARD) .setShape(ComponentConstants.COMPONENT_SHAPE_STANDARD) .setSize(ComponentConstants.COMPONENT_SIZE_STANDARD) .build(); } ComponentInstallation ci = new ComponentInstallationBuilder() .setComponentRevision( new ComponentRevisionBuilder() .setComponentInterface(componentInterface) .setClassName("de.rcenvironment.core.component.sshremoteaccess.SshRemoteAccessClientComponent").build()) .setNodeId(getLocalLogicalNodeIdForRemoteNode(hostId)) .setInstallationId(createUniqueToolAndHostId(componentInterface.getIdentifier(), hostId, connectionId)) .setIsPublished(true) .build(); registry.addComponent(ci); registeredComponentsPerConnection.get(connectionId).put(createUniqueToolAndHostId(componentId, hostId, connectionId), ci); } private LogicalNodeId getLocalLogicalNodeIdForRemoteNode(String remoteNodeId) { if (logicalNodeMap.containsKey(remoteNodeId)) { return logicalNodeMap.get(remoteNodeId); } try { LogicalNodeId remoteId = NodeIdentifierUtils.parseLogicalNodeIdString(remoteNodeId); String recPart = remoteId.getInstanceNodeIdString(); // If there is no local node yet representing the given remote node, create a new one LogicalNodeId logicalNode = platformService.createRecognizableLocalLogicalNodeId(recPart); logicalNodeMap.put(remoteNodeId, logicalNode); return logicalNode; } catch (IdentifierException e) { return platformService.getLocalDefaultLogicalNodeId(); } } protected void removeToolAccessComponent(String toolAndHostId, String connectionId) { ComponentInstallation ci = registeredComponentsPerConnection.get(connectionId).get(toolAndHostId); if (ci != null) { registry.removeComponent(ci.getInstallationId()); } } private ConfigurationDefinition generateConfiguration(String toolName, String toolVersion, String hostName, String hostId, String connectionId, boolean isWorkflow) { List<Object> configuration = new LinkedList<Object>(); Map<String, String> readOnlyConfiguration = new HashMap<String, String>(); readOnlyConfiguration.put(SshRemoteAccessConstants.KEY_TOOL_NAME, toolName); readOnlyConfiguration.put(SshRemoteAccessConstants.KEY_TOOL_VERSION, toolVersion); readOnlyConfiguration.put(SshRemoteAccessConstants.KEY_CONNECTION, connectionId); readOnlyConfiguration.put(SshRemoteAccessConstants.KEY_HOST_ID, hostId); readOnlyConfiguration.put(SshRemoteAccessConstants.KEY_HOST_NAME, hostName); readOnlyConfiguration.put(SshRemoteAccessConstants.KEY_IS_WORKFLOW, Boolean.toString(isWorkflow)); return ComponentConfigurationModelFactory.createConfigurationDefinition(configuration, new LinkedList<Object>(), new LinkedList<Object>(), readOnlyConfiguration); } private Set<EndpointDefinition> createOutputs() { Set<EndpointDefinition> outputs = new HashSet<EndpointDefinition>(); Map<String, Object> description = new HashMap<String, Object>(); description.put(SshRemoteAccessConstants.KEY_ENDPOINT_NAME, SshRemoteAccessConstants.OUTPUT_NAME); description.put(SshRemoteAccessConstants.KEY_ENDPOINT_DATA_TYPE, DataType.DirectoryReference.name()); List<String> dataTypes = new LinkedList<String>(); dataTypes.add(DataType.DirectoryReference.name()); description.put(SshRemoteAccessConstants.KEY_ENDPOINT_DATA_TYPES, dataTypes); outputs.add(ComponentEndpointModelFactory.createEndpointDefinition(description, EndpointType.OUTPUT)); return outputs; } private Set<EndpointDefinition> createInputs() { Set<EndpointDefinition> inputs = new HashSet<EndpointDefinition>(); List<String> inputHandlings = new ArrayList<String>(); inputHandlings.add(EndpointDefinition.InputDatumHandling.Constant.name()); inputHandlings.add(EndpointDefinition.InputDatumHandling.Single.name()); inputHandlings.add(EndpointDefinition.InputDatumHandling.Queue.name()); List<String> inputExecutionConstraints = new ArrayList<String>(); inputExecutionConstraints.add(EndpointDefinition.InputExecutionContraint.NotRequired.name()); inputExecutionConstraints.add(EndpointDefinition.InputExecutionContraint.Required.name()); inputExecutionConstraints.add(EndpointDefinition.InputExecutionContraint.RequiredIfConnected.name()); // Short Text input Map<String, Object> description = new HashMap<String, Object>(); description.put(SshRemoteAccessConstants.KEY_ENDPOINT_NAME, SshRemoteAccessConstants.INPUT_NAME_SHORT_TEXT); description.put(SshRemoteAccessConstants.KEY_ENDPOINT_DATA_TYPE, DataType.ShortText.name()); List<String> dataTypes = new LinkedList<String>(); dataTypes.add(DataType.ShortText.name()); description.put(SshRemoteAccessConstants.KEY_ENDPOINT_DATA_TYPES, dataTypes); description.put(SshRemoteAccessConstants.KEY_DEFAULT_INPUT_EXEC_CONSTRAINT, EndpointDefinition.InputExecutionContraint.Required.name()); description.put(SshRemoteAccessConstants.KEY_INPUT_EXEC_CONSTRAINTS, inputExecutionConstraints); description.put(SshRemoteAccessConstants.KEY_DEFAULT_INPUT_HANDLING, EndpointDefinition.InputDatumHandling.Queue.name()); description.put(SshRemoteAccessConstants.KEY_INPUT_HANDLINGS, inputHandlings); inputs.add(ComponentEndpointModelFactory.createEndpointDefinition(description, EndpointType.INPUT)); // Directory input Map<String, Object> description2 = new HashMap<String, Object>(); description2.put(SshRemoteAccessConstants.KEY_ENDPOINT_NAME, SshRemoteAccessConstants.INPUT_NAME_DIRECTORY); description2.put(SshRemoteAccessConstants.KEY_ENDPOINT_DATA_TYPE, DataType.DirectoryReference.name()); List<String> dataTypes2 = new LinkedList<String>(); dataTypes2.add(DataType.DirectoryReference.name()); description2.put(SshRemoteAccessConstants.KEY_ENDPOINT_DATA_TYPES, dataTypes2); description2.put(SshRemoteAccessConstants.KEY_DEFAULT_INPUT_EXEC_CONSTRAINT, EndpointDefinition.InputExecutionContraint.Required.name()); description2.put(SshRemoteAccessConstants.KEY_INPUT_EXEC_CONSTRAINTS, inputExecutionConstraints); description2.put(SshRemoteAccessConstants.KEY_DEFAULT_INPUT_HANDLING, EndpointDefinition.InputDatumHandling.Queue.name()); description2.put(SshRemoteAccessConstants.KEY_INPUT_HANDLINGS, inputHandlings); inputs.add(ComponentEndpointModelFactory.createEndpointDefinition(description2, EndpointType.INPUT)); return inputs; } protected void bindComponentRegistry(ComponentRegistry newRegistry) { registry = newRegistry; } protected void unbindComponentRegistry(ComponentRegistry newRegistry) { registry = ServiceUtils.createFailingServiceProxy(ComponentRegistry.class); } protected void bindPlatformService(PlatformService newService) { platformService = newService; } protected void unbindPlatformService(PlatformService newService) { platformService = ServiceUtils.createFailingServiceProxy(PlatformService.class); } protected void bindSSHConnectionService(SshConnectionService newService) { sshService = newService; } // For unit tests public SshConnectionService getSshService() { return sshService; } @Override public void updateSshRemoteAccessComponents() { Collection<String> activeConnectionIds = sshService.getAllActiveSshConnectionSetupIds(); // for each connection, update the accessible tools for (String id : activeConnectionIds) { updateSshRemoteAccessComponents(id); } // Check for connections that are stored, but not active anymore. Set<String> outdatedConnectionIds = new HashSet<String>(registeredComponentsPerConnection.keySet()); outdatedConnectionIds.removeAll(activeConnectionIds); // Remove all tools from outdated connections. for (String id : outdatedConnectionIds) { Map<String, ComponentInstallation> registeredComponents = registeredComponentsPerConnection.get(id); if (registeredComponents != null) { for (String oldCompName : registeredComponents.keySet()) { removeToolAccessComponent(oldCompName, id); } registeredComponentsPerConnection.remove(id); } } } /** * Task for updating remote access components. * * @author Brigitte Boden */ public class UpdateSSHToolsTask implements Runnable { @Override @TaskDescription("Periodic updating of SSH-accessible remote tools") public void run() { updateSshRemoteAccessComponents(); } } /** * OSGi-DS life cycle method. */ public void activate() { taskFuture = ConcurrencyUtils.getAsyncTaskService() .scheduleAtFixedRate(new UpdateSSHToolsTask(), TimeUnit.SECONDS.toMillis(UPDATE_TOOLS_INTERVAL_SECS)); started = true; } /** * OSGi-DS life cycle method. */ public void deactivate() { started = false; if (taskFuture != null) { taskFuture.cancel(false); taskFuture = null; } } private byte[] readDefaultToolIcon(int iconSize) { try (InputStream inputStream = getClass().getResourceAsStream("/icons/tool" + iconSize + ".png")) { return IOUtils.toByteArray(inputStream); } catch (FileNotFoundException e) { return null; } catch (IOException | NullPointerException e) { return null; } } }