/* * RHQ Management Platform * Copyright (C) 2005-2014 Red Hat, Inc. * All rights reserved. * * 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 2 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, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package org.rhq.enterprise.server.plugins.alertCli; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.io.PrintWriter; import java.nio.charset.Charset; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import javax.script.ScriptEngine; import javax.script.ScriptException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.rhq.bindings.ScriptEngineFactory; import org.rhq.bindings.StandardBindings; import org.rhq.bindings.StandardScriptPermissions; import org.rhq.bindings.util.PackageFinder; import org.rhq.core.domain.alert.Alert; import org.rhq.core.domain.alert.notification.SenderResult; import org.rhq.core.domain.auth.Subject; import org.rhq.core.domain.configuration.PropertySimple; import org.rhq.core.domain.content.PackageVersion; import org.rhq.core.domain.content.Repo; import org.rhq.core.domain.criteria.RepoCriteria; import org.rhq.core.util.exception.ThrowableUtil; import org.rhq.enterprise.client.LocalClient; import org.rhq.enterprise.server.auth.SessionManager; import org.rhq.enterprise.server.auth.SubjectManagerLocal; import org.rhq.enterprise.server.content.ContentSourceManagerLocal; import org.rhq.enterprise.server.content.RepoManagerLocal; import org.rhq.enterprise.server.plugin.pc.alert.AlertSender; import org.rhq.enterprise.server.plugin.pc.alert.AlertSenderValidationResults; import org.rhq.enterprise.server.util.LookupUtil; import org.rhq.scripting.ScriptEngineInitializer; import org.rhq.scripting.ScriptSourceProvider; import org.rhq.scripting.ScriptSourceProviderFactory; /** * Uses CLI to perform the alert notification. * * @author Lukas Krejci */ public class CliSender extends AlertSender<CliComponent> { private static final Log LOG = LogFactory.getLog(CliSender.class); private static final int MAX_RESULT_SIZE = 4000; public static final String PROP_PACKAGE_ID = "packageId"; public static final String PROP_REPO_ID = "repoId"; public static final String PROP_USER_ID = "userId"; public static final String PROP_USER_NAME = "userName"; public static final String PROP_USER_PASSWORD = "userPassword"; private static final String SUMMARY_TEMPLATE = "Ran script $packageName in version $packageVersion from repo $repoName as user $userName."; private static final String PREVIEW_TEMPLATE = "Run script $packageName from repo $repoName as user $userName."; private static final String VALIDATION_ERROR_MESSAGE = "The provided user failed to authenticate."; //no more than 10 concurrently running CLI notifications.. //is that enough? private static final int MAX_SCRIPT_ENGINES = 10; private static final Map<String, Queue<ScriptEngine>> SCRIPT_ENGINES = new HashMap<String, Queue<ScriptEngine>>(); private static int ENGINES_IN_USE = 0; /** * Simple strongly typed representation of the alert configuration */ private static class Config { Subject subject; int packageId; int repoId; } private static class ExceptionHolder { public ScriptException scriptException; public Throwable throwable; } private static class BitsAndFileExtension { InputStream packageBits; String scriptFileExtension; } @Override public SenderResult send(Alert alert) { SenderResult result = new SenderResult(); BufferedReader reader = null; ScriptEngine engine = null; Subject subjectWithSession = null; final SessionManager sessionManager = SessionManager.getInstance(); String language = null; try { final Config config = getConfig(); // simulate the login by getting a session ID config.subject = sessionManager.put(config.subject, pluginComponent.getScriptTimeout() * 1000); subjectWithSession = config.subject; result.setSummary(createSummary(config, SUMMARY_TEMPLATE)); ByteArrayOutputStream scriptOutputStream = new ByteArrayOutputStream(); PrintWriter scriptOut = new PrintWriter(scriptOutputStream); BitsAndFileExtension packageData = getPackageBits(config.packageId, config.repoId); InputStream packageBits = packageData.packageBits; String scriptFileExtension = packageData.scriptFileExtension; language = ScriptEngineFactory.getLanguageByScriptFileExtension(scriptFileExtension); if (language == null) { ArrayList<String> supportedExtensions = new ArrayList<String>(); for (String lang : ScriptEngineFactory.getSupportedLanguages()) { supportedExtensions.add(ScriptEngineFactory.getFileExtensionForLanguage(lang)); } throw new IllegalArgumentException( "Could not determine the script engine to use based on the script file extension '" + scriptFileExtension + "'. Only the following extensions are currently supported: " + supportedExtensions); } engine = getScriptEngine(alert, scriptOut, config, language, pluginComponent.getDomainPackagesNames()); reader = new BufferedReader(new InputStreamReader(packageBits)); final BufferedReader rdr = reader; final ExceptionHolder exceptionHolder = new ExceptionHolder(); final ScriptEngine e = engine; Thread scriptRunner = new Thread(new Runnable() { @Override public void run() { try { e.eval(rdr); } catch (ScriptException e) { exceptionHolder.scriptException = e; } catch (Throwable e) { exceptionHolder.throwable = e; } } }, "Script Runner for alert " + alert); scriptRunner.setDaemon(true); scriptRunner.start(); if (pluginComponent.getScriptTimeout() <= 0) { scriptRunner.join(); } else { scriptRunner.join(pluginComponent.getScriptTimeout() * 1000); } scriptRunner.interrupt(); if (exceptionHolder.scriptException != null) { LOG.info("The script execution for CLI notification of alert [" + alert + "] failed.", exceptionHolder.scriptException); //make things pretty for the UI ScriptEngineInitializer initializer = ScriptEngineFactory.getInitializer(language); String message = initializer.extractUserFriendlyErrorMessage(exceptionHolder.scriptException); int col = exceptionHolder.scriptException.getColumnNumber(); int line = exceptionHolder.scriptException.getLineNumber(); String scriptName = createSummary(config, "script $packageName ($packageVersion) in repo $repoName"); throw new ScriptException(message, scriptName, line, col); } else if (exceptionHolder.throwable != null) { LOG.info("The script execution for CLI notification of alert [" + alert + "] failed.", exceptionHolder.throwable); throw exceptionHolder.throwable; } scriptOut.flush(); String scriptOutput = scriptOutputStream.toString(Charset.defaultCharset().name()); if (scriptOutput.length() == 0) { scriptOutput = "Script generated no output."; } if (scriptOutput.length() > remainingResultSize(result)) { scriptOutput = scriptOutput.substring(0, remainingResultSize(result)); } result.addSuccessMessage(scriptOutput); return result; } catch (IllegalArgumentException e) { return SenderResult.getSimpleFailure(e.getMessage()); //well, let's just hope the message doesn't exceed 4k. } catch (Throwable e) { result.addFailureMessage(ThrowableUtil.getAllMessages(e, true, remainingResultSize(result))); return result; } finally { if (subjectWithSession != null) { sessionManager.invalidate(subjectWithSession.getSessionId()); } if (engine != null) { returnEngine(engine, language); } if (reader != null) { try { reader.close(); } catch (IOException e) { LOG.error("Failed to close the script reader.", e); } } } } @Override public String previewConfiguration() { try { Config c = getConfig(); return createSummary(c, PREVIEW_TEMPLATE); } catch (Exception e) { LOG.warn("Failed to get the configuration preview.", e); return "Failed to get configuration preview: " + e.getMessage(); } } @Override public AlertSenderValidationResults validateAndFinalizeConfiguration(Subject subject) { AlertSenderValidationResults results = new AlertSenderValidationResults(alertParameters, extraParameters); String userIdString = alertParameters.getSimpleValue(PROP_USER_ID, null); String userName = alertParameters.getSimpleValue(PROP_USER_NAME, null); String userPassword = alertParameters.getSimpleValue(PROP_USER_PASSWORD, null); Integer userId = userIdString == null ? null : Integer.valueOf(userIdString); if (userId == null || userId != subject.getId()) { SubjectManagerLocal subjectManager = LookupUtil.getSubjectManager(); Subject authSubject = subjectManager.checkAuthentication(userName, userPassword); if (authSubject == null) { PropertySimple userNameProp = new PropertySimple(PROP_USER_NAME, userName); userNameProp.setErrorMessage(VALIDATION_ERROR_MESSAGE); alertParameters.put(userNameProp); alertParameters.put(new PropertySimple(PROP_USER_ID, null)); } else { //make sure we store the id of the user that actually authenticated to prevent //security breaches. alertParameters.put(new PropertySimple(PROP_USER_ID, authSubject.getId())); } } else { //make sure to store the username of the user... not that it is functionally //required but prevent confusions in case of debugging some errorneous situation alertParameters.put(new PropertySimple(PROP_USER_NAME, subject.getName())); } //do not store the password in the database ever alertParameters.put(new PropertySimple(PROP_USER_PASSWORD, null)); return results; } private static ScriptEngine getScriptEngine(Alert alert, PrintWriter output, Config config, String language, Set<String> domainPackagesNames) throws ScriptException, IOException, InterruptedException { Subject user = config.subject; LocalClient client = new LocalClient(user); StandardBindings bindings = new StandardBindings(output, client); bindings.put("alert", alert); ScriptEngine engine = takeEngine(bindings, language, domainPackagesNames); engine.getContext().setWriter(output); engine.getContext().setErrorWriter(output); return engine; } private static BitsAndFileExtension getPackageBits(int packageId, int repoId) throws IOException { final ContentSourceManagerLocal csm = LookupUtil.getContentSourceManager(); RepoManagerLocal rm = LookupUtil.getRepoManagerLocal(); final PackageVersion versionToUse = rm.getLatestPackageVersion(LookupUtil.getSubjectManager().getOverlord(), packageId, repoId); if (versionToUse == null) { throw new IllegalArgumentException( "The package with id " + packageId + " either doesn't exist at all or doesn't have any version. Can't execute a CLI script without a script to run."); } PipedInputStream bits = new PipedInputStream(); final PipedOutputStream out = new PipedOutputStream(bits); Thread reader = new Thread(new Runnable() { @Override public void run() { try { csm.outputPackageVersionBits(versionToUse, out); } catch (RuntimeException e) { LOG.warn("The thread for reading the bits of package version [" + versionToUse + "] failed with exception.", e); throw e; } finally { try { out.close(); } catch (IOException e) { //doesn't happen in piped output stream LOG.error( "Failed to close the piped output stream receiving the package bits of package version " + versionToUse + ". This should never happen.", e); } } } }); reader.setName("CLI Alert download thread for package version " + versionToUse); reader.setDaemon(true); reader.start(); BitsAndFileExtension ret = new BitsAndFileExtension(); ret.packageBits = bits; String fileName = versionToUse.getFileName(); String extension = ""; if (fileName != null) { int dotIdx = fileName.lastIndexOf('.'); if (dotIdx >= 0) { extension = fileName.substring(dotIdx + 1); } } ret.scriptFileExtension = extension; return ret; } /** * Possible replacements are: * <ul> * <li><code>$userName</code> * <li><code>$packageName</code> * <li><code>$packageVersion</code> * <li><code>$repoName</code> * </ul> * @param config * @param template * @return */ private static String createSummary(Config config, String template) { try { String ret = template; ret = ret.replace("$userName", config.subject.getName()); //now get the package and repo info Subject overlord = LookupUtil.getSubjectManager().getOverlord(); RepoManagerLocal rm = LookupUtil.getRepoManagerLocal(); PackageVersion versionToUse = rm.getLatestPackageVersion(overlord, config.packageId, config.repoId); if (versionToUse != null) { ret = ret.replace("$packageName", versionToUse.getDisplayName()); ret = ret.replace( "$packageVersion", versionToUse.getDisplayVersion() == null ? versionToUse.getVersion() : versionToUse .getDisplayVersion()); } else { ret = ret.replace("$packageName", "unknown script with package id " + config.packageId); ret = ret.replace("$packageVersion", "no version"); } RepoCriteria criteria = new RepoCriteria(); criteria.addFilterId(config.repoId); criteria.clearPaging();//disable paging as the code assumes all the results will be returned. List<Repo> repos = rm.findReposByCriteria(overlord, criteria); String repoName; if (repos.size() > 0) { repoName = repos.get(0).getName(); } else { repoName = "unknown repo with id " + config.repoId; } ret = ret.replace("$repoName", repoName); return ret; } catch (Exception e) { LOG.info("Failed to create alert sender summary.", e); return "Failed to create summary: " + e.getMessage(); } } private Config getConfig() throws IllegalArgumentException { Config ret = new Config(); int subjectId = getIntFromConfiguration(PROP_USER_ID, "User id not specified.", "Failed to read subject id property: "); int packageId = getIntFromConfiguration(PROP_PACKAGE_ID, "Package id of the script not specified.", "Failed to read the package id property: "); int repoId = getIntFromConfiguration(PROP_REPO_ID, "Repo to download the script package from not specified.", "Failed to read the repo id property: "); Subject subject = LookupUtil.getSubjectManager().getSubjectById(subjectId); if (subject == null) { throw new IllegalArgumentException("User with id " + subjectId + " doesn't exist anymore."); } ret.subject = subject; ret.packageId = packageId; ret.repoId = repoId; return ret; } private int getIntFromConfiguration(String propName, String errorMessage, String convertErrorMessage) throws IllegalArgumentException { PropertySimple prop = alertParameters.getSimple(propName); if (prop == null) { throw new IllegalArgumentException(errorMessage); } Integer integerValue; try { integerValue = prop.getIntegerValue(); } catch (Exception e) { throw new IllegalArgumentException(convertErrorMessage + e.getMessage(), e); } if (integerValue == null) { throw new IllegalArgumentException(errorMessage); } return integerValue; } private static ScriptEngine takeEngine(StandardBindings bindings, String language, Set<String> domainPackagesNames) throws InterruptedException, ScriptException, IOException { synchronized (SCRIPT_ENGINES) { if (ENGINES_IN_USE >= MAX_SCRIPT_ENGINES) { SCRIPT_ENGINES.wait(); } Queue<ScriptEngine> q = SCRIPT_ENGINES.get(language); if (q == null) { q = new ArrayDeque<ScriptEngine>(); SCRIPT_ENGINES.put(language, q); } ScriptEngine engine = q.poll(); if (engine == null) { engine = ScriptEngineFactory.getSecuredScriptEngine(language, new CliSenderPackageFinder( domainPackagesNames), bindings, new StandardScriptPermissions()); } //TODO is this OK, or should we use a different classloader than the context classloader? ScriptSourceProvider[] providers = ScriptSourceProviderFactory.get(null); ScriptEngineFactory.injectStandardBindings(engine, bindings, true, providers); ++ENGINES_IN_USE; return engine; } } private static void returnEngine(ScriptEngine engine, String language) { synchronized (SCRIPT_ENGINES) { Queue<ScriptEngine> q = SCRIPT_ENGINES.get(language); if (q == null) { //hmm... this is very strange and should not happen, because we should have initied the queue in the //takeEngine() method... q = new ArrayDeque<ScriptEngine>(); SCRIPT_ENGINES.put(language, q); } q.offer(engine); --ENGINES_IN_USE; SCRIPT_ENGINES.notify(); } } private static int remainingResultSize(SenderResult r) { //the "10" is a ballpark to allow for some formatting //done by the receivers of the SenderResult. int ret = MAX_RESULT_SIZE - r.getSummary().length() - 10; for (String m : r.getSuccessMessages()) { ret -= m.length() + 10; } for (String m : r.getFailureMessages()) { ret -= m.length() + 10; } return ret; } private static class CliSenderPackageFinder extends PackageFinder { final Set<String> domainPackagesNames; CliSenderPackageFinder(Set<String> domainPackagesNames) { super(Collections.<File> emptyList()); this.domainPackagesNames = domainPackagesNames; } @Override public Set<String> findPackages(String packageRoot) throws IOException { return domainPackagesNames; } } }