com.eucalyptus.cloudwatch.domain.alarms.AlarmManager.java Source code

Java tutorial

Introduction

Here is the source code for com.eucalyptus.cloudwatch.domain.alarms.AlarmManager.java

Source

/*************************************************************************
 * Copyright 2009-2013 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.cloudwatch.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 javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.persistence.EntityTransaction;

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.Accounts;
import com.eucalyptus.auth.principal.Account;
import com.eucalyptus.auth.principal.User;
import com.eucalyptus.autoscaling.common.AutoScaling;
import com.eucalyptus.autoscaling.common.AutoScalingMessage;
import com.eucalyptus.autoscaling.common.ExecutePolicyResponseType;
import com.eucalyptus.autoscaling.common.ExecutePolicyType;
import com.eucalyptus.cloudwatch.CloudWatchException;
import com.eucalyptus.cloudwatch.CloudWatchMetadata;
import com.eucalyptus.cloudwatch.CloudWatchResourceName;
import com.eucalyptus.cloudwatch.ResourceNotFoundException;
import com.eucalyptus.cloudwatch.domain.DimensionEntity;
import com.eucalyptus.cloudwatch.domain.NextTokenUtils;
import com.eucalyptus.cloudwatch.domain.alarms.AlarmEntity.ComparisonOperator;
import com.eucalyptus.cloudwatch.domain.alarms.AlarmEntity.StateValue;
import com.eucalyptus.cloudwatch.domain.alarms.AlarmEntity.Statistic;
import com.eucalyptus.cloudwatch.domain.alarms.AlarmHistory.HistoryItemType;
import com.eucalyptus.cloudwatch.domain.metricdata.MetricEntity.MetricType;
import com.eucalyptus.cloudwatch.domain.metricdata.MetricEntity.Units;
import com.eucalyptus.component.ComponentIds;
import com.eucalyptus.component.ServiceConfiguration;
import com.eucalyptus.component.ServiceConfigurations;
import com.eucalyptus.component.id.Eucalyptus;
import com.eucalyptus.crypto.util.Timestamps;
import com.eucalyptus.entities.Entities;
import com.eucalyptus.records.Logs;
import com.eucalyptus.util.CollectionUtils;
import com.eucalyptus.util.Callback;
import com.eucalyptus.util.DispatchingClient;
import com.eucalyptus.util.async.AsyncRequests;
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;

import edu.ucsb.eucalyptus.msgs.BaseMessage;
import edu.ucsb.eucalyptus.msgs.EucalyptusMessage;
import edu.ucsb.eucalyptus.msgs.StopInstancesType;
import edu.ucsb.eucalyptus.msgs.TerminateInstancesType;

public class AlarmManager {
    private static final Logger LOG = Logger.getLogger(AlarmManager.class);

    public static Long countMetricAlarms(String accountId) {
        EntityTransaction db = Entities.get(AlarmEntity.class);
        try {
            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();
        } catch (RuntimeException ex) {
            Logs.extreme().error(ex, ex);
            throw ex;
        } finally {
            db.rollback();
        }
    }

    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);
        EntityTransaction db = Entities.get(AlarmEntity.class);
        boolean inDb = false;
        try {
            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();
        } catch (RuntimeException ex) {
            Logs.extreme().error(ex, ex);
            throw ex;
        } finally {
            if (db.isActive())
                db.rollback();
        }
    }

    static void addAlarmHistoryItem(String accountId, String alarmName, String historyData,
            HistoryItemType historyItemType, String historySummary, Date now) {
        if (now == null)
            now = new Date();
        EntityTransaction db = Entities.get(AlarmHistory.class);
        try {
            AlarmHistory alarmHistory = new AlarmHistory();
            alarmHistory.setAccountId(accountId);
            alarmHistory.setAlarmName(alarmName);
            alarmHistory.setHistoryData(historyData);
            alarmHistory.setHistoryItemType(historyItemType);
            alarmHistory.setHistorySummary(historySummary);
            alarmHistory.setTimestamp(now);
            Entities.persist(alarmHistory);
            db.commit();
        } catch (RuntimeException ex) {
            Logs.extreme().error(ex, ex);
            throw ex;
        } finally {
            if (db.isActive())
                db.rollback();
        }
    }

    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);
        final EntityTransaction db = Entities.get(AlarmEntity.class);
        try {
            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;
        } catch (RuntimeException ex) {
            Logs.extreme().error(ex, ex);
            throw ex;
        } finally {
            if (db.isActive())
                db.rollback();
        }
    }

    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 CloudWatchException {
        final EntityTransaction db = Entities.get(AlarmEntity.class);
        try {
            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 ResourceNotFoundException("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();
        } catch (RuntimeException ex) {
            Logs.extreme().error(ex, ex);
            throw ex;
        } finally {
            if (db.isActive())
                db.rollback();
        }
    }

    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<CloudWatchMetadata.AlarmMetadata> filter) throws CloudWatchException {
        final List<AlarmEntity> results = Lists.newArrayList();
        final EntityTransaction db = Entities.get(AlarmEntity.class);
        try {
            boolean first = true;
            String token = nextToken;
            while (token != null || first) {
                first = false;
                final Date nextTokenCreatedTime = NextTokenUtils.getNextTokenCreatedTime(token, AlarmEntity.class,
                        true);
                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();
        } catch (RuntimeException ex) {
            Logs.extreme().error(ex, ex);
            throw ex;
        } finally {
            if (db.isActive())
                db.rollback();
        }
        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<CloudWatchMetadata.AlarmMetadata> filter) {
        final List<AlarmEntity> results = Lists.newArrayList();
        final EntityTransaction db = Entities.get(AlarmEntity.class);
        try {
            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();
        } catch (RuntimeException ex) {
            Logs.extreme().error(ex, ex);
            throw ex;
        } finally {
            if (db.isActive())
                db.rollback();
        }
        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 CloudWatchException {
        final List<AlarmHistory> results = Lists.newArrayList();
        final EntityTransaction db = Entities.get(AlarmHistory.class);
        try {
            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,
                        true);
                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();
        } catch (RuntimeException ex) {
            Logs.extreme().error(ex, ex);
            throw ex;
        } finally {
            if (db.isActive())
                db.rollback();
        }
        return results;
    }

    /**
     * Delete all alarm history before a certain date
     * @param before the date to delete before (inclusive)
     */
    public static void deleteAlarmHistory(Date before) {
        EntityTransaction db = Entities.get(AlarmHistory.class);
        try {
            Map<String, Date> criteria = Maps.newHashMap();
            criteria.put("before", before);
            Entities.deleteAllMatching(AlarmHistory.class, "WHERE timestamp < :before", criteria);
            db.commit();
        } catch (RuntimeException ex) {
            Logs.extreme().error(ex, ex);
            throw ex;
        } finally {
            if (db.isActive())
                db.rollback();
        }
    }

    static void changeAlarmState(AlarmEntity alarmEntity, AlarmState newState, Date now) {
        LOG.info("Updating alarm " + alarmEntity.getAlarmName() + " from " + alarmEntity.getStateValue() + " to "
                + newState.getStateValue());
        alarmEntity.setStateUpdatedTimestamp(now);
        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()));
        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 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;
    }

    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.info("Executing alarm " + alarmEntity.getAlarmName() + " action " + action);
                    actionToExecute.executeAction(action, alarmEntity.getDimensionMap(), alarmEntity, now);
                }
            }
        }
        alarmEntity.setLastActionsUpdatedTimestamp(now);
    }

    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;
    }

    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);
    }

    private static class AutoScalingClient extends DispatchingClient<AutoScalingMessage, AutoScaling> {
        public AutoScalingClient(final String userId) {
            super(userId, AutoScaling.class);
        }
    }

    private static class EucalyptusClient extends DispatchingClient<EucalyptusMessage, Eucalyptus> {
        public EucalyptusClient(final String userId) {
            super(userId, 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();

        public void success(final String actionARN, final AlarmEntity alarmEntity, final Date now) {
            JSONObject historyDataJSON = new JSONObject();
            historyDataJSON.element("actionState", "Succeeded");
            historyDataJSON.element("notificationResource", actionARN);
            historyDataJSON.element("stateUpdateTimestamp",
                    Timestamps.formatIso8601UTCLongDateMillisTimezone(alarmEntity.getStateUpdatedTimestamp()));
            String historyData = historyDataJSON.toString();
            AlarmManager.addAlarmHistoryItem(alarmEntity.getAccountId(), alarmEntity.getAlarmName(), historyData,
                    HistoryItemType.Action, " Successfully executed action " + actionARN, now);
        }

        public void failure(final String actionARN, final AlarmEntity alarmEntity, final Date now,
                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());
            String historyData = historyDataJSON.toString();
            AlarmManager.addAlarmHistoryItem(alarmEntity.getAccountId(), alarmEntity.getAlarmName(), historyData,
                    HistoryItemType.Action, " Failed to execute action " + actionARN, 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 = new Callback.Checked<AutoScalingMessage>() {
                @Override
                public void fire(AutoScalingMessage input) {
                    success(action, alarmEntity, now);
                }

                @Override
                public void fireException(Throwable t) {
                    failure(action, alarmEntity, now, t);
                }
            };
            try {
                Account account = Accounts.getAccountProvider().lookupAccountById(alarmEntity.getAccountId());
                User user = account.lookupUserByName(User.ACCOUNT_ADMIN);
                AutoScalingClient client = new AutoScalingClient(user.getUserId());
                client.init();
                client.dispatch(executePolicyType, callback);
            } catch (Exception ex) {
                failure(action, alarmEntity, now, ex);
            }
        }

        @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<EucalyptusMessage> callback = new Callback.Checked<EucalyptusMessage>() {
                @Override
                public void fire(EucalyptusMessage input) {
                    success(action, alarmEntity, now);
                }

                @Override
                public void fireException(Throwable t) {
                    failure(action, alarmEntity, now, t);
                }
            };
            try {
                Account account = Accounts.getAccountProvider().lookupAccountById(alarmEntity.getAccountId());
                User user = account.lookupUserByName(User.ACCOUNT_ADMIN);
                EucalyptusClient client = new EucalyptusClient(user.getUserId());
                client.init();
                client.dispatch(terminateInstances, callback);
            } catch (Exception ex) {
                failure(action, alarmEntity, now, ex);
            }
        }

        @Override
        public boolean alwaysExecute() {
            return false;
        }
    }

    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<EucalyptusMessage> callback = new Callback.Checked<EucalyptusMessage>() {
                @Override
                public void fire(EucalyptusMessage input) {
                    success(action, alarmEntity, now);
                }

                @Override
                public void fireException(Throwable t) {
                    failure(action, alarmEntity, now, t);
                }
            };
            try {
                Account account = Accounts.getAccountProvider().lookupAccountById(alarmEntity.getAccountId());
                User user = account.lookupUserByName(User.ACCOUNT_ADMIN);
                EucalyptusClient client = new EucalyptusClient(user.getUserId());
                client.init();
                client.dispatch(stopInstances, callback);
            } catch (Exception ex) {
                failure(action, alarmEntity, now, ex);
            }
        }

        @Override
        public boolean alwaysExecute() {
            return false;
        }
    }

    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;
        }
    }

}