/**
* 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.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executor;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.notifications.DispatchCallback;
import org.apache.ambari.server.notifications.DispatchFactory;
import org.apache.ambari.server.notifications.Notification;
import org.apache.ambari.server.notifications.NotificationDispatcher;
import org.apache.ambari.server.orm.entities.AlertDefinitionEntity;
import org.apache.ambari.server.orm.entities.AlertHistoryEntity;
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.ambari.server.state.stack.OsFamily;
import org.easymock.EasyMock;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.easymock.PowerMock;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import junit.framework.Assert;
/**
* Tests {@link AlertScriptDispatcher}.
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest({ ProcessBuilder.class, AlertScriptDispatcher.class })
public class AlertScriptDispatcherTest {
private static final String SCRIPT_CONFIG_VALUE = "/foo/script.py";
private Injector m_injector;
@Inject
private DispatchFactory m_dispatchFactory;
@Inject
private Configuration m_configuration;
@Before
public void before() throws Exception {
m_injector = Guice.createInjector(new MockModule());
m_injector.injectMembers(this);
}
/**
* Tests that a callback error happens when the notification is not an
* {@link AlertNotification}.
*/
@Test
public void testNonAlertNotification() throws Exception {
DispatchCallback callback = EasyMock.createNiceMock(DispatchCallback.class);
Notification notification = EasyMock.createNiceMock(Notification.class);
notification.Callback = callback;
notification.CallbackIds = Collections.singletonList(UUID.randomUUID().toString());
callback.onFailure(notification.CallbackIds);
EasyMock.expectLastCall().once();
EasyMock.replay(callback, notification);
NotificationDispatcher dispatcher = m_dispatchFactory.getDispatcher(TargetType.ALERT_SCRIPT.name());
dispatcher.dispatch(notification);
EasyMock.verify(callback, notification);
}
/**
* Tests that a missing script property results in a callback failure.
*/
@Test
public void testMissingScriptConfiguration() throws Exception {
m_configuration.setProperty(AlertScriptDispatcher.SCRIPT_CONFIG_DEFAULT_KEY, null);
DispatchCallback callback = EasyMock.createNiceMock(DispatchCallback.class);
AlertNotification notification = new AlertNotification();
notification.Callback = callback;
notification.CallbackIds = Collections.singletonList(UUID.randomUUID().toString());
callback.onFailure(notification.CallbackIds);
EasyMock.expectLastCall().once();
EasyMock.replay(callback);
NotificationDispatcher dispatcher = m_dispatchFactory.getDispatcher(TargetType.ALERT_SCRIPT.name());
dispatcher.dispatch(notification);
EasyMock.verify(callback);
}
/**
* Tests a successfull invocation of the script.
*
* @throws Exception
*/
@Test
public void testProcessBuilderInvocation() throws Exception {
DispatchCallback callback = EasyMock.createNiceMock(DispatchCallback.class);
AlertNotification notification = new AlertNotification();
notification.Callback = callback;
notification.CallbackIds = Collections.singletonList(UUID.randomUUID().toString());
callback.onSuccess(notification.CallbackIds);
EasyMock.expectLastCall().once();
AlertScriptDispatcher dispatcher = (AlertScriptDispatcher) m_dispatchFactory.getDispatcher(TargetType.ALERT_SCRIPT.name());
m_injector.injectMembers(dispatcher);
ProcessBuilder powerMockProcessBuilder = m_injector.getInstance(ProcessBuilder.class);
EasyMock.expect(dispatcher.getProcessBuilder(SCRIPT_CONFIG_VALUE, notification)).andReturn(
powerMockProcessBuilder).once();
EasyMock.replay(callback, dispatcher);
dispatcher.dispatch(notification);
EasyMock.verify(callback, dispatcher);
PowerMock.verifyAll();
}
/**
* Tests that we will pickup the correct script when its specified on the
* notification.
*/
@Test
public void testCustomScriptConfiguration() throws Exception {
// remove the default value from configuration and put the customized value
final String customScriptKey = "foo.bar.key";
final String customScriptValue = "/foo/bar/foobar.py";
m_configuration.setProperty(AlertScriptDispatcher.SCRIPT_CONFIG_DEFAULT_KEY, null);
m_configuration.setProperty(customScriptKey, customScriptValue);
DispatchCallback callback = EasyMock.createNiceMock(DispatchCallback.class);
AlertNotification notification = new AlertNotification();
notification.Callback = callback;
notification.CallbackIds = Collections.singletonList(UUID.randomUUID().toString());
// put the custom config value
notification.DispatchProperties = new HashMap<>();
notification.DispatchProperties.put(AlertScriptDispatcher.DISPATCH_PROPERTY_SCRIPT_CONFIG_KEY,
customScriptKey);
callback.onSuccess(notification.CallbackIds);
EasyMock.expectLastCall().once();
AlertScriptDispatcher dispatcher = (AlertScriptDispatcher) m_dispatchFactory.getDispatcher(TargetType.ALERT_SCRIPT.name());
m_injector.injectMembers(dispatcher);
ProcessBuilder powerMockProcessBuilder = m_injector.getInstance(ProcessBuilder.class);
EasyMock.expect(dispatcher.getProcessBuilder(customScriptValue, notification)).andReturn(
powerMockProcessBuilder).once();
EasyMock.replay(callback, dispatcher);
dispatcher.dispatch(notification);
EasyMock.verify(callback, dispatcher);
PowerMock.verifyAll();
}
/**
* Tests that a process with an error code of 255 causes the failure callback
* to be invoked.
*
* @throws Exception
*/
@Test
public void testFailedProcess() throws Exception {
DispatchCallback callback = EasyMock.createNiceMock(DispatchCallback.class);
AlertNotification notification = new AlertNotification();
notification.Callback = callback;
notification.CallbackIds = Collections.singletonList(UUID.randomUUID().toString());
callback.onFailure(notification.CallbackIds);
EasyMock.expectLastCall().once();
AlertScriptDispatcher dispatcher = (AlertScriptDispatcher) m_dispatchFactory.getDispatcher(TargetType.ALERT_SCRIPT.name());
m_injector.injectMembers(dispatcher);
ProcessBuilder powerMockProcessBuilder = m_injector.getInstance(ProcessBuilder.class);
EasyMock.expect(dispatcher.getProcessBuilder(SCRIPT_CONFIG_VALUE, notification)).andReturn(
powerMockProcessBuilder).once();
Process mockProcess = powerMockProcessBuilder.start();
EasyMock.expect(mockProcess.exitValue()).andReturn(255).anyTimes();
EasyMock.replay(callback, dispatcher, mockProcess);
dispatcher.dispatch(notification);
EasyMock.verify(callback, dispatcher);
PowerMock.verifyAll();
}
/**
* Tests that arguments given to the {@link ProcessBuilder} are properly
* escaped.
*
* @throws Exception
*/
@Test
public void testArgumentEscaping() throws Exception {
final String ALERT_DEFINITION_NAME = "mock_alert_with_quotes";
final String ALERT_DEFINITION_LABEL = "Mock alert with Quotes";
final String ALERT_LABEL = "Alert Label";
final String ALERT_SERVICE_NAME = "FOO_SERVICE";
final String ALERT_TEXT = "Did you know, \"Quotes are hard!!!\"";
final String ALERT_TEXT_ESCAPED = "Did you know, \\\"Quotes are hard\\!\\!\\!\\\"";
final String ALERT_HOST = "mock_host";
final long ALERT_TIMESTAMP = 1111111l;
DispatchCallback callback = EasyMock.createNiceMock(DispatchCallback.class);
AlertNotification notification = new AlertNotification();
notification.Callback = callback;
notification.CallbackIds = Collections.singletonList(UUID.randomUUID().toString());
AlertDefinitionEntity definition = new AlertDefinitionEntity();
definition.setDefinitionName(ALERT_DEFINITION_NAME);
definition.setLabel(ALERT_DEFINITION_LABEL);
AlertHistoryEntity history = new AlertHistoryEntity();
history.setAlertDefinition(definition);
history.setAlertLabel(ALERT_LABEL);
history.setAlertText(ALERT_TEXT);
history.setAlertState(AlertState.OK);
history.setServiceName(ALERT_SERVICE_NAME);
history.setHostName(ALERT_HOST);
history.setAlertTimestamp(ALERT_TIMESTAMP);
AlertInfo alertInfo = new AlertInfo(history);
notification.setAlertInfo(alertInfo);
AlertScriptDispatcher dispatcher = new AlertScriptDispatcher();
m_injector.injectMembers(dispatcher);
ProcessBuilder processBuilder = dispatcher.getProcessBuilder(SCRIPT_CONFIG_VALUE, notification);
List<String> commands = processBuilder.command();
Assert.assertEquals(3, commands.size());
Assert.assertEquals("sh", commands.get(0));
Assert.assertEquals("-c", commands.get(1));
StringBuilder buffer = new StringBuilder();
buffer.append(SCRIPT_CONFIG_VALUE).append(" ");
buffer.append(ALERT_DEFINITION_NAME).append(" ");
buffer.append("\"").append(ALERT_DEFINITION_LABEL).append("\"").append(" ");
buffer.append(ALERT_SERVICE_NAME).append(" ");
buffer.append(AlertState.OK).append(" ");
buffer.append("\"").append(ALERT_TEXT_ESCAPED).append("\"").append(" ");
buffer.append(ALERT_TIMESTAMP).append(" ");
buffer.append(ALERT_HOST);
Assert.assertEquals(buffer.toString(), commands.get(2));
//if hostname is null
history.setHostName(null);
alertInfo = new AlertInfo(history);
notification.setAlertInfo(alertInfo);
processBuilder = dispatcher.getProcessBuilder(SCRIPT_CONFIG_VALUE, notification);
commands = processBuilder.command();
buffer = new StringBuilder();
buffer.append(SCRIPT_CONFIG_VALUE).append(" ");
buffer.append(ALERT_DEFINITION_NAME).append(" ");
buffer.append("\"").append(ALERT_DEFINITION_LABEL).append("\"").append(" ");
buffer.append(ALERT_SERVICE_NAME).append(" ");
buffer.append(AlertState.OK).append(" ");
buffer.append("\"").append(ALERT_TEXT_ESCAPED).append("\"").append(" ");
buffer.append(ALERT_TIMESTAMP).append(" ");
buffer.append("");
Assert.assertEquals(buffer.toString(), commands.get(2));
}
/**
*
*/
private class MockModule extends AbstractModule {
/**
* {@inheritDoc}
*/
@Override
protected void configure() {
try {
// populate configuration with the 1 config we need
Configuration configuration = new Configuration();
configuration.setProperty(AlertScriptDispatcher.SCRIPT_CONFIG_DEFAULT_KEY, SCRIPT_CONFIG_VALUE);
// do some basic bindings
DispatchFactory dispatchFactory = DispatchFactory.getInstance();
bind(DispatchFactory.class).toInstance(dispatchFactory);
bind(Configuration.class).toInstance(configuration);
bind(OsFamily.class).toInstance(EasyMock.createNiceMock(OsFamily.class));
// mock the dispatcher to return our doctored ProcessBuilder
AlertScriptDispatcher dispatcher = EasyMock.createMockBuilder(AlertScriptDispatcher.class).addMockedMethods(
"getProcessBuilder").createNiceMock();
// make the executor synchronized for testing
SynchronizedExecutor synchronizedExecutor = new SynchronizedExecutor();
Field field = AlertScriptDispatcher.class.getDeclaredField("m_executor");
field.setAccessible(true);
field.set(dispatcher, synchronizedExecutor);
// since we're bypassing normal bootstrapping, register the dispatcher
// manually
dispatchFactory.register(dispatcher.getType(), dispatcher);
// bind the dispatcher to force member injection
bind(AlertScriptDispatcher.class).toInstance(dispatcher);
Process processMock = EasyMock.createNiceMock(Process.class);
// use powermock since EasyMock can't mock final classes
ProcessBuilder powerMockProcessBuilder = PowerMock.createNiceMock(ProcessBuilder.class);
EasyMock.expect(powerMockProcessBuilder.start()).andReturn(processMock).atLeastOnce();
PowerMock.replay(powerMockProcessBuilder);
// bind the doctored ProcessBuilder so we can use it from anywhere
bind(ProcessBuilder.class).toInstance(powerMockProcessBuilder);
} catch (Exception exception) {
throw new RuntimeException(exception);
}
}
}
/**
* The {@link SynchronizedExecutor} is used to execute {@link Runnable}s
* serially.
*/
private static final class SynchronizedExecutor implements Executor {
/**
* @param command
*/
@Override
public void execute(Runnable command) {
command.run();
}
}
}