/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.ambari.server.notifications.dispatchers; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import org.apache.ambari.server.configuration.Configuration; import org.apache.ambari.server.notifications.Notification; import org.apache.ambari.server.notifications.NotificationDispatcher; import org.apache.ambari.server.notifications.TargetConfigurationResult; import org.apache.ambari.server.orm.entities.AlertDefinitionEntity; import org.apache.ambari.server.state.AlertState; import org.apache.ambari.server.state.alert.AlertNotification; import org.apache.ambari.server.state.alert.TargetType; import org.apache.ambari.server.state.services.AlertNoticeDispatchService.AlertInfo; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.SystemUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.escape.Escaper; import com.google.common.escape.Escapers; import com.google.inject.Inject; /** * The {@link AlertScriptDispatcher} is used to dispatch * {@link AlertNotification}s to a script via a {@link Process} and command line * arguments. This dispatcher does not know how to work with any other types of * {@link Notification}. * <p/> * This dispatcher only deals with non-digest notifications. */ public class AlertScriptDispatcher implements NotificationDispatcher { /** * The key in {@code ambari.properties} which contains the script to execute. */ public static final String SCRIPT_CONFIG_DEFAULT_KEY = "notification.dispatch.alert.script"; /** * The key in {@code ambari.properties} which contains the timeout, in * milliseconds, of any script run by this dispatcher. */ public static final String SCRIPT_CONFIG_TIMEOUT_KEY = "notification.dispatch.alert.script.timeout"; /** * A dispatch property that instructs this dispatcher to read a different key * from {@link Configuration}. If this value is not a part of the * {@link Notification#DispatchProperties} then * {@link #SCRIPT_CONFIG_DEFAULT_KEY} will be used. */ public static final String DISPATCH_PROPERTY_SCRIPT_CONFIG_KEY = "ambari.dispatch-property.script"; /** * Logger. */ private static final Logger LOG = LoggerFactory.getLogger(AlertScriptDispatcher.class); /** * Default script timeout is 5s */ private static final long DEFAULT_SCRIPT_TIMEOUT = 5000L; /** * Used to escape text being passed into the shell command. */ public static final Escaper SHELL_ESCAPE; static { final Escapers.Builder builder = Escapers.builder(); builder.addEscape('\"', "\\\""); builder.addEscape('!', "\\!"); SHELL_ESCAPE = builder.build(); } /** * Configuration data from the ambari.properties file. */ @Inject protected Configuration m_configuration; /** * The executor responsible for dispatching. */ private final Executor m_executor = new ThreadPoolExecutor(0, 1, 5L, TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>(), new ScriptDispatchThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); /** * Gets the key that will be used to lookup the script to execute from * {@link Configuration}. * * @return the key that will be used to lookup the script value from * {@link Configuration} (never {@code null}). */ public String getScriptConfigurationKey( Notification notification ) { if( null == notification || null == notification.DispatchProperties ){ return SCRIPT_CONFIG_DEFAULT_KEY; } if (null == notification.DispatchProperties.get(DISPATCH_PROPERTY_SCRIPT_CONFIG_KEY)) { return SCRIPT_CONFIG_DEFAULT_KEY; } return notification.DispatchProperties.get(DISPATCH_PROPERTY_SCRIPT_CONFIG_KEY); } /** * Gets the timeout value for the script, in milliseconds, as set in * {@link Configuration}. If there is no setting, then * {@link #DEFAULT_SCRIPT_TIMEOUT} will be returned. * * @return the timeout value in milliseconds */ public long getScriptConfigurationTimeout(){ String scriptTimeout = m_configuration.getProperty(SCRIPT_CONFIG_TIMEOUT_KEY); if (null == scriptTimeout) { return DEFAULT_SCRIPT_TIMEOUT; } return Long.parseLong(scriptTimeout); } /** * {@inheritDoc} */ @Override public final String getType() { return TargetType.ALERT_SCRIPT.name(); } /** * {@inheritDoc} * <p/> * Returns {@code false} always. */ @Override public final boolean isNotificationContentGenerationRequired() { return false; } /** * {@inheritDoc} */ @Override public void dispatch(Notification notification) { String scriptKey = getScriptConfigurationKey(notification); String script = m_configuration.getProperty(scriptKey); // this dispatcher requires a script to run if (null == script) { LOG.warn( "Unable to dispatch notification because the {} configuration property was not found", scriptKey); if (null != notification.Callback) { notification.Callback.onFailure(notification.CallbackIds); } return; } // this dispatcher can only handler alert notifications if (notification.getType() != Notification.Type.ALERT) { LOG.warn("The {} dispatcher is not able to dispatch notifications of type {}", getType(), notification.getType()); if (null != notification.Callback) { notification.Callback.onFailure(notification.CallbackIds); } return; } // execute the script asynchronously long timeout = getScriptConfigurationTimeout(); TimeUnit timeUnit = TimeUnit.MILLISECONDS; AlertNotification alertNotification = (AlertNotification) notification; ProcessBuilder processBuilder = getProcessBuilder(script, alertNotification); AlertScriptRunnable runnable = new AlertScriptRunnable(alertNotification, script, processBuilder, timeout, timeUnit); m_executor.execute(runnable); } /** * {@inheritDoc} * <p/> * Returns {@code false} always. */ @Override public final boolean isDigestSupported() { return false; } /** * {@inheritDoc} */ @Override public final TargetConfigurationResult validateTargetConfig(Map<String, Object> properties) { // there's no setup required for this dispatcher; always return valid return TargetConfigurationResult.valid(); } /** * Gets a {@link ProcessBuilder} initialized with a script command to run with * the parameters from the notification. * * @param script * the absolute path to the script (not {@code null}). * @param notification * the notification to parameterie (not {@code null}). * @return */ ProcessBuilder getProcessBuilder(String script, AlertNotification notification) { final String shellCommand; final String shellCommandOption; if (SystemUtils.IS_OS_WINDOWS) { shellCommand = "cmd"; shellCommandOption = "/c"; } else { shellCommand = "sh"; shellCommandOption = "-c"; } AlertInfo alertInfo = notification.getAlertInfo(); AlertDefinitionEntity definition = alertInfo.getAlertDefinition(); String definitionName = definition.getDefinitionName(); AlertState alertState = alertInfo.getAlertState(); String serviceName = alertInfo.getServiceName(); // these could have spaces in them, so quote them so they don't mess up the // command line String alertLabel = "\"" + SHELL_ESCAPE.escape(definition.getLabel()) + "\""; String alertText = "\"" + SHELL_ESCAPE.escape(alertInfo.getAlertText()) + "\""; long alertTimestamp = alertInfo.getAlertTimestamp(); String hostName = alertInfo.getHostName(); // null if alert do not run against host Object[] params = new Object[] { script, definitionName, alertLabel, serviceName, alertState.name(), alertText, alertTimestamp, hostName}; String foo = StringUtils.join(params, " "); // sh -c '/foo/sys_logger.py ambari_server_agent_heartbeat "Agent Heartbeat" // AMBARI CRITICAL "Something went wrong with the host" 1111111 host222' return new ProcessBuilder(shellCommand, shellCommandOption, foo); } /** * The {@link AlertScriptRunnable} is used to invoke a script with alert data * inside of an {@link Executor}. */ private final static class AlertScriptRunnable implements Runnable { /** * Logger. */ private static final Logger LOG = LoggerFactory.getLogger(AlertScriptRunnable.class); private final ProcessBuilder m_processBuilder; private final long m_timeout; private final TimeUnit m_timeoutUnits; private final Notification m_notification; private final String m_script; /** * Constructor. * * @param notification * @param script * @param processBuilder * @param timeout * @param timeoutUnits */ private AlertScriptRunnable(Notification notification, String script, ProcessBuilder processBuilder, long timeout, TimeUnit timeoutUnits) { m_notification = notification; m_script = script; m_processBuilder = processBuilder; m_timeout = timeout; m_timeoutUnits = timeoutUnits; } /** * */ @Override public void run() { boolean isDispatchSuccessful = true; try { Process process = m_processBuilder.start(); int exitCode = execute(process, m_timeout, TimeUnit.MILLISECONDS); if (exitCode != 0) { LOG.warn("Unable to dispatch {} notification because {} terminated with exit code {}", TargetType.ALERT_SCRIPT, m_script, exitCode); isDispatchSuccessful = false; } } catch (TimeoutException timeoutException) { isDispatchSuccessful = false; LOG.warn("Unable to dispatch notification with {} in under {}ms", m_script, m_timeoutUnits.toMillis(m_timeout), timeoutException); } catch (Exception exception) { isDispatchSuccessful = false; LOG.warn("Unable to dispatch notification with {}", m_script, exception); } // callback if (null != m_notification.Callback) { if (isDispatchSuccessful) { m_notification.Callback.onSuccess(m_notification.CallbackIds); } else { m_notification.Callback.onFailure(m_notification.CallbackIds); } } } /** * Executes the specified process within the supplied timeout value. If the * process terminates normally within the timeout period, then the exit code * is returned. If the process did not succeed within the specified timeout * value, then a {@link TimeoutException} is thrown. * <p/> * If the process did not finish within the specified timeout, then the * process will be destroyed via {@link Process#destroy()}. * * @param process * the process to execute (not {@code null}). * @param timeout * the timeout to apply to the process. * @param unit * the time units for the timeout * @return the exit code of the process * @throws TimeoutException * if the process did not finish within the specified time. */ public int execute(Process process, long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { long timeRemaining = unit.toMillis(timeout); long startTime = System.currentTimeMillis(); while (timeRemaining > 0) { try { return process.exitValue(); } catch (IllegalThreadStateException ex) { if (timeRemaining > 0) { Thread.sleep(Math.min(timeRemaining, 500)); } } long timeElapsed = System.currentTimeMillis() - startTime; timeRemaining = unit.toMillis(timeout) - timeElapsed; } process.destroy(); throw new TimeoutException(); } } /** * A custom {@link ThreadFactory} for the threads that will handle dispatching * to scripts. Threads created will have slightly reduced priority since * {@link AlertNotification} instances are not critical to the system. */ private static final class ScriptDispatchThreadFactory implements ThreadFactory { private static final AtomicInteger s_threadIdPool = new AtomicInteger(1); /** * {@inheritDoc} */ @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r, "script-dispatcher-" + s_threadIdPool.getAndIncrement()); thread.setDaemon(false); thread.setPriority(Thread.NORM_PRIORITY - 1); return thread; } } }