/** * Copyright 2013-2017 Linagora, Université Joseph Fourier, Floralis * * The present code is developed in the scope of the joint LINAGORA - * Université Joseph Fourier - Floralis research program and is designated * as a "Result" pursuant to the terms and conditions of the LINAGORA * - Université Joseph Fourier - Floralis research program. Each copyright * holder of Results enumerated here above fully & independently holds complete * ownership of the complete Intellectual Property rights applicable to the whole * of said Results, and may freely exploit it in any manner which does not infringe * the moral rights of the other copyright holders. * * 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 net.roboconf.plugin.script.internal; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Logger; import net.roboconf.core.model.beans.Import; import net.roboconf.core.model.beans.Instance; import net.roboconf.core.model.beans.Instance.InstanceStatus; import net.roboconf.core.model.helpers.InstanceHelpers; import net.roboconf.core.model.helpers.VariableHelpers; import net.roboconf.core.utils.DockerAndScriptUtils; import net.roboconf.core.utils.ProgramUtils; import net.roboconf.core.utils.Utils; import net.roboconf.plugin.api.PluginException; import net.roboconf.plugin.api.PluginInterface; import net.roboconf.plugin.script.internal.ScriptUtils.ActionFileFilter; import net.roboconf.plugin.script.internal.templating.InstanceTemplateHelper; /** * The plug-in invokes a script (eg. shell) on every life cycle change. * <p> * The action is one of "deploy", "start", "stop", "undeploy" and "update".<br> * Let's take an example with the "start" action to understand the way this plug-in works. * </p> * <ul> * <li>The plug-in will load scripts/start.sh</li> * <li>If it is not found, it will try to load templates/start.sh.template</li> * <li>If it is not found, it will try to load templates/default.sh.template</li> * <li>If it is not found, the plug-in will do nothing</li> * </ul> * <p> * The default template is used to factorize actions. * </p> * * @author Noël - LIG * @author Linh-Manh Pham - LIG * @author Pierre-Yves Gibello - Linagora * @author Christophe Hamerling - Linagora */ public class PluginScript implements PluginInterface { public static final String PLUGIN_NAME = "script"; private static final String SCRIPTS_FOLDER_NAME = "scripts"; private static final String TEMPLATES_FOLDER_NAME = "roboconf-templates"; private static final String FILES_FOLDER_NAME = "files"; private final Logger logger = Logger.getLogger( getClass().getName()); String agentId; String applicationName, scopedInstancePath; @Override public String getPluginName() { return PLUGIN_NAME; } @Override public void setNames( String applicationName, String scopedInstancePath ) { this.applicationName = applicationName; this.scopedInstancePath = scopedInstancePath; this.agentId = "'" + scopedInstancePath + "' agent"; } @Override public void initialize( Instance instance ) throws PluginException { this.logger.fine(this.agentId + ": initializing the plugin for " + instance); // All scripts deployed should be made executable (the agent is supposed to run as root) File instanceDirectory = InstanceHelpers.findInstanceDirectoryOnAgent( instance ); ScriptUtils.setScriptsExecutable( new File( instanceDirectory, SCRIPTS_FOLDER_NAME )); } @Override public void deploy( Instance instance ) throws PluginException { this.logger.fine( this.agentId + " is deploying instance " + instance ); try { prepareAndExecuteCommand( "deploy", instance, null, null ); } catch( Exception e ) { throw new PluginException( e ); } } @Override public void start( Instance instance ) throws PluginException { this.logger.fine( this.agentId + " is starting instance " + instance ); try { prepareAndExecuteCommand( "start", instance, null, null ); } catch( Exception e ) { throw new PluginException( e ); } } @Override public void update(Instance instance, Import importChanged, InstanceStatus statusChanged) throws PluginException { this.logger.fine( this.agentId + " is updating instance " + instance ); try { prepareAndExecuteCommand( "update", instance, importChanged, statusChanged ); } catch( Exception e ) { throw new PluginException( e ); } } @Override public void stop( Instance instance ) throws PluginException { this.logger.fine( this.agentId + " is stopping instance " + instance ); try { prepareAndExecuteCommand( "stop", instance, null, null ); } catch( Exception e ) { throw new PluginException( e ); } } @Override public void undeploy( Instance instance ) throws PluginException { this.logger.fine( this.agentId + " is undeploying instance " + instance ); try { prepareAndExecuteCommand( "undeploy", instance, null, null ); } catch( Exception e ) { throw new PluginException( e ); } } private void prepareAndExecuteCommand(String action, Instance instance, Import importChanged, InstanceStatus statusChanged) throws IOException, InterruptedException { this.logger.info("Preparing the invocation of " + action + " script for instance " + instance ); File instanceDirectory = InstanceHelpers.findInstanceDirectoryOnAgent( instance ); File scriptsFolder = new File(instanceDirectory, SCRIPTS_FOLDER_NAME); File templatesFolder = new File(instanceDirectory, TEMPLATES_FOLDER_NAME); // Look for action script (default <action>.sh, or any file that starts with <action>) File script = new File(scriptsFolder, action + ".sh"); if(! script.exists()) { File[] foundFiles = scriptsFolder.listFiles(new ActionFileFilter(action)); if(foundFiles != null && foundFiles.length > 0) { script = foundFiles[0]; if(foundFiles.length > 1) this.logger.warning("More than one " + action + " script found: taking the 1st one, " + script.getName()); } } File template = new File(templatesFolder, action + ".template"); if( ! template.exists()) template = new File(templatesFolder, "default.template"); if (script.exists()) { executeScript(script, instance, importChanged, statusChanged, instanceDirectory.getAbsolutePath()); } else if (template.exists()) { File generated = generateTemplate(template, instance); executeScript(generated, instance, importChanged, statusChanged, instanceDirectory.getAbsolutePath()); Utils.deleteFilesRecursively( generated ); } else { this.logger.warning("Can not find a script or a template for action " + action); } } /** * Generates a file from the template and the instance. * @param template * @param instance * @return the generated file * @throws IOException */ protected File generateTemplate(File template, Instance instance) throws IOException { String scriptName = instance.getName().replace( "\\s+", "_" ); File generated = File.createTempFile( scriptName, ".script"); InstanceTemplateHelper.injectInstanceImports(instance, template, generated); return generated; } protected void executeScript( File script, Instance instance, Import importChanged, InstanceStatus statusChanged, String instanceDir ) throws IOException, InterruptedException { String[] command = { script.getAbsolutePath() }; if(! script.canExecute()) script.setExecutable(true); Map<String, String> environmentVars = new HashMap<String, String>(); Map<String, String> vars = ScriptUtils.formatExportedVars(instance); environmentVars.putAll(vars); Map<String, String> importedVars = ScriptUtils.formatImportedVars( instance ); environmentVars.putAll( DockerAndScriptUtils.buildReferenceMap( instance )); environmentVars.putAll( importedVars ); environmentVars.put("ROBOCONF_FILES_DIR", new File( instanceDir, FILES_FOLDER_NAME ).getAbsolutePath()); // Upon update, retrieve the status of the instance that triggered the update. // Should be either DEPLOYED_STARTED or DEPLOYED_STOPPED... if( statusChanged != null ) environmentVars.put("ROBOCONF_UPDATE_STATUS", statusChanged.toString()); // Upon update, retrieve the import that changed // (removed when an instance stopped, or added when it started) if( importChanged != null ) { environmentVars.put("ROBOCONF_IMPORT_CHANGED_INSTANCE_PATH", importChanged.getInstancePath()); environmentVars.put("ROBOCONF_IMPORT_CHANGED_COMPONENT", importChanged.getComponentName()); for (Entry<String, String> entry : importChanged.getExportedVars().entrySet()) { // "ROBOCONF_IMPORT_CHANGED_ip=127.0.0.1" String vname = VariableHelpers.parseVariableName(entry.getKey()).getValue(); importedVars.put("ROBOCONF_IMPORT_CHANGED_" + vname, entry.getValue()); } } int exitCode = ProgramUtils.executeCommand( this.logger, command, script.getParentFile(), environmentVars, this.applicationName, this.scopedInstancePath ); if( exitCode != 0 ) throw new IOException( "Script execution failed. Exit code: " + exitCode ); } }