/** * Copyright 2015-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.core.model; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.roboconf.core.Constants; import net.roboconf.core.ErrorCode; import net.roboconf.core.model.beans.Component; import net.roboconf.core.model.beans.ImportedVariable; import net.roboconf.core.model.beans.Instance; import net.roboconf.core.model.helpers.ComponentHelpers; import net.roboconf.core.model.helpers.VariableHelpers; import net.roboconf.core.utils.ProgramUtils; import net.roboconf.core.utils.ResourceUtils; import net.roboconf.core.utils.Utils; /** * Modularity-breaking validator for recipes. * <p> * A class that centralizes validation for recipes. Recipes handlers are usually * plugged through extensions. It would have been logical to plug extra-validation in the same * way. However, Roboconf's core module aims at being a stand-alone library that can be used at * runtime but also in external tools. * </p> * <p> * This is why we allow this class to break modularity.<br> * And once again, it aims at bringing useful feedback to users. * </p> * * @author Vincent Zurczak - Linagora */ public final class RecipesValidator { public static final String SCRIPTS_DIR_NAME = "scripts"; /** * Private empty constructor. */ private RecipesValidator() { // nothing } /** * Validates the recipes of a component. * @param applicationFilesDirectory the application's directory * @param component the component * @return a non-null list of errors */ public static List<ModelError> validateComponentRecipes( File applicationFilesDirectory, Component component ) { List<ModelError> result; if( "puppet".equalsIgnoreCase( component.getInstallerName())) result = validatePuppetComponent( applicationFilesDirectory, component ); else if( "script".equalsIgnoreCase( component.getInstallerName())) result = validateScriptComponent( applicationFilesDirectory, component ); else result = Collections.emptyList(); return result; } /** * Validates a component associated with the Puppet installer. * @param applicationFilesDirectory the application's directory * @param component the component * @return a non-null list of errors */ private static List<ModelError> validateScriptComponent( File applicationFilesDirectory, Component component ) { List<ModelError> result = new ArrayList<ModelError> (); // There must be a "scripts" directory File directory = ResourceUtils.findInstanceResourcesDirectory( applicationFilesDirectory, component ); File scriptsDir = new File( directory, SCRIPTS_DIR_NAME ); if( ! scriptsDir.exists()) result.add( new ModelError( ErrorCode.REC_SCRIPT_NO_SCRIPTS_DIR, component, "Component: " + component )); return result; } /** * Validates a component associated with the Puppet installer. * @param applicationFilesDirectory the application's directory * @param component the component * @return a non-null list of errors */ private static List<ModelError> validatePuppetComponent( File applicationFilesDirectory, Component component ) { List<ModelError> result = new ArrayList<ModelError> (); // Check imports for( ImportedVariable var : ComponentHelpers.findAllImportedVariables( component ).values()) { if( var.getName().endsWith( "." + Constants.WILDCARD )) { result.add( new ModelError( ErrorCode.REC_PUPPET_DISLIKES_WILDCARD_IMPORTS, component, "Component: " + component )); break; } } // There must be a Puppet module that starts with "roboconf_" File directory = ResourceUtils.findInstanceResourcesDirectory( applicationFilesDirectory, component ); File[] children = directory.listFiles(); children = children == null ? new File[ 0 ] : children; List<File> modules = new ArrayList<File> (); for( File f : children ) { if( f.isDirectory() && f.getName().toLowerCase().startsWith( "roboconf_" )) modules.add( f ); } if( modules.isEmpty()) result.add( new ModelError( ErrorCode.REC_PUPPET_HAS_NO_RBCF_MODULE, component, "Component: " + component )); else if( modules.size() > 1 ) result.add( new ModelError( ErrorCode.REC_PUPPET_HAS_TOO_MANY_RBCF_MODULES, component, "Component: " + component )); // Analyze the module parameters if( modules.size() == 1 ) { File pp1 = new File( modules.get( 0 ), "manifests/update.pp" ); File pp2 = new File( modules.get( 0 ), "manifests/init.pp" ); File withUpdateParams = pp1.exists() ? pp1 : pp2; // Validate the files children = new File( modules.get( 0 ), "manifests" ).listFiles(); children = children == null ? new File[ 0 ] : children; for( File f : children ) { try { if( f.isFile() && f.getName().toLowerCase().endsWith( ".pp" )) checkPuppetFile( f, f.equals( withUpdateParams ), component, result ); } catch( IOException e ) { Logger logger = Logger.getLogger( RecipesValidator.class.getName()); logger.warning( "The content of the Puppet file '" + f + "' could not be read." ); Utils.logException( logger, e ); } } } return result; } /** * Reads a Puppet script and validates it. * @param pp the Puppet script * @param withUpdateParams true if update parameters should be present * @param component the component * @param errors a non-null list of errors * @throws IOException if the file content could be read */ private static void checkPuppetFile( File pp, boolean withUpdateParams, Component component, Collection<ModelError> errors ) throws IOException { // Try to use the Puppet validator String[] cmd = { "puppet", "parser", "validate", pp.getAbsolutePath()}; Logger logger = Logger.getLogger( RecipesValidator.class.getName()); try { int execCode = ProgramUtils.executeCommand( logger, cmd, null, null, null, null ); if( execCode != 0 ) errors.add( new ModelError( ErrorCode.REC_PUPPET_SYNTAX_ERROR, component, "Component: " + component + ", File: " + pp )); } catch( Exception e ) { logger.info( "Puppet parser is not available on the machine." ); } // We do not validate with puppet-lint. // Indeed, this tool is mostly about coding style and conventions. // Extract the script parameters Pattern pattern = Pattern.compile( "class [^(]+\\(([^)]+)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL ); String content = Utils.readFileContent( pp ); Matcher m = pattern.matcher( content ); Set<String> params = new HashSet<String> (); if( ! m.find()) return; for( String s : m.group( 1 ).split( "," )) { Entry<String,String> entry = VariableHelpers.parseExportedVariable( s.trim()); params.add( entry.getKey()); } // Check the update parameters if( withUpdateParams ) { if( ! params.remove( "$importDiff" )) errors.add( new ModelError( ErrorCode.REC_PUPPET_MISSING_PARAM_IMPORT_DIFF, component, "Component: " + component + ", File: " + pp )); } // Prevent errors with start.pp, etc params.remove( "$importDiff" ); // Check the other ones if( ! params.remove( "$runningState" )) errors.add( new ModelError( ErrorCode.REC_PUPPET_MISSING_PARAM_RUNNING_STATE, component, "Component: " + component + ", File: " + pp )); // Imports imply some variables are expected Instance fake = new Instance( "fake" ).component( component ); for( String facetOrComponentName : VariableHelpers.findPrefixesForImportedVariables( fake )) { String details = "Component: " + component + ", File: " + pp + ", Parameter: " + facetOrComponentName.toLowerCase(); if( ! params.remove( "$" + facetOrComponentName.toLowerCase())) errors.add( new ModelError( ErrorCode.REC_PUPPET_MISSING_PARAM_FROM_IMPORT, component, details )); } } }