/*************************************************************************
* Copyright 2009-2015 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.
*
* This file may incorporate work covered under the following copyright
* and permission notice:
*
* Copyright 2010-2014 Amazon.com, Inc. or its affiliates. All Rights
* Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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 com.eucalyptus.cloudwatch.common.internal.domain.alarms;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import com.eucalyptus.cloudwatch.common.internal.domain.InvalidTokenException;
import com.eucalyptus.entities.TransactionResource;
import com.eucalyptus.util.async.CheckedListenableFuture;
import com.eucalyptus.util.async.Futures;
import net.sf.json.JSONObject;
import org.apache.log4j.Logger;
import org.hibernate.Criteria;
import org.hibernate.criterion.Junction;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import com.eucalyptus.auth.principal.AccountFullName;
import com.eucalyptus.autoscaling.common.AutoScaling;
import com.eucalyptus.autoscaling.common.msgs.AutoScalingMessage;
import com.eucalyptus.autoscaling.common.msgs.ExecutePolicyType;
import com.eucalyptus.cloudwatch.common.CloudWatchMetadata;
import com.eucalyptus.cloudwatch.common.CloudWatchResourceName;
import com.eucalyptus.cloudwatch.common.internal.domain.DimensionEntity;
import com.eucalyptus.cloudwatch.common.internal.domain.NextTokenUtils;
import com.eucalyptus.cloudwatch.common.internal.domain.alarms.AlarmEntity.ComparisonOperator;
import com.eucalyptus.cloudwatch.common.internal.domain.alarms.AlarmEntity.StateValue;
import com.eucalyptus.cloudwatch.common.internal.domain.alarms.AlarmEntity.Statistic;
import com.eucalyptus.cloudwatch.common.internal.domain.alarms.AlarmHistory.HistoryItemType;
import com.eucalyptus.cloudwatch.common.internal.domain.metricdata.MetricEntity.MetricType;
import com.eucalyptus.cloudwatch.common.internal.domain.metricdata.Units;
import com.eucalyptus.component.id.Eucalyptus;
import com.eucalyptus.compute.common.ComputeMessage;
import com.eucalyptus.compute.common.backend.StopInstancesType;
import com.eucalyptus.compute.common.backend.TerminateInstancesType;
import com.eucalyptus.crypto.util.Timestamps;
import com.eucalyptus.entities.Entities;
import com.eucalyptus.util.CollectionUtils;
import com.eucalyptus.util.Callback;
import com.eucalyptus.util.DispatchingClient;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
public class AlarmManager {
private static final Logger LOG = Logger.getLogger(AlarmManager.class);
public static Long countMetricAlarms(String accountId) {
try (final TransactionResource db = Entities.transactionFor(AlarmEntity.class)) {
Criteria criteria = Entities.createCriteria(AlarmEntity.class);
criteria = criteria.setProjection(Projections.rowCount());
if (accountId != null) {
criteria = criteria.add( Restrictions.eq( "accountId" , accountId ) );
}
return (Long) criteria.uniqueResult();
}
}
public static void putMetricAlarm(String accountId, Boolean actionsEnabled,
Collection<String> alarmActions, String alarmDescription,
String alarmName, ComparisonOperator comparisonOperator,
Map<String, String> dimensionMap, Integer evaluationPeriods,
Collection<String> insufficientDataActions, String metricName,
MetricType metricType, String namespace, Collection<String> okActions,
Integer period, Statistic statistic, Double threshold, Units unit) {
if (dimensionMap == null) {
dimensionMap = Maps.newHashMap();
} else if (dimensionMap.size() > AlarmEntity.MAX_DIM_NUM) {
throw new IllegalArgumentException("Too many dimensions for metric, " + dimensionMap.size());
}
AlarmEntity alarmEntity = new AlarmEntity();
alarmEntity.setAccountId(accountId);
alarmEntity.setAlarmName(alarmName);
try (final TransactionResource db = Entities.transactionFor(AlarmEntity.class)) {
boolean inDb = false;
Criteria criteria = Entities.createCriteria(AlarmEntity.class)
.add( Restrictions.eq( "accountId" , accountId ) )
.add( Restrictions.eq( "alarmName" , alarmName ) );
AlarmEntity inDbAlarm = (AlarmEntity) criteria.uniqueResult();
if (inDbAlarm != null) {
inDb = true;
alarmEntity = inDbAlarm;
}
alarmEntity.setActionsEnabled(actionsEnabled);
alarmEntity.setAlarmActions(alarmActions);
alarmEntity.setAlarmDescription(alarmDescription);
alarmEntity.setComparisonOperator(comparisonOperator);
TreeSet<DimensionEntity> dimensions = Sets.newTreeSet();
for (Map.Entry<String,String> entry: dimensionMap.entrySet()) {
DimensionEntity d = new DimensionEntity();
d.setName(entry.getKey());
d.setValue(entry.getValue());
dimensions.add(d);
}
alarmEntity.setDimensions(dimensions);
alarmEntity.setEvaluationPeriods(evaluationPeriods);
alarmEntity.setInsufficientDataActions(insufficientDataActions);
alarmEntity.setMetricName(metricName);
alarmEntity.setMetricType(metricType);
alarmEntity.setNamespace(namespace);
alarmEntity.setOkActions(okActions);
alarmEntity.setPeriod(period);
alarmEntity.setStatistic(statistic);
alarmEntity.setThreshold(threshold);
alarmEntity.setUnit(unit);
Date now = new Date();
alarmEntity.setAlarmConfigurationUpdatedTimestamp(now);
if (!inDb) {
alarmEntity.setStateValue(StateValue.INSUFFICIENT_DATA);
alarmEntity.setStateReason("Unchecked: Initial alarm creation");
alarmEntity.setStateUpdatedTimestamp(now);
// TODO: revisit (we are not firing actions on alarm creation, but may after one period)
alarmEntity.setLastActionsUpdatedTimestamp(now);
JSONObject historyDataJSON = new JSONObject();
historyDataJSON.element("version", "1.0");
historyDataJSON.element("type", "Create");
JSONObject historyDataDeletedAlarmJSON = getJSONObjectFromAlarmEntity(alarmEntity);
historyDataJSON.element("createdAlarm", historyDataDeletedAlarmJSON);
String historyData = historyDataJSON.toString();
AlarmManager.addAlarmHistoryItem(accountId, alarmName, historyData,
HistoryItemType.ConfigurationUpdate, "Alarm \"" + alarmName + "\" created", now);
Entities.persist(alarmEntity);
} else {
JSONObject historyDataJSON = new JSONObject();
historyDataJSON.element("version", "1.0");
historyDataJSON.element("type", "Update");
JSONObject historyDataDeletedAlarmJSON = getJSONObjectFromAlarmEntity(alarmEntity);
historyDataJSON.element("updatedAlarm", historyDataDeletedAlarmJSON);
String historyData = historyDataJSON.toString();
AlarmManager.addAlarmHistoryItem(accountId, alarmName, historyData,
HistoryItemType.ConfigurationUpdate, "Alarm \"" + alarmName + "\" updated", now);
}
db.commit();
}
}
static void addAlarmHistoryItem(String accountId, String alarmName,
String historyData, HistoryItemType historyItemType, String historySummary,
Date now) {
if (now == null) now = new Date();
try (final TransactionResource db = Entities.transactionFor(AlarmHistory.class)) {
AlarmHistory alarmHistory = createAlarmHistoryItem(accountId, alarmName, historyData, historyItemType, historySummary, now);
Entities.persist(alarmHistory);
db.commit();
}
}
public static AlarmHistory createAlarmHistoryItem(String accountId, String alarmName, String historyData, HistoryItemType historyItemType, String historySummary, Date now) {
AlarmHistory alarmHistory = new AlarmHistory();
alarmHistory.setAccountId(accountId);
alarmHistory.setAlarmName(alarmName);
alarmHistory.setHistoryData(historyData);
alarmHistory.setHistoryItemType(historyItemType);
alarmHistory.setHistorySummary(historySummary);
alarmHistory.setTimestamp(now);
return alarmHistory;
}
public static boolean deleteAlarms(
final String accountId,
final Collection<String> alarmNames,
final Predicate<CloudWatchMetadata.AlarmMetadata> filter
) {
return modifySelectedAlarms( accountId, alarmNames, filter, new Predicate<AlarmEntity>() {
private final Date now = new Date();
@Override
public boolean apply( final AlarmEntity alarmEntity ) {
final String alarmName = alarmEntity.getAlarmName();
JSONObject historyDataJSON = new JSONObject();
historyDataJSON.element( "version", "1.0" );
historyDataJSON.element( "type", "Delete" );
JSONObject historyDataDeletedAlarmJSON = getJSONObjectFromAlarmEntity( alarmEntity );
historyDataJSON.element( "deletedAlarm", historyDataDeletedAlarmJSON );
String historyData = historyDataJSON.toString();
AlarmManager.addAlarmHistoryItem( alarmEntity.getAccountId(), alarmName, historyData,
HistoryItemType.ConfigurationUpdate, "Alarm \"" + alarmName + "\" deleted", now );
Entities.delete( alarmEntity );
return true;
}
} );
}
private static JSONObject getJSONObjectFromAlarmEntity(AlarmEntity alarmEntity) {
JSONObject jsonObject = new JSONObject();
jsonObject.element("threshold", alarmEntity.getThreshold());
jsonObject.element("namespace", alarmEntity.getNamespace());
jsonObject.element("stateValue", alarmEntity.getStateValue().toString());
ArrayList<JSONObject> dimensions = new ArrayList<JSONObject>();
if (alarmEntity.getDimensions() != null) {
for (DimensionEntity dimensionEntity: alarmEntity.getDimensions()) {
JSONObject dimension = new JSONObject();
dimension.element("name", dimensionEntity.getName());
dimension.element("value", dimensionEntity.getValue());
dimensions.add(dimension);
}
}
jsonObject.element("dimensions", dimensions);
jsonObject.element("okactions", alarmEntity.getOkActions() != null ? alarmEntity.getOkActions() : new ArrayList<String>());
jsonObject.element("alarmActions", alarmEntity.getAlarmActions() != null ? alarmEntity.getAlarmActions() : new ArrayList<String>());
jsonObject.element("evaluationPeriods", alarmEntity.getEvaluationPeriods());
jsonObject.element("comparisonOperator", alarmEntity.getComparisonOperator().toString());
jsonObject.element("metricName", alarmEntity.getMetricName());
jsonObject.element("period", alarmEntity.getPeriod());
jsonObject.element("alarmName", alarmEntity.getAlarmName());
jsonObject.element("insufficientDataActions", alarmEntity.getInsufficientDataActions() != null ? alarmEntity.getInsufficientDataActions() : new ArrayList<String>());
jsonObject.element("actionsEnabled", alarmEntity.getActionsEnabled());
jsonObject.element("alarmDescription", alarmEntity.getAlarmDescription());
jsonObject.element("statistic", alarmEntity.getStatistic());
jsonObject.element("alarmArn", alarmEntity.getResourceName());
jsonObject.element("alarmConfigurationUpdatedTimestamp", Timestamps.formatIso8601UTCLongDateMillisTimezone(alarmEntity.getAlarmConfigurationUpdatedTimestamp()));
jsonObject.element("stateUpdatedTimestamp", Timestamps.formatIso8601UTCLongDateMillisTimezone(alarmEntity.getStateUpdatedTimestamp()));
return jsonObject;
}
/**
* @return False if enable rejected due to filter
*/
public static boolean enableAlarmActions(
final String accountId,
final Collection<String> alarmNames,
final Predicate<CloudWatchMetadata.AlarmMetadata> filter
) {
return modifySelectedAlarms( accountId, alarmNames, filter, new Predicate<AlarmEntity>() {
private final Date now = new Date();
@Override
public boolean apply( final AlarmEntity alarmEntity ) {
final String alarmName = alarmEntity.getAlarmName();
if ( !Boolean.TRUE.equals( alarmEntity.getActionsEnabled() ) ) {
JSONObject historyDataJSON = new JSONObject();
historyDataJSON.element( "version", "1.0" );
historyDataJSON.element( "type", "Update" );
JSONObject historyDataDeletedAlarmJSON = getJSONObjectFromAlarmEntity( alarmEntity );
historyDataJSON.element( "updatedAlarm", historyDataDeletedAlarmJSON );
String historyData = historyDataJSON.toString();
AlarmManager.addAlarmHistoryItem( alarmEntity.getAccountId(), alarmName, historyData,
HistoryItemType.ConfigurationUpdate, "Alarm \"" + alarmName + "\" updated", now );
alarmEntity.setActionsEnabled( Boolean.TRUE );
}
return true;
}
} );
}
/**
* @return False if disable rejected due to filter
*/
public static boolean disableAlarmActions(
final String accountId,
final Collection<String> alarmNames,
final Predicate<CloudWatchMetadata.AlarmMetadata> filter
) {
return modifySelectedAlarms( accountId, alarmNames, filter, new Predicate<AlarmEntity>() {
private final Date now = new Date();
@Override
public boolean apply( final AlarmEntity alarmEntity ) {
final String alarmName = alarmEntity.getAlarmName();
if ( !Boolean.FALSE.equals( alarmEntity.getActionsEnabled() ) ) {
JSONObject historyDataJSON = new JSONObject();
historyDataJSON.element( "version", "1.0" );
historyDataJSON.element( "type", "Update" );
JSONObject historyDataDeletedAlarmJSON = getJSONObjectFromAlarmEntity( alarmEntity );
historyDataJSON.element( "updatedAlarm", historyDataDeletedAlarmJSON );
String historyData = historyDataJSON.toString();
AlarmManager.addAlarmHistoryItem( alarmEntity.getAccountId(), alarmName, historyData,
HistoryItemType.ConfigurationUpdate, "Alarm \"" + alarmName + "\" updated", now );
alarmEntity.setActionsEnabled( Boolean.FALSE );
}
return true;
}
} );
}
private static boolean modifySelectedAlarms(
final String accountId,
final Collection<String> alarmNames,
final Predicate<CloudWatchMetadata.AlarmMetadata> filter,
final Predicate<AlarmEntity> update
) {
final Map<String,Collection<String>> accountToNamesMap =
buildAccountIdToAlarmNamesMap( accountId, alarmNames );
try (final TransactionResource db = Entities.transactionFor(AlarmEntity.class)) {
final Criteria criteria = Entities.createCriteria(AlarmEntity.class);
final Junction disjunction = Restrictions.disjunction();
for ( final Map.Entry<String,Collection<String>> entry : accountToNamesMap.entrySet( ) ) {
final Junction conjunction = Restrictions.conjunction();
conjunction.add( Restrictions.eq( "accountId", entry.getKey() ) );
conjunction.add( Restrictions.in( "alarmName", entry.getValue() ) );
disjunction.add( conjunction );
}
criteria.add( disjunction );
criteria.addOrder( Order.asc( "creationTimestamp" ) );
criteria.addOrder( Order.asc( "naturalId" ) );
final Collection<AlarmEntity> alarmEntities = (Collection<AlarmEntity>) criteria.list();
if ( !Iterables.all( alarmEntities, filter ) ) {
return false;
}
CollectionUtils.each( alarmEntities, update );
db.commit();
return true;
}
}
private static Map<String,Collection<String>> buildAccountIdToAlarmNamesMap(
@Nullable final String accountId,
@Nullable final Collection<String> alarmNames
) {
final Multimap<String,String> alarmNamesMultimap = HashMultimap.create( );
if ( alarmNames != null ) {
if ( accountId != null ) {
alarmNamesMultimap.putAll( accountId, alarmNames ); // An ARN is also a valid name
}
CollectionUtils.putAll(
Optional.presentInstances( Iterables.transform(
alarmNames,
CloudWatchResourceName.asArnOfType( CloudWatchResourceName.Type.alarm ) ) ),
alarmNamesMultimap,
CloudWatchResourceName.toNamespace( ),
CloudWatchResourceName.toName( )
);
}
return alarmNamesMultimap.asMap();
}
public static void setAlarmState(
final String accountId,
final String alarmName,
final String stateReason,
final String stateReasonData,
final StateValue stateValue,
final Predicate<CloudWatchMetadata.AlarmMetadata> filter
) throws AlarmNotFoundException {
try (final TransactionResource db = Entities.transactionFor(AlarmEntity.class)) {
AlarmEntity alarmEntity = (AlarmEntity) Entities.createCriteria( AlarmEntity.class )
.add( Restrictions.eq( "accountId" , accountId ) )
.add( Restrictions.eq( "alarmName" , alarmName ) )
.uniqueResult();
if ( alarmEntity == null && CloudWatchResourceName.isResourceName().apply( alarmName ) ) try {
final CloudWatchResourceName arn =
CloudWatchResourceName.parse( alarmName, CloudWatchResourceName.Type.alarm );
alarmEntity = (AlarmEntity) Entities.createCriteria(AlarmEntity.class)
.add( Restrictions.eq( "accountId", arn.getNamespace() ) )
.add( Restrictions.eq( "alarmName", arn.getName() ) )
.uniqueResult();
} catch ( CloudWatchResourceName.InvalidResourceNameException e ) {
}
if ( alarmEntity == null || !filter.apply( alarmEntity ) ) {
throw new AlarmNotFoundException("Could not find alarm with name '" + alarmName + "'");
}
StateValue oldStateValue = alarmEntity.getStateValue();
if (stateValue != oldStateValue) {
Date evaluationDate = new Date();
AlarmState newState = createAlarmState(stateValue, stateReason, stateReasonData);
AlarmManager.changeAlarmState(alarmEntity, newState, evaluationDate);
AlarmManager.executeActions(alarmEntity, newState, true, evaluationDate);
}
db.commit();
}
}
public static List<AlarmEntity> describeAlarms(
@Nullable final String accountId,
@Nullable final String actionPrefix,
@Nullable final String alarmNamePrefix,
@Nullable final Collection<String> alarmNames,
@Nullable final Integer maxRecords,
@Nullable final StateValue stateValue,
@Nullable final String nextToken,
final Predicate<? super CloudWatchMetadata.AlarmMetadata> filter
) throws InvalidTokenException {
final List<AlarmEntity> results = Lists.newArrayList();
try (final TransactionResource db = Entities.transactionFor(AlarmEntity.class)) {
boolean first = true;
String token = nextToken;
while ( token != null || first ) {
first = false;
final Date nextTokenCreatedTime = NextTokenUtils.getNextTokenCreatedTime(token, AlarmEntity.class);
final Criteria criteria = Entities.createCriteria(AlarmEntity.class);
if (accountId != null) {
criteria.add( Restrictions.eq( "accountId" , accountId ) );
}
if (actionPrefix != null) {
final Junction actionsOf = Restrictions.disjunction();
for (int i=1; i<= AlarmEntity.MAX_OK_ACTIONS_NUM; i++) {
actionsOf.add( Restrictions.like( "okAction" + i, actionPrefix + "%" ) ); // May need Restrictions.ilike for case insensitive
}
for (int i=1; i<= AlarmEntity.MAX_ALARM_ACTIONS_NUM; i++) {
actionsOf.add( Restrictions.like( "alarmAction" + i, actionPrefix + "%" ) ); // May need Restrictions.ilike for case insensitive
}
for (int i=1; i<= AlarmEntity.MAX_INSUFFICIENT_DATA_ACTIONS_NUM; i++) {
actionsOf.add( Restrictions.like( "insufficientDataAction" + i, actionPrefix + "%" ) ); // May need Restrictions.ilike for case insensitive
}
criteria.add( actionsOf );
}
if (alarmNamePrefix != null) {
criteria.add( Restrictions.like( "alarmName" , alarmNamePrefix + "%" ) );
}
if (alarmNames != null && !alarmNames.isEmpty()) {
criteria.add( Restrictions.in( "alarmName", alarmNames ) );
}
if (stateValue != null) {
criteria.add( Restrictions.eq( "stateValue" , stateValue ) );
}
NextTokenUtils.addNextTokenConstraints(
maxRecords == null ? null : maxRecords - results.size( ), token, nextTokenCreatedTime, criteria);
final List<AlarmEntity> alarmEntities = (List<AlarmEntity>) criteria.list();
Iterables.addAll( results, Iterables.filter( alarmEntities, filter ) );
token = maxRecords==null || ( maxRecords!=null && ( results.size() >= maxRecords || alarmEntities.size() < maxRecords ) ) ?
null :
alarmEntities.get(alarmEntities.size() - 1).getNaturalId();
}
db.commit();
}
return results;
}
public static Collection<AlarmEntity> describeAlarmsForMetric(
@Nullable final String accountId,
@Nonnull final Map<String, String> dimensionMap,
@Nullable final String metricName,
@Nullable final String namespace,
@Nullable final Integer period,
@Nullable final Statistic statistic,
@Nullable final Units unit,
@Nonnull final Predicate<? super CloudWatchMetadata.AlarmMetadata> filter
) {
final List<AlarmEntity> results = Lists.newArrayList();
try (final TransactionResource db = Entities.transactionFor(AlarmEntity.class)) {
final Criteria criteria = Entities.createCriteria(AlarmEntity.class);
if (accountId != null) {
criteria.add( Restrictions.eq( "accountId" , accountId ) );
}
final Set<DimensionEntity> dimensions = Sets.newTreeSet( );
for ( final Map.Entry<String,String> entry: dimensionMap.entrySet( ) ) {
final DimensionEntity d = new DimensionEntity();
d.setName(entry.getKey());
d.setValue(entry.getValue());
dimensions.add(d);
}
int dimIndex = 1;
for (final DimensionEntity d: dimensions) {
criteria.add( Restrictions.eq( "dim" + dimIndex + "Name", d.getName() ) );
criteria.add( Restrictions.eq( "dim" + dimIndex + "Value", d.getValue() ) );
dimIndex++;
}
while (dimIndex <= AlarmEntity.MAX_DIM_NUM) {
criteria.add( Restrictions.isNull( "dim" + dimIndex + "Name") );
criteria.add( Restrictions.isNull( "dim" + dimIndex + "Value") );
dimIndex++;
}
if (metricName != null) {
criteria.add( Restrictions.eq( "metricName" , metricName ) );
}
if (namespace != null) {
criteria.add( Restrictions.eq( "namespace" , namespace ) );
}
if (period != null) {
criteria.add( Restrictions.eq( "period" , period ) );
}
if (statistic != null) {
criteria.add( Restrictions.eq( "statistic" , statistic ) );
}
if (unit != null) {
criteria.add( Restrictions.eq( "unit" , unit ) );
}
final List<AlarmEntity> alarmEntities = (List<AlarmEntity>) criteria.list();
Iterables.addAll( results, Iterables.filter( alarmEntities, filter ) );
db.commit();
}
return results;
}
public static List<AlarmHistory> describeAlarmHistory(
@Nullable final String accountId,
@Nullable final String alarmName,
@Nullable final Date endDate,
@Nullable final HistoryItemType historyItemType,
@Nullable final Integer maxRecords,
@Nullable final Date startDate,
@Nullable final String nextToken,
final Predicate<AlarmHistory> filter ) throws InvalidTokenException {
final List<AlarmHistory> results = Lists.newArrayList();
try (final TransactionResource db = Entities.transactionFor(AlarmHistory.class)) {
final Map<String,Collection<String>> accountToNamesMap = alarmName == null ?
Collections.<String,Collection<String>>emptyMap( ) :
buildAccountIdToAlarmNamesMap( accountId, Collections.singleton( alarmName ) );
boolean first = true;
String token = nextToken;
while ( token != null || first ) {
first = false;
final Date nextTokenCreatedTime = NextTokenUtils.getNextTokenCreatedTime(token, AlarmHistory.class);
final Criteria criteria = Entities.createCriteria(AlarmHistory.class);
final Junction disjunction = Restrictions.disjunction();
for ( final Map.Entry<String,Collection<String>> entry : accountToNamesMap.entrySet( ) ) {
final Junction conjunction = Restrictions.conjunction();
conjunction.add( Restrictions.eq( "accountId", entry.getKey() ) );
conjunction.add( Restrictions.in( "alarmName", entry.getValue() ) );
disjunction.add( conjunction );
}
criteria.add( disjunction );
if (historyItemType != null) {
criteria.add( Restrictions.eq( "historyItemType" , historyItemType ) );
}
if (startDate != null) {
criteria.add( Restrictions.ge( "timestamp" , startDate ) );
}
if (endDate != null) {
criteria.add( Restrictions.le( "timestamp" , endDate ) );
}
NextTokenUtils.addNextTokenConstraints(
maxRecords == null ? null : maxRecords - results.size( ), token, nextTokenCreatedTime, criteria);
final List<AlarmHistory> alarmHistoryEntities = (List<AlarmHistory>) criteria.list();
Iterables.addAll( results, Iterables.filter( alarmHistoryEntities, filter ) );
token = maxRecords==null || ( maxRecords!=null && ( results.size() >= maxRecords || alarmHistoryEntities.size() < maxRecords ) ) ?
null :
alarmHistoryEntities.get(alarmHistoryEntities.size() - 1).getNaturalId();
}
db.commit();
}
return results;
}
/**
* Delete all alarm history before a certain date
* @param before the date to delete before (inclusive)
*/
public static void deleteAlarmHistory(Date before) {
try (final TransactionResource db = Entities.transactionFor(AlarmHistory.class)) {
Map<String, Date> criteria = Maps.newHashMap();
criteria.put("before", before);
Entities.deleteAllMatching(AlarmHistory.class, "WHERE timestamp < :before", criteria);
db.commit();
}
}
public static void changeAlarmState(AlarmEntity alarmEntity, AlarmState newState, Date now) {
LOG.debug("Updating alarm " + alarmEntity.getAlarmName() + " from " + alarmEntity.getStateValue() + " to " + newState.getStateValue());
alarmEntity.setStateUpdatedTimestamp(now);
JSONObject historyDataJSON = getJSONObjectForStateChange(alarmEntity, newState);
String historyData = historyDataJSON.toString();
AlarmManager.addAlarmHistoryItem(alarmEntity.getAccountId(), alarmEntity.getAlarmName(), historyData,
HistoryItemType.StateUpdate, " Alarm updated from " + alarmEntity.getStateValue() + " to " + newState.getStateValue(), now);
alarmEntity.setStateReason(newState.getStateReason());
alarmEntity.setStateReasonData(newState.getStateReasonData());
alarmEntity.setStateValue(newState.getStateValue());
alarmEntity.setStateUpdatedTimestamp(now);
}
private static JSONObject getJSONObjectForStateChange(AlarmEntity alarmEntity, AlarmState newState) {
JSONObject historyDataJSON = new JSONObject();
historyDataJSON.element("version", "1.0");
historyDataJSON.element("oldState", getJSONObjectFromState(alarmEntity.getStateValue(), alarmEntity.getStateReason(), alarmEntity.getStateReasonData()));
historyDataJSON.element("newState", getJSONObjectFromState(newState.getStateValue(), newState.getStateReason(), newState.getStateReasonData()));
return historyDataJSON;
}
private static JSONObject getJSONObjectFromState(StateValue stateValue,
String stateReason, String stateReasonData) {
JSONObject jsonObject = new JSONObject();
jsonObject.element("stateValue", stateValue.toString());
jsonObject.element("stateReason", stateReason);
if (stateReasonData != null) {
jsonObject.element("stateReasonData", stateReasonData);
}
return jsonObject;
}
public static void executeActions(AlarmEntity alarmEntity, AlarmState state,
boolean stateJustChanged, Date now) {
if (alarmEntity.getActionsEnabled()) {
Collection<String> actions = AlarmUtils.getActionsByState(alarmEntity, state);
for (String action: actions) {
Action actionToExecute = ActionManager.getAction(action, alarmEntity.getDimensionMap());
if (actionToExecute == null) {
LOG.warn("Unsupported action " + action); // TODO: do not let it in to start with...
}
// always execute autoscaling actions, but others only on state change...
else if (actionToExecute.alwaysExecute() || stateJustChanged) {
LOG.debug("Executing alarm " + alarmEntity.getAccountId() + "/" + alarmEntity.getAlarmName() + " action " + action);
actionToExecute.executeAction(action, alarmEntity.getDimensionMap(), alarmEntity, now);
}
}
}
alarmEntity.setLastActionsUpdatedTimestamp(now);
}
public static Collection<AlarmHistory> executeActionsAndRecord(AlarmEntity alarmEntity, AlarmState state,
boolean stateJustChanged, Date now, List<AlarmHistory> historyList) {
List<AlarmHistory> alarmHistoryList = Lists.newArrayList();
if (alarmEntity.getActionsEnabled()) {
Collection<String> actions = AlarmUtils.getActionsByState(alarmEntity, state);
for (String action : actions) {
Action actionToExecute = ActionManager.getAction(action, alarmEntity.getDimensionMap());
if (actionToExecute == null) {
LOG.warn("Unsupported action " + action); // TODO: do not let it in to start with...
}
// always execute autoscaling actions, but others only on state change...
else if (actionToExecute.alwaysExecute() || stateJustChanged) {
LOG.debug("Executing alarm " + alarmEntity.getAccountId() + "/" + alarmEntity.getAlarmName() + " action " + action);
alarmHistoryList.add(actionToExecute.executeActionAndRecord(action, alarmEntity.getDimensionMap(), alarmEntity, now));
}
}
}
return alarmHistoryList;
}
private static String createStateReasonData(StateValue stateValue,
List<Double> relevantDataPoints, List<Double> recentDataPoints,
ComparisonOperator comparisonOperator, Double threshold, String stateReason, Integer period, Date queryDate, Statistic statistic) {
JSONObject stateReasonDataJSON = new JSONObject();
stateReasonDataJSON.element("version", "1.0");
stateReasonDataJSON.element("queryDate", Timestamps.formatIso8601UTCLongDateMillisTimezone(queryDate));
stateReasonDataJSON.element("statistic", statistic.toString());
stateReasonDataJSON.element("recentDatapoints", pruneNullsAtBeginning(recentDataPoints));
stateReasonDataJSON.element("period", period);
stateReasonDataJSON.element("threshold", threshold);
String stateReasonData = stateReasonDataJSON.toString();
return stateReasonData;
}
private static List<Double> pruneNullsAtBeginning(List<Double> recentDataPoints) {
ArrayList<Double> returnValue = new ArrayList<Double>();
boolean foundNotNull = false;
for (Double recentDataPoint: recentDataPoints) {
if (recentDataPoint != null) {
foundNotNull = true;
}
if (foundNotNull) {
returnValue.add(recentDataPoint);
}
}
return returnValue;
}
private static String createStateReason(StateValue stateValue, List<Double> relevantDataPoints,
ComparisonOperator comparisonOperator, Double threshold) {
String stateReason = null;
if (stateValue == StateValue.INSUFFICIENT_DATA) {
stateReason = "Insufficient Data: " + relevantDataPoints.size() +
AlarmUtils.matchSingularPlural(relevantDataPoints.size(), " datapoint was ", " datapoints were ") +
"unknown.";
} else {
stateReason = "Threshold Crossed: " + relevantDataPoints.size() +
AlarmUtils.matchSingularPlural(relevantDataPoints.size(), " datapoint ", " datapoints ") +
AlarmUtils.makeDoubleList(relevantDataPoints) +
AlarmUtils.matchSingularPlural(relevantDataPoints.size(), " was ", " were ") +
(stateValue == StateValue.OK ? " not " : "") +
AlarmUtils.comparisonOperatorString(comparisonOperator) +
" the threshold (" + threshold + ").";
}
return stateReason;
}
public static AlarmState createAlarmState(StateValue stateValue,
List<Double> relevantDataPoints, List<Double> recentDataPoints,
ComparisonOperator comparisonOperator, Double threshold, Integer period, Date queryDate, Statistic statistic) {
String stateReason = createStateReason(stateValue, relevantDataPoints, comparisonOperator, threshold);
return createAlarmState(stateValue, relevantDataPoints, recentDataPoints, comparisonOperator, threshold, stateReason, period, queryDate, statistic);
}
static AlarmState createAlarmState(StateValue stateValue,
List<Double> relevantDataPoints, List<Double> recentDataPoints,
ComparisonOperator comparisonOperator, Double threshold, String stateReason, Integer period, Date queryDate, Statistic statistic) {
String stateReasonData = createStateReasonData(stateValue, relevantDataPoints, recentDataPoints, comparisonOperator, threshold, stateReason, period, queryDate, statistic);
return new AlarmState(stateValue, stateReason, stateReasonData);
}
private static AlarmState createAlarmState(StateValue stateValue,
String stateReason, String stateReasonData) {
return new AlarmState(stateValue, stateReason, stateReasonData);
}
public static AlarmHistory createChangeAlarmStateHistoryItem(AlarmEntity alarmEntity, AlarmState newState, Date evaluationDate)
{
JSONObject historyDataJSON = getJSONObjectForStateChange(alarmEntity, newState);
String historyData = historyDataJSON.toString();
return createAlarmHistoryItem(alarmEntity.getAccountId(), alarmEntity.getAlarmName(), historyData,
HistoryItemType.StateUpdate, " Alarm updated from " + alarmEntity.getStateValue() + " to " + newState.getStateValue(), evaluationDate);
}
public static void changeAlarmStateBatch(Map<String, AlarmState> statesToUpdate, Date evaluationDate) {
if (statesToUpdate.isEmpty()) return;
try (final TransactionResource db = Entities.transactionFor(AlarmEntity.class)) {
Criteria criteria = Entities.createCriteria(AlarmEntity.class);
criteria = criteria.add(Restrictions.in("naturalId", statesToUpdate.keySet()));
List<AlarmEntity> result = criteria.list();
for (AlarmEntity alarmEntity: result) {
AlarmState newState = statesToUpdate.get(alarmEntity.getNaturalId());
if (newState != null) {
alarmEntity.setStateReason(newState.getStateReason());
alarmEntity.setStateReasonData(newState.getStateReasonData());
alarmEntity.setStateValue(newState.getStateValue());
alarmEntity.setStateUpdatedTimestamp(evaluationDate);
}
}
db.commit();
}
}
public static void addAlarmHistoryEvents(List<AlarmHistory> historyList) {
try (final TransactionResource db = Entities.transactionFor(AlarmHistory.class)) {
for (AlarmHistory alarmHistory: historyList) {
Entities.persist(alarmHistory);
}
db.commit();
}
}
private static class AutoScalingClient extends DispatchingClient<AutoScalingMessage,AutoScaling> {
public AutoScalingClient( final String userId ) {
super( userId, AutoScaling.class );
}
public AutoScalingClient( final AccountFullName accountFullName ) {
super( accountFullName, AutoScaling.class );
}
}
private static class EucalyptusClient extends DispatchingClient<ComputeMessage,Eucalyptus> {
public EucalyptusClient( final String userId ) {
super( userId, Eucalyptus.class );
}
public EucalyptusClient( final AccountFullName accountFullName ) {
super( accountFullName, Eucalyptus.class );
}
}
private static abstract class Action {
public abstract boolean filter(final String actionURN, final Map<String, String> dimensionMap);
public abstract void executeAction(final String actionARN, final Map<String, String> dimensionMap, final AlarmEntity alarmEntity, final Date now);
public abstract boolean alwaysExecute();
<R> Callback.Checked<R> getCallback(final String action, final AlarmEntity alarmEntity, final Date now) {
return new Callback.Checked<R>() {
@Override
public void fire(R input) {
success(action, alarmEntity, now);
}
@Override
public void fireException(Throwable t) {
failure(action, alarmEntity, now, t);
}
};
}
<R> Callback.Checked<R> getRecordCallback(final String action, final AlarmEntity alarmEntity, final Date now, final CheckedListenableFuture<AlarmHistory> resultFuture) {
return new Callback.Checked<R>() {
@Override
public void fire(R input) {
resultFuture.set(recordSuccess(action, alarmEntity, now));
}
@Override
public void fireException(Throwable t) {
resultFuture.setException(t);
}
};
}
public void success(final String actionARN, final AlarmEntity alarmEntity, final Date now) {
JSONObject historyDataJSON = getSuccessJSON(actionARN, alarmEntity);
String historyData = historyDataJSON.toString();
AlarmManager.addAlarmHistoryItem(alarmEntity.getAccountId(), alarmEntity.getAlarmName(), historyData,
HistoryItemType.Action, " Successfully executed action " + actionARN, now);
}
private JSONObject getSuccessJSON(String actionARN, AlarmEntity alarmEntity) {
JSONObject historyDataJSON = new JSONObject();
historyDataJSON.element("actionState", "Succeeded");
historyDataJSON.element("notificationResource", actionARN);
historyDataJSON.element("stateUpdateTimestamp", Timestamps.formatIso8601UTCLongDateMillisTimezone(alarmEntity.getStateUpdatedTimestamp()));
return historyDataJSON;
}
public void failure(final String actionARN, final AlarmEntity alarmEntity, final Date now, Throwable cause) {
JSONObject historyDataJSON = getFailureJSON(actionARN, alarmEntity, cause);
String historyData = historyDataJSON.toString();
AlarmManager.addAlarmHistoryItem(alarmEntity.getAccountId(), alarmEntity.getAlarmName(), historyData,
HistoryItemType.Action, " Failed to execute action " + actionARN, now);
}
private JSONObject getFailureJSON(String actionARN, AlarmEntity alarmEntity, Throwable cause) {
JSONObject historyDataJSON = new JSONObject();
historyDataJSON.element("actionState", "Failed");
historyDataJSON.element("notificationResource", actionARN);
historyDataJSON.element("stateUpdateTimestamp", Timestamps.formatIso8601UTCLongDateMillisTimezone(alarmEntity.getStateUpdatedTimestamp()));
historyDataJSON.element("error", cause.getMessage() != null ? cause.getMessage() : cause.getClass().getName());
return historyDataJSON;
}
public AlarmHistory recordSuccess(final String actionARN, final AlarmEntity alarmEntity, final Date now) {
JSONObject historyDataJSON = getSuccessJSON(actionARN, alarmEntity);
String historyData = historyDataJSON.toString();
return AlarmManager.createAlarmHistoryItem(alarmEntity.getAccountId(), alarmEntity.getAlarmName(), historyData,
HistoryItemType.Action, " Successfully executed action " + actionARN, now);
}
public AlarmHistory recordFailure(final String actionARN, final AlarmEntity alarmEntity, final Date now, Throwable cause) {
JSONObject historyDataJSON = getFailureJSON(actionARN, alarmEntity, cause);
String historyData = historyDataJSON.toString();
return AlarmManager.createAlarmHistoryItem(alarmEntity.getAccountId(), alarmEntity.getAlarmName(), historyData,
HistoryItemType.Action, " Failed to execute action " + actionARN, now);
}
public abstract AlarmHistory executeActionAndRecord(final String action, final Map<String, String> dimensionMap, final AlarmEntity alarmEntity, final Date now);
}
private static class ExecuteAutoScalingPolicyAction extends Action {
@Override
public boolean filter(String action, Map<String, String> dimensionMap) {
return (action != null && action.startsWith("arn:aws:autoscaling:"));
}
@Override
public void executeAction(final String action, final Map<String, String> dimensionMap, final AlarmEntity alarmEntity, final Date now) {
ExecutePolicyType executePolicyType = new ExecutePolicyType();
executePolicyType.setPolicyName(action);
executePolicyType.setHonorCooldown(true);
Callback.Checked<AutoScalingMessage> callback = getCallback(action, alarmEntity, now);
try {
AutoScalingClient client = new AutoScalingClient(AccountFullName.getInstance( alarmEntity.getAccountId() ));
client.init();
client.dispatch(executePolicyType, callback);
} catch (Exception ex) {
failure(action, alarmEntity, now, ex);
}
}
@Override
public AlarmHistory executeActionAndRecord(final String action, final Map<String, String> dimensionMap, final AlarmEntity alarmEntity, final Date now) {
ExecutePolicyType executePolicyType = new ExecutePolicyType();
executePolicyType.setPolicyName(action);
executePolicyType.setHonorCooldown(true);
CheckedListenableFuture<AlarmHistory> alarmHistoryFuture = Futures.newGenericeFuture();
Callback.Checked<AutoScalingMessage> callback = getRecordCallback(action, alarmEntity, now, alarmHistoryFuture);
try {
AutoScalingClient client = new AutoScalingClient(AccountFullName.getInstance( alarmEntity.getAccountId() ));
client.init();
client.dispatch(executePolicyType, callback);
} catch (Exception ex) {
alarmHistoryFuture.set(recordFailure(action, alarmEntity, now, ex));
}
try {
return alarmHistoryFuture.get();
} catch (InterruptedException | ExecutionException e) {
Throwable cause = (e instanceof ExecutionException) ? e.getCause() : e;
return recordFailure(action, alarmEntity, now, e);
}
}
@Override
public boolean alwaysExecute() {
return true;
}
}
private static class TerminateInstanceAction extends Action {
@Override
public boolean filter(String action, Map<String, String> dimensionMap) {
if (action == null) return false;
// Example:
// arn:aws:automate:us-east-1:ec2:terminate
if (!action.startsWith("arn:aws:automate:")) return false;
if (!action.endsWith(":ec2:terminate")) return false;
if (dimensionMap == null) return false;
return (dimensionMap.containsKey("InstanceId"));
}
@Override
public void executeAction(final String action, final Map<String, String> dimensionMap, final AlarmEntity alarmEntity, final Date now) {
TerminateInstancesType terminateInstances = new TerminateInstancesType();
terminateInstances.getInstancesSet().add( dimensionMap.get("InstanceId"));
Callback.Checked<ComputeMessage> callback = getCallback(action, alarmEntity, now);
try {
EucalyptusClient client = new EucalyptusClient( AccountFullName.getInstance( alarmEntity.getAccountId( ) ) );
client.init();
client.dispatch(terminateInstances, callback);
} catch (Exception ex) {
failure(action, alarmEntity, now, ex);
}
}
@Override
public boolean alwaysExecute() {
return false;
}
@Override
public AlarmHistory executeActionAndRecord(final String action, final Map<String, String> dimensionMap, final AlarmEntity alarmEntity, final Date now) {
TerminateInstancesType terminateInstances = new TerminateInstancesType();
terminateInstances.getInstancesSet().add( dimensionMap.get("InstanceId"));
CheckedListenableFuture<AlarmHistory> alarmHistoryFuture = Futures.newGenericeFuture();
Callback.Checked<ComputeMessage> callback = getRecordCallback(action, alarmEntity, now, alarmHistoryFuture);
try {
EucalyptusClient client = new EucalyptusClient( AccountFullName.getInstance( alarmEntity.getAccountId( ) ) );
client.init();
client.dispatch(terminateInstances, callback);
} catch (Exception ex) {
alarmHistoryFuture.set(recordFailure(action, alarmEntity, now, ex));
}
try {
return alarmHistoryFuture.get();
} catch (InterruptedException | ExecutionException e) {
Throwable cause = (e instanceof ExecutionException) ? e.getCause() : e;
return recordFailure(action, alarmEntity, now, e);
}
}
}
private static class StopInstanceAction extends Action {
@Override
public boolean filter(String action, Map<String, String> dimensionMap) {
if (action == null) return false;
// Example:
// arn:aws:automate:us-east-1:ec2:stop
if (!action.startsWith("arn:aws:automate:")) return false;
if (!action.endsWith(":ec2:stop")) return false;
if (dimensionMap == null) return false;
return (dimensionMap.containsKey("InstanceId"));
}
@Override
public void executeAction(final String action, final Map<String, String> dimensionMap, final AlarmEntity alarmEntity, final Date now) {
StopInstancesType stopInstances = new StopInstancesType();
stopInstances.getInstancesSet().add( dimensionMap.get("InstanceId"));
Callback.Checked<ComputeMessage> callback = getCallback(action, alarmEntity, now);
try {
EucalyptusClient client = new EucalyptusClient(AccountFullName.getInstance( alarmEntity.getAccountId( ) ));
client.init();
client.dispatch(stopInstances, callback);
} catch (Exception ex) {
failure(action, alarmEntity, now, ex);
}
}
@Override
public boolean alwaysExecute() {
return false;
}
@Override
public AlarmHistory executeActionAndRecord(String action, Map<String, String> dimensionMap, AlarmEntity alarmEntity, Date now) {
StopInstancesType stopInstances = new StopInstancesType();
stopInstances.getInstancesSet().add( dimensionMap.get("InstanceId"));
CheckedListenableFuture<AlarmHistory> alarmHistoryFuture = Futures.newGenericeFuture();
Callback.Checked<ComputeMessage> callback = getRecordCallback(action, alarmEntity, now, alarmHistoryFuture);
try {
EucalyptusClient client = new EucalyptusClient(AccountFullName.getInstance( alarmEntity.getAccountId( ) ));
client.init();
client.dispatch(stopInstances, callback);
} catch (Exception ex) {
alarmHistoryFuture.set(recordFailure(action, alarmEntity, now, ex));
}
try {
return alarmHistoryFuture.get();
} catch (InterruptedException | ExecutionException e) {
Throwable cause = (e instanceof ExecutionException) ? e.getCause() : e;
return recordFailure(action, alarmEntity, now, e);
}
}
}
public static class ActionManager {
private static List<Action> actions = new ArrayList<Action>();
static {
actions.add(new StopInstanceAction());
actions.add(new TerminateInstanceAction());
actions.add(new ExecuteAutoScalingPolicyAction());
}
public static Action getAction(String action, Map<String, String> dimensionMap) {
for (Action actionFromList :actions) {
if (actionFromList.filter(action, dimensionMap)) {
return actionFromList;
}
}
return null;
}
}
}