/* * ProActive Parallel Suite(TM): * The Open Source library for parallel and distributed * Workflows & Scheduling, Orchestration, Cloud Automation * and Big Data Analysis on Enterprise Grids & Clouds. * * Copyright (c) 2007 - 2017 ActiveEon * Contact: contact@activeeon.com * * This library is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License * as published by the Free Software Foundation: version 3 of * the License. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * If needed, contact us to obtain a release under GPL Version 2 or 3 * or a different license than the AGPL. */ package org.ow2.proactive.scheduler.core; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import org.apache.commons.io.FileUtils; import org.apache.log4j.Logger; import org.objectweb.proactive.api.PAActiveObject; import org.objectweb.proactive.core.ProActiveException; import org.objectweb.proactive.core.node.Node; import org.objectweb.proactive.core.node.NodeFactory; import org.objectweb.proactive.extensions.dataspaces.api.PADataSpaces; import org.objectweb.proactive.extensions.dataspaces.core.BaseScratchSpaceConfiguration; import org.objectweb.proactive.extensions.dataspaces.core.DataSpacesNodes; import org.objectweb.proactive.extensions.dataspaces.core.InputOutputSpaceConfiguration; import org.objectweb.proactive.extensions.dataspaces.core.SpaceInstanceInfo; import org.objectweb.proactive.extensions.dataspaces.core.naming.NamingService; import org.objectweb.proactive.extensions.dataspaces.core.naming.NamingServiceDeployer; import org.objectweb.proactive.extensions.dataspaces.exceptions.FileSystemException; import org.objectweb.proactive.extensions.dataspaces.exceptions.SpaceAlreadyRegisteredException; import org.objectweb.proactive.extensions.vfsprovider.FileSystemServerDeployer; import org.objectweb.proactive.extensions.vfsprovider.util.URIHelper; import org.ow2.proactive.scheduler.common.SchedulerConstants; import org.ow2.proactive.scheduler.core.properties.PASchedulerProperties; public class DataSpaceServiceStarter implements Serializable { public static final Logger logger = Logger.getLogger(DataSpaceServiceStarter.class); /** * Default Local Paths */ private static final String DEFAULT_LOCAL = PASchedulerProperties.SCHEDULER_HOME.getValueAsString() + File.separator + "data"; private static final String DEFAULT_LOCAL_INPUT = DEFAULT_LOCAL + File.separator + "defaultinput"; private static final String DEFAULT_LOCAL_OUTPUT = DEFAULT_LOCAL + File.separator + "defaultoutput"; private static final String DEFAULT_LOCAL_GLOBAL = DEFAULT_LOCAL + File.separator + "defaultglobal"; private static final String DEFAULT_LOCAL_USER = DEFAULT_LOCAL + File.separator + "defaultuser"; private static final String DEFAULT_LOCAL_SCRATCH = DEFAULT_LOCAL + File.separator + "scratch"; private static HashMap<String, HashSet<String>> spacesConfigurations = new HashMap<>(); /** * Naming service */ private static String namingServiceURL; private static NamingServiceDeployer namingServiceDeployer; private static NamingService namingService; private static String localhostname = null; /** * static instance */ private static DataSpaceServiceStarter instance = null; /** * Local node (should be the one of the scheduler) */ private Node schedulerNode = null; private boolean serviceStarted = false; /** * Application ID last used to configure the local node, null if none */ private static String appidConfigured = null; /** * Dataspace servers */ private ArrayList<FileSystemServerDeployer> servers = new ArrayList<>(4); private DataSpaceServiceStarter() { } public static DataSpaceServiceStarter getDataSpaceServiceStarter() { if (instance == null) { instance = new DataSpaceServiceStarter(); } return instance; } /** * StartNaming service and default file system server if needed. * * @throws Exception */ public void startNamingService() throws Exception { schedulerNode = PAActiveObject.getNode(); localhostname = java.net.InetAddress.getLocalHost().getHostName(); namingServiceDeployer = new NamingServiceDeployer(true); namingServiceURL = namingServiceDeployer.getNamingServiceURL(); namingService = NamingService.createNamingServiceStub(namingServiceURL); // configure node for Data Spaces final BaseScratchSpaceConfiguration scratchConf = new BaseScratchSpaceConfiguration((String) null, DEFAULT_LOCAL_SCRATCH); DataSpacesNodes.configureNode(schedulerNode, scratchConf); //set default INPUT/OUTPUT spaces if needed PASchedulerProperties[][] confs = { { PASchedulerProperties.DATASPACE_DEFAULTINPUT_URL, PASchedulerProperties.DATASPACE_DEFAULTINPUT_LOCALPATH, PASchedulerProperties.DATASPACE_DEFAULTINPUT_HOSTNAME }, { PASchedulerProperties.DATASPACE_DEFAULTOUTPUT_URL, PASchedulerProperties.DATASPACE_DEFAULTOUTPUT_LOCALPATH, PASchedulerProperties.DATASPACE_DEFAULTOUTPUT_HOSTNAME }, { PASchedulerProperties.DATASPACE_DEFAULTGLOBAL_URL, PASchedulerProperties.DATASPACE_DEFAULTGLOBAL_LOCALPATH, PASchedulerProperties.DATASPACE_DEFAULTGLOBAL_HOSTNAME }, { PASchedulerProperties.DATASPACE_DEFAULTUSER_URL, PASchedulerProperties.DATASPACE_DEFAULTUSER_LOCALPATH, PASchedulerProperties.DATASPACE_DEFAULTUSER_HOSTNAME } }; String[] spacesNames = { "DefaultInputSpace", "DefaultOutputSpace", "GlobalSpace", "UserSpaces" }; String[] humanReadableNames = { "default INPUT space", "default OUTPUT space", "shared GLOBAL space", "USER spaces" }; String[] default_paths = { DEFAULT_LOCAL_INPUT, DEFAULT_LOCAL_OUTPUT, DEFAULT_LOCAL_GLOBAL, DEFAULT_LOCAL_USER }; for (int i = 0; i < confs.length; i++) { //variable used to precise exception String spaceDir = null; if (confs[i][0].isSet() && confs[i][0].getValueAsString().trim().isEmpty()) { logger.info("Unsetting property : " + confs[i][0].getKey()); confs[i][0].unSet(); } if (!confs[i][0].isSet()) { // if URL is set, if not we start a server ourselves try { logger.debug("Starting " + humanReadableNames[i] + " server..."); if (!confs[i][1].isSet()) { // check if the localpath is set, if not we use the default path spaceDir = default_paths[i]; confs[i][1].updateProperty(spaceDir); } else { // otherwise, we build a FileServer on the provided path logger.debug("Using property-defined path at " + confs[i][1].getValueAsString()); spaceDir = confs[i][1].getValueAsString(); } File dir = new File(spaceDir); if (!dir.exists()) { dir.mkdirs(); } FileSystemServerDeployer server = startServer(spacesNames[i], humanReadableNames[i], spaceDir); servers.add(server); confs[i][0].updateProperty(buildServerUrlList(server.getVFSRootURLs())); // use the hostname property if it is set, otherwise, use the local hostname if (!confs[i][2].isSet()) { confs[i][2].updateProperty(localhostname); } logger.debug(humanReadableNames[i] + " server local path is " + spaceDir); } catch (IllegalArgumentException iae) { throw new IllegalArgumentException("Directory '" + spaceDir + "' cannot be accessed. Check if directory exists or if you have read/write rights."); } } } Set<SpaceInstanceInfo> predefinedSpaces = new HashSet<>(); namingService.registerApplication(SchedulerConstants.SCHEDULER_DATASPACE_APPLICATION_ID, predefinedSpaces); serviceStarted = true; try { // register the Global space createSpace(SchedulerConstants.SCHEDULER_DATASPACE_APPLICATION_ID, SchedulerConstants.GLOBALSPACE_NAME, PASchedulerProperties.DATASPACE_DEFAULTGLOBAL_URL.getValueAsString(), PASchedulerProperties.DATASPACE_DEFAULTGLOBAL_LOCALPATH.getValueAsString(), localhostname, false, true); } catch (Exception e) { logger.error("", e); } } private String buildServerUrlList(String[] urls) { StringBuilder builder = new StringBuilder(); for (String url : urls) { if (!url.endsWith("/")) { builder.append(url + "/ "); } else { builder.append(url + " "); } } builder.deleteCharAt(builder.length() - 1); return builder.toString(); } private FileSystemServerDeployer startServer(String spaceName, String readableName, String spaceDir) throws IOException { FileSystemServerDeployer server = new FileSystemServerDeployer(spaceName, spaceDir, true, true); String[] urls = server.getVFSRootURLs(); logger.info("Started " + readableName + " server at " + Arrays.toString(urls)); return server; } /** * Helper method used to create a space configuration and register it into the naming service * This helper can eventually configure the local Node for the provided application ID, if the dataspace needs to be user locally. * It will only register the dataspace in the naming service otherwise * @param appID the Application ID * @param name the name of the dataspace * @param urlsproperty the space-delimited url list property of the Virtual File Systems (for different protocols) * @param path the path to the dataspace in the localfilesystem * @param hostname the host where the file server is deployed * @param inputConfiguration if the configuration is an InputSpace configuration (read-only) * @param localConfiguration if the local node needs to be configured for the provided application */ public void createSpace(String appID, String name, String urlsproperty, String path, String hostname, boolean inputConfiguration, boolean localConfiguration) throws FileSystemException, URISyntaxException, ProActiveException, MalformedURLException { if (!serviceStarted) { throw new IllegalStateException("DataSpace service is not started"); } if (!spacesConfigurations.containsKey(appID)) { if (localConfiguration) { if (appidConfigured != null) { logger.warn("Node " + schedulerNode.getNodeInformation().getURL() + " was configured for appid = " + appidConfigured + ", reconfiguring..."); } DataSpacesNodes.configureApplication(schedulerNode, appID, namingService); logger.debug("Node " + schedulerNode.getNodeInformation().getURL() + " configured for appid = " + appID); appidConfigured = appID; } else { namingService.registerApplication(appID, new HashSet<SpaceInstanceInfo>()); } spacesConfigurations.put(appID, new HashSet<String>()); } if (spacesConfigurations.get(appID).contains(name) && !PADataSpaces.DEFAULT_IN_OUT_NAME.equals(name)) { throw new SpaceAlreadyRegisteredException("Space " + name + " for appid=" + appID + " is already registered"); } InputOutputSpaceConfiguration spaceConf = null; // Converts the property to an ArrayList ArrayList<String> finalurls = new ArrayList<>(Arrays.asList(dsConfigPropertyToUrls(urlsproperty))); if (inputConfiguration) { spaceConf = InputOutputSpaceConfiguration.createInputSpaceConfiguration(finalurls, path, hostname != null ? hostname : localhostname, name); } else { spaceConf = InputOutputSpaceConfiguration.createOutputSpaceConfiguration(finalurls, path, hostname != null ? hostname : localhostname, name); } namingService.register(new SpaceInstanceInfo(appID, spaceConf)); spacesConfigurations.get(appID).add(spaceConf.getName()); logger.debug("Space " + name + " for appid = " + appID + " with urls = " + finalurls + " registered"); } public void clearSpaceConfigurations() { spacesConfigurations.clear(); } /** * Similar to createSpace, but in addition it will use a provided username to append it to the given urls * If the localpath is provided, it will also create sub folders to the dataspace root with this username * * @param username username used to update urls and create folders * @param appID the Application ID * @param spaceName the name of the dataspace * @param urls the url list of the Virtual File Systems (for different protocols) * @param localpath the path to the dataspace in the localfilesystem * @param hostname the host where the file server is deployed * @param inputConfiguration if the configuration is an InputSpace configuration (read-only) * @param localConfiguration if the local node needs to be configured for the provided application * @throws URISyntaxException * @throws MalformedURLException * @throws ProActiveException * @throws FileSystemException */ public void createSpaceWithUserNameSubfolder(String username, String appID, String spaceName, String urls, String localpath, String hostname, boolean inputConfiguration, boolean localConfiguration) throws URISyntaxException, IOException, ProActiveException { // create a local folder with the username if (localpath != null) { localpath = localpath + File.separator + username; File localPathFile = new File(localpath); if (!localPathFile.exists()) { FileUtils.forceMkdir(localPathFile); } } // updates the urls with the username String[] urlarray = dsConfigPropertyToUrls(urls); String[] updatedArray = urlsWithUserDir(urlarray, username); String newPropertyValue = urlsToDSConfigProperty(updatedArray); // create the User Space for the given user createSpace(appID, spaceName, newPropertyValue, localpath, hostname, inputConfiguration, localConfiguration); } /** * Converts url array to a property separated by spaces * @param urls url array * @return property */ public static String urlsToDSConfigProperty(String[] urls) { if (urls == null || urls.length == 0) { throw new IllegalArgumentException("Empty url array"); } StringBuilder urlProperty = new StringBuilder(); urlProperty.append("\""); for (String url : urls) { // in case the username contains a space, encode the total url urlProperty.append(url); urlProperty.append("\" \""); } // remove the last two characters urlProperty.delete(urlProperty.length() - 2, urlProperty.length() - 2); return urlProperty.toString(); } /** * Parses the given dataspace configuration property to an array of strings * The parsing handles double quotes and space separators * @param property dataspace configuration property * @return an array of string urls */ public static String[] dsConfigPropertyToUrls(String property) { if (property.trim().length() == 0) { return new String[0]; } if (property.contains("\"")) { // if the input contains quote, split it along space delimiters and quotes "A" "B" etc... // the pattern uses positive look-behind and look-ahead final String[] outputWithQuotes = property.trim().split("(?<=\") +(?=\")"); // removing quotes ArrayList<String> output = new ArrayList<>(); for (String outputWithQuote : outputWithQuotes) { int len = outputWithQuote.length(); if (outputWithQuote.length() > 2) { output.add(outputWithQuote.substring(1, len - 1)); } } return output.toArray(new String[0]); } else { // if the input contains no quote, split it along space delimiters return property.trim().split(" +"); } } /** * Appends the given userName into each member of the url array * @param inputUrls * @param username * @return an url array with the userName appended */ public static String[] urlsWithUserDir(String[] inputUrls, String username) { String[] output = new String[inputUrls.length]; for (int i = 0; i < inputUrls.length; i++) { String url = inputUrls[i]; String urlToAdd; if (!url.endsWith("/")) { urlToAdd = url + "/" + username; } else { urlToAdd = url + username; } output[i] = URIHelper.convertToEncodedURIString(urlToAdd); } return output; } /** * Terminate naming service and file system server if needed */ public void terminateNamingService() { if (!serviceStarted) { throw new IllegalStateException("DataSpace service is not started"); } try { DataSpacesNodes.closeNodeConfig(NodeFactory.getDefaultNode()); namingServiceDeployer.terminate(); } catch (Throwable t) { } for (int i = 0; i < servers.size(); i++) { try { servers.get(i).terminate(); } catch (Throwable t) { } } serviceStarted = false; } /** * Get the namingServiceURL * * @return the namingServiceURL */ public String getNamingServiceURL() { if (!serviceStarted) { throw new IllegalStateException("DataSpace service is not started"); } return namingServiceURL; } /** * Get the namingService * * @return the namingService */ public NamingService getNamingService() { if (!serviceStarted) { throw new IllegalStateException("DataSpace service is not started"); } return namingService; } }