/************************************************************************* * Copyright 2009-2016 Eucalyptus Systems, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * * Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta * CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need * additional information or have any questions. ************************************************************************/ package com.eucalyptus.simpleworkflow.common.client; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.reflect.Modifier; import java.util.Collections; import java.util.List; import java.util.Properties; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.GnuParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.log4j.Logger; import com.amazonaws.ClientConfiguration; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.auth.InstanceProfileCredentialsProvider; import com.amazonaws.services.simpleworkflow.AmazonSimpleWorkflow; import com.amazonaws.services.simpleworkflow.AmazonSimpleWorkflowClient; import com.amazonaws.services.simpleworkflow.flow.annotations.Activities; import com.amazonaws.services.simpleworkflow.flow.annotations.Workflow; import com.eucalyptus.records.Logs; import com.eucalyptus.system.Ats; import com.google.common.collect.Lists; import com.google.common.collect.Sets; /** * @author Sang-Min Park (sangmin.park@hpe.com) * */ public class WorkflowClientStandalone { private static Logger LOG = Logger.getLogger( WorkflowClientStandalone.class ); private static WorkflowClientStandalone _instance = new WorkflowClientStandalone(); public static WorkflowClientStandalone getInstance() { return _instance; } private List<Class> activityClasses = Lists.newArrayList(); private List<Class> workflowClasses = Lists.newArrayList(); private List<WorkflowClient> clients = Lists.newArrayList(); private String jarFile = null; private Set<String> allowedClassNames = Sets.newHashSet(); private String credentialPropertyFile = null; private String swfEndpoint = null; private String taskList = null; private String domain = null; private int clientConnectionTimeout = 30000; private int clientMaxConnections = 100; private int domainRetentionPeriodInDays = 1; private int pollThreadCount = 1; private String logLevel = "DEBUG"; private String logDir = "/var/log/eucalyptus"; private String logAppender = "console-log"; @SuppressWarnings("static-access") private static Options buildOptions() { final Options opts = new Options(); opts.addOption( OptionBuilder .withLongOpt("endpoint") .hasArgs(1) .withDescription("SWF Service Endpoint") .isRequired() .create('e')); opts.addOption(OptionBuilder .withLongOpt("domain") .hasArgs(1) .withDescription("SWF Domain") .isRequired() .create('d')); opts.addOption(OptionBuilder .withLongOpt("tasklist") .hasArgs(1) .withDescription("SWF task list") .isRequired() .create('l')); opts.addOption(OptionBuilder .withLongOpt("timeout") .hasArgs(1) .withDescription("SWF client connection timeout") .isRequired(false) .create('o')); opts.addOption(OptionBuilder .withLongOpt("maxconn") .hasArgs(1) .withDescription("SWF client max connections") .isRequired(false) .create('m')); opts.addOption(OptionBuilder .withLongOpt("retention") .hasArgs(1) .withDescription("SWF domain retention period in days") .isRequired(false) .create('r')); opts.addOption(OptionBuilder .withLongOpt("threads") .hasArgs(1) .withDescription("Polling threads count") .isRequired(false) .create('t')); opts.addOption(OptionBuilder .withLongOpt("jar") .hasArgs(1) .withDescription("JAR file that implement workflows" + " and activities") .isRequired(true) .create()); opts.addOption(OptionBuilder .withLongOpt("classes") .hasArgs(1) .withDescription("Limit workflow and activities classes to load (class names are separated by ':')") .isRequired(false) .create()); opts.addOption(OptionBuilder .withLongOpt("credential") .hasArgs(1) .withDescription("Property file containing AWS credentials to use (default is to use session credentials from instance's metadata") .isRequired(false) .create()); opts.addOption(OptionBuilder .withLongOpt("loglevel") .hasArgs(1) .withDescription("Logging level (default: DEBUG)") .isRequired(false) .create()); opts.addOption(OptionBuilder .withLongOpt("logdir") .hasArgs(1) .withDescription("Directory containing log files (default: /var/log/eucalyptus)") .isRequired(false) .create()); opts.addOption(OptionBuilder .withLongOpt("logappender") .hasArgs(1) .withDescription("Log4j appender to use") .isRequired(false) .create()); return opts; } private void readOptions(final CommandLine cli) throws NumberFormatException{ this.swfEndpoint = cli.getOptionValue("endpoint"); this.domain = cli.getOptionValue("domain"); this.taskList = cli.getOptionValue("tasklist"); if (cli.hasOption("timeout")) this.clientConnectionTimeout = Integer.parseInt(cli.getOptionValue("timeout")); if (cli.hasOption("maxconn")) this.clientMaxConnections = Integer.parseInt(cli.getOptionValue("maxconn")); if (cli.hasOption("retention")) this.domainRetentionPeriodInDays = Integer.parseInt(cli.getOptionValue("retention")); if (cli.hasOption("threads")) this.pollThreadCount = Integer.parseInt(cli.getOptionValue("threads")); if (cli.hasOption("jar")) this.jarFile = cli.getOptionValue("jar"); if (cli.hasOption("credential")) this.credentialPropertyFile = cli.getOptionValue("credential"); if (cli.hasOption("loglevel")) this.logLevel = cli.getOptionValue("loglevel"); if (cli.hasOption("logdir")) this.logDir = cli.getOptionValue("logdir"); if (cli.hasOption("logappender")) this.logAppender = cli.getOptionValue("logappender"); if (cli.hasOption("classes")) { final String classNames = cli.getOptionValue("classes"); if(classNames.contains(":")) { for (final String cls : classNames.split(":")) { this.allowedClassNames.add(cls); } }else { this.allowedClassNames.add(classNames); } } } private static void printHelp(final Options opts, final String error) { final HelpFormatter formatter = new HelpFormatter(); formatter.setDescPadding(0); String header = "\n" + "Welcome to the Standalone SWF Host!\n" + "The program discovers and hosts SWF workflows and activities"; final String footer = error == null? "\n" : String.format("\n%s", error); formatter.printHelp("java -cp jarfiles com.eucalyptus.simpleworkflow.common.client.WorkflowClientStandalone", header, opts, footer, true); } private static void initLogs() { final WorkflowClientStandalone instance = WorkflowClientStandalone.getInstance(); System.setProperty("euca.log.level", instance.logLevel); System.setProperty("euca.log.dir", instance.logDir); System.setProperty("euca.log.appender", instance.logAppender); Logs.init(); } private void discoverWorkflows() throws Exception { final File f = new File(this.jarFile); if (f.exists() && !f.isDirectory()) processJar(f); else throw new Exception(String.format("No such file is found: %s", this.jarFile)); } private void processJar( File f ) throws Exception { final JarFile jar = new JarFile( f ); final Properties props = new Properties( ); final List<JarEntry> jarList = Collections.list( jar.entries( ) ); LOG.trace( "-> Trying to load component info from " + f.getAbsolutePath( ) ); for ( final JarEntry j : jarList ) { try { if ( j.getName( ).matches( ".*\\.class.{0,1}" ) ) { handleClassFile( f, j ); } } catch ( RuntimeException ex ) { LOG.error( ex, ex ); jar.close( ); throw ex; } } jar.close( ); } private void handleClassFile( final File f, final JarEntry j ) throws IOException, RuntimeException { final String classGuess = j.getName( ).replaceAll( "/", "." ).replaceAll( "\\.class.{0,1}", "" ); try { final Class candidate = ClassLoader.getSystemClassLoader( ).loadClass( classGuess ); final Ats ats = Ats.inClassHierarchy(candidate); if ((this.allowedClassNames.isEmpty() || this.allowedClassNames.contains(candidate.getName()) || this.allowedClassNames.contains(candidate.getCanonicalName()) || this.allowedClassNames.contains(candidate.getSimpleName())) && ( ats.has( Workflow.class ) || ats.has( Activities.class ) ) && !Modifier.isAbstract( candidate.getModifiers() ) && !Modifier.isInterface( candidate.getModifiers( ) ) && !candidate.isLocalClass( ) && !candidate.isAnonymousClass( ) ) { if ( ats.has( Workflow.class ) ) { this.workflowClasses.add(candidate); LOG.debug( "Discovered workflow implementation class: " + candidate.getName( ) ); } else { this.activityClasses.add(candidate); LOG.debug( "Discovered activity implementation class: " + candidate.getName( ) ); } } } catch ( final ClassNotFoundException e ) { LOG.debug( e, e ); } } private AWSCredentialsProvider getCredentialsProvider() { AWSCredentialsProvider provider = null; if (this.credentialPropertyFile != null) { provider = new AWSCredentialsProvider() { private String accessKey = null; private String secretAccessKey = null; private void readProperty() throws FileNotFoundException, IOException{ final FileInputStream stream = new FileInputStream(new File(credentialPropertyFile)); try { Properties credentialProperties = new Properties(); credentialProperties.load(stream); if (credentialProperties.getProperty("accessKey") == null || credentialProperties.getProperty("secretKey") == null) { throw new IllegalArgumentException( "The specified file (" + credentialPropertyFile + ") doesn't contain the expected properties 'accessKey' " + "and 'secretKey'." ); } accessKey = credentialProperties.getProperty("accessKey"); secretAccessKey = credentialProperties.getProperty("secretKey"); } finally { try { stream.close(); } catch (final IOException e) { } } } @Override public AWSCredentials getCredentials() { if (this.accessKey == null || this.secretAccessKey == null) { try{ readProperty(); }catch(final Exception ex) { throw new RuntimeException("Failed to read credentials file", ex); } } return new BasicAWSCredentials(accessKey, secretAccessKey); } @Override public void refresh() { this.accessKey = null; } }; } else { provider = new InstanceProfileCredentialsProvider(); } return provider; } private ClientConfiguration buildClientConfig() { final ClientConfiguration configuration = new ClientConfiguration( ); configuration.setConnectionTimeout( this.clientConnectionTimeout ); configuration.setMaxConnections( this.clientMaxConnections ); return configuration; } private String buildWorkflowWorkerConfig() { return String.format("{ \"DomainRetentionPeriodInDays\": %d, \"PollThreadCount\": %d }", this.domainRetentionPeriodInDays, this.pollThreadCount); } private String buildActivityWorkerConfig() { return String.format("{ \"DomainRetentionPeriodInDays\": %d, \"PollThreadCount\": %d }", this.domainRetentionPeriodInDays, this.pollThreadCount); } private AmazonSimpleWorkflow getAWSClient() { final AWSCredentialsProvider provider = this.getCredentialsProvider(); final ClientConfiguration configuration = this.buildClientConfig(); final AmazonSimpleWorkflow client = new AmazonSimpleWorkflowClient( provider, configuration ); client.setEndpoint(this.swfEndpoint); return client; } private static void addShutdownHook(final AmazonSimpleWorkflow swfClient) { Runtime.getRuntime().addShutdownHook(new Thread( new Runnable() { public void run() { LOG.debug("Shutting down existing SWF clients"); final WorkflowClientStandalone instance = WorkflowClientStandalone.getInstance(); for (final WorkflowClient client : instance.clients) { try{ client.stop(); }catch(final InterruptedException ex) { ; } } swfClient.shutdown(); } })); } public static void main(String[] args) { final WorkflowClientStandalone instance = WorkflowClientStandalone.getInstance(); final Options opts = buildOptions(); final GnuParser cliParser = new GnuParser(); CommandLine cmd = null; try{ cmd = cliParser.parse(opts, args); }catch(final ParseException ex) { printHelp(opts, null); System.exit(1); } try{ instance.readOptions(cmd); }catch(final NumberFormatException ex) { printHelp(opts, "Some number format arguents are not recognizable"); System.exit(1); } initLogs(); LOG.debug("Starting Workflow Standalone Host"); try{ instance.discoverWorkflows(); }catch(final Exception ex) { LOG.debug("Failed to discover workflow and activities implementation"); printHelp(opts, "Failed to discover implementation classes"); System.exit(1); } try { final AmazonSimpleWorkflow swfClient = instance.getAWSClient(); addShutdownHook(swfClient); final WorkflowClient workflowClient = new WorkflowClient( instance.workflowClasses.toArray(new Class<?>[instance.workflowClasses.size()]), instance.activityClasses.toArray(new Class<?>[instance.activityClasses.size()]), false, swfClient, instance.domain, instance.taskList, instance.buildWorkflowWorkerConfig(), instance.buildActivityWorkerConfig()); workflowClient.start(); instance.clients.add(workflowClient); }catch(final Exception ex) { LOG.debug("Failed to create workflow clients", ex); System.exit(1); } do { try{ Thread.sleep(1000); }catch(final Exception ex) { } }while(true); } }