Explorar o código

AMBARI-6976 - Alert: Merge Stack Definitions With the Database (jonathanhurley)

Jonathan Hurley %!s(int64=10) %!d(string=hai) anos
pai
achega
3da10f6ef2
Modificáronse 24 ficheiros con 1220 adicións e 76 borrados
  1. 137 4
      ambari-server/src/main/java/org/apache/ambari/server/api/services/AmbariMetaInfo.java
  2. 3 0
      ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java
  3. 7 1
      ambari-server/src/main/java/org/apache/ambari/server/controller/internal/AlertDefinitionResourceProvider.java
  4. 16 0
      ambari-server/src/main/java/org/apache/ambari/server/orm/dao/AlertDefinitionDAO.java
  5. 54 0
      ambari-server/src/main/java/org/apache/ambari/server/state/alert/AggregateSource.java
  6. 96 2
      ambari-server/src/main/java/org/apache/ambari/server/state/alert/AlertDefinition.java
  7. 68 0
      ambari-server/src/main/java/org/apache/ambari/server/state/alert/AlertDefinitionFactory.java
  8. 69 8
      ambari-server/src/main/java/org/apache/ambari/server/state/alert/MetricSource.java
  9. 116 1
      ambari-server/src/main/java/org/apache/ambari/server/state/alert/PercentSource.java
  10. 68 1
      ambari-server/src/main/java/org/apache/ambari/server/state/alert/PortSource.java
  11. 158 3
      ambari-server/src/main/java/org/apache/ambari/server/state/alert/Reporting.java
  12. 13 5
      ambari-server/src/main/java/org/apache/ambari/server/state/alert/Scope.java
  13. 45 0
      ambari-server/src/main/java/org/apache/ambari/server/state/alert/ScriptSource.java
  14. 58 0
      ambari-server/src/main/java/org/apache/ambari/server/state/alert/Source.java
  15. 2 1
      ambari-server/src/main/java/org/apache/ambari/server/upgrade/UpgradeCatalog170.java
  16. 1 1
      ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql
  17. 1 1
      ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql
  18. 1 1
      ambari-server/src/main/resources/Ambari-DDL-Postgres-CREATE.sql
  19. 1 1
      ambari-server/src/main/resources/Ambari-DDL-Postgres-EMBEDDED-CREATE.sql
  20. 67 38
      ambari-server/src/main/resources/stacks/HDP/2.0.6/services/HDFS/alerts.json
  21. 76 3
      ambari-server/src/test/java/org/apache/ambari/server/api/services/AmbariMetaInfoTest.java
  22. 2 1
      ambari-server/src/test/java/org/apache/ambari/server/controller/internal/AlertDefinitionResourceProviderTest.java
  23. 157 0
      ambari-server/src/test/java/org/apache/ambari/server/state/alerts/AlertDefinitionEqualityTest.java
  24. 4 4
      ambari-server/src/test/resources/stacks/HDP/2.0.5/services/HDFS/alerts.json

+ 137 - 4
ambari-server/src/main/java/org/apache/ambari/server/api/services/AmbariMetaInfo.java

@@ -31,6 +31,7 @@ import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Scanner;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
@@ -47,13 +48,18 @@ import org.apache.ambari.server.api.util.StackExtensionHelper;
 import org.apache.ambari.server.configuration.Configuration;
 import org.apache.ambari.server.customactions.ActionDefinition;
 import org.apache.ambari.server.customactions.ActionDefinitionManager;
+import org.apache.ambari.server.orm.dao.AlertDefinitionDAO;
 import org.apache.ambari.server.orm.dao.MetainfoDAO;
+import org.apache.ambari.server.orm.entities.AlertDefinitionEntity;
 import org.apache.ambari.server.orm.entities.MetainfoEntity;
+import org.apache.ambari.server.state.Cluster;
+import org.apache.ambari.server.state.Clusters;
 import org.apache.ambari.server.state.ComponentInfo;
 import org.apache.ambari.server.state.DependencyInfo;
 import org.apache.ambari.server.state.OperatingSystemInfo;
 import org.apache.ambari.server.state.PropertyInfo;
 import org.apache.ambari.server.state.RepositoryInfo;
+import org.apache.ambari.server.state.Service;
 import org.apache.ambari.server.state.ServiceInfo;
 import org.apache.ambari.server.state.Stack;
 import org.apache.ambari.server.state.StackId;
@@ -131,7 +137,15 @@ public class AmbariMetaInfo {
   @Inject
   Injector injector;
 
-  @Inject
+  /**
+   * Alert Definition DAO used to merge stack definitions into the database.
+   */
+  AlertDefinitionDAO alertDefinitionDao;
+
+  /**
+   * A factory that assists in the creation of {@link AlertDefinition} and
+   * {@link AlertDefinitionEntity}.
+   */
   private AlertDefinitionFactory alertDefinitionFactory;
 
   // Required properties by stack version
@@ -171,6 +185,7 @@ public class AmbariMetaInfo {
     getCustomActionDefinitions(customActionRoot);
 
     alertDefinitionFactory = injector.getInstance(AlertDefinitionFactory.class);
+    alertDefinitionDao = injector.getInstance(AlertDefinitionDAO.class);
   }
 
   /**
@@ -1085,13 +1100,131 @@ public class AmbariMetaInfo {
       String serviceName) throws AmbariException {
 
     ServiceInfo svc = getService(stackName, stackVersion, serviceName);
-    File alertsFile = svc.getAlertsFile();
+    return getAlertDefinitions(svc);
+  }
+
+  /**
+   * @param stackName
+   *          the stack name
+   * @param stackVersion
+   *          the stack version
+   * @param serviceName
+   *          the service name
+   * @return the alert definitions for a stack
+   * @throws AmbariException
+   */
+  public Set<AlertDefinition> getAlertDefinitions(ServiceInfo service)
+      throws AmbariException {
+    File alertsFile = service.getAlertsFile();
 
     if (null == alertsFile || !alertsFile.exists()) {
-      LOG.debug("Alerts file for " + stackName + "/" + stackVersion + "/" + serviceName + " not found.");
+      LOG.debug("Alerts file for {}/{} not found.", service.getSchemaVersion(),
+          service.getName());
+
       return null;
     }
 
-    return alertDefinitionFactory.getAlertDefinitions(alertsFile, serviceName);
+    return alertDefinitionFactory.getAlertDefinitions(alertsFile,
+        service.getName());
+  }
+
+  /**
+   * Compares the alert definitions defined on the stack with those in the
+   * database and merges any new or updated definitions. This method will first
+   * determine the services that are installed on each cluster to prevent alert
+   * definitions from undeployed services from being shown.
+   *
+   * @param clusters
+   * @throws AmbariException
+   */
+  public void reconcileAlertDefinitions(Clusters clusters)
+      throws AmbariException {
+
+    Map<String, Cluster> clusterMap = clusters.getClusters();
+    if (null == clusterMap || clusterMap.size() == 0) {
+      return;
+    }
+
+    // for every cluster
+    Set<Entry<String, Cluster>> clusterEntries = clusterMap.entrySet();
+    for (Entry<String, Cluster> clusterEntry : clusterEntries) {
+      Cluster cluster = clusterEntry.getValue();
+      long clusterId = cluster.getClusterId();
+      StackId stackId = cluster.getDesiredStackVersion();
+      StackInfo stackInfo = getStackInfo(stackId.getStackName(),
+          stackId.getStackVersion());
+
+      // creating a mapping between service name and service for fast lookups
+      List<ServiceInfo> stackServices = stackInfo.getServices();
+      Map<String, ServiceInfo> stackServiceMap = new HashMap<String, ServiceInfo>();
+      for (ServiceInfo stackService : stackServices) {
+        stackServiceMap.put(stackService.getName(), stackService);
+      }
+
+      Map<String, Service> clusterServiceMap = cluster.getServices();
+      Set<String> clusterServiceNames = clusterServiceMap.keySet();
+
+      // for every service installed in that cluster, get the service metainfo
+      // and off of that the alert definitions
+      List<AlertDefinition> stackDefinitions = new ArrayList<AlertDefinition>(50);
+      for (String clusterServiceName : clusterServiceNames) {
+        ServiceInfo stackService = stackServiceMap.get(clusterServiceName);
+        if (null == stackService) {
+          continue;
+        }
+
+        // get all alerts defined on the stack for each cluster service
+        Set<AlertDefinition> serviceDefinitions = getAlertDefinitions(stackService);
+        if (null != serviceDefinitions) {
+          stackDefinitions.addAll(serviceDefinitions);
+        }
+      }
+
+      // if there are no alert definitions defined for the cluster services
+      // then don't do anything and go to the next cluster
+      if (null == stackDefinitions || stackDefinitions.size() == 0) {
+        continue;
+      }
+
+      List<AlertDefinitionEntity> persist = new ArrayList<AlertDefinitionEntity>();
+      List<AlertDefinitionEntity> entities = alertDefinitionDao.findAll(clusterId);
+
+      // create a map of the enntities for fast extraction
+      Map<String, AlertDefinitionEntity> mappedEntities = new HashMap<String, AlertDefinitionEntity>(100);
+      for (AlertDefinitionEntity entity : entities) {
+        mappedEntities.put(entity.getDefinitionName(), entity);
+      }
+
+      // for each stack definition, see if it exists and compare if it does
+      for( AlertDefinition stackDefinition : stackDefinitions ){
+        AlertDefinitionEntity entity = mappedEntities.get(stackDefinition.getName());
+
+        // no entity means this is new; create a new entity
+        if (null == entity) {
+          entity = alertDefinitionFactory.coerce(clusterId, stackDefinition);
+          persist.add(entity);
+          continue;
+        }
+
+        // if the definition exists in the stack and the database and they are
+        // not deeply equal, then merge the stack definition into the database
+        AlertDefinition databaseDefinition = alertDefinitionFactory.coerce(entity);
+        if (!stackDefinition.deeplyEquals(databaseDefinition)) {
+          entity = alertDefinitionFactory.merge(stackDefinition, entity);
+          persist.add(entity);
+          continue;
+        }
+      }
+
+      // persist any new or updated definition
+      for (AlertDefinitionEntity entity : persist) {
+        if (LOG.isDebugEnabled()) {
+          LOG.info("Merging Alert Definition {} into the database",
+              entity.getDefinitionName());
+        }
+
+        alertDefinitionDao.createOrUpdate(entity);
+      }
+    }
   }
 }

+ 3 - 0
ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java

@@ -413,6 +413,9 @@ public class AmbariServer {
       LOG.info("********* Current Clusters State *********");
       LOG.info(clusterDump.toString());
 
+      LOG.info("********* Reconciling Alert Definitions **********");
+      ambariMetaInfo.reconcileAlertDefinitions(clusters);
+
       LOG.info("********* Initializing ActionManager **********");
       ActionManager manager = injector.getInstance(ActionManager.class);
       LOG.info("********* Initializing Controller **********");

+ 7 - 1
ambari-server/src/main/java/org/apache/ambari/server/controller/internal/AlertDefinitionResourceProvider.java

@@ -297,7 +297,7 @@ public class AlertDefinitionResourceProvider extends AbstractControllerResourceP
    * being created, an {@link IllegalArgumentException} is thrown when a
    * required property is absent. When updating, missing properties are assume
    * to not have changed.
-   * 
+   *
    * @param entity
    *          the entity to merge the properties into (not {@code null}).
    * @param requestMap
@@ -338,6 +338,12 @@ public class AlertDefinitionResourceProvider extends AbstractControllerResourceP
       scope = Scope.valueOf(desiredScope);
     }
 
+    // if not specified when creating an alert definition, the scope is
+    // assumed to be ANY
+    if (null == scope && bCreate) {
+      scope = Scope.ANY;
+    }
+
     if (StringUtils.isEmpty(clusterName)) {
       throw new IllegalArgumentException(
           "Invalid argument, cluster name is required");

+ 16 - 0
ambari-server/src/main/java/org/apache/ambari/server/orm/dao/AlertDefinitionDAO.java

@@ -264,6 +264,22 @@ public class AlertDefinitionDAO {
     return entityManagerProvider.get().merge(alertDefinition);
   }
 
+  /**
+   * Creates or updates the specified entity. This method will check
+   * {@link AlertDefinitionEntity#getDefinitionId()} in order to determine
+   * whether the entity should be created or merged.
+   * 
+   * @param alertDefinition
+   *          the definition to create or update (not {@code null}).
+   */
+  public void createOrUpdate(AlertDefinitionEntity alertDefinition) {
+    if (null == alertDefinition.getDefinitionId()) {
+      create(alertDefinition);
+    } else {
+      merge(alertDefinition);
+    }
+  }
+
   /**
    * Removes the specified alert definition and all related history and
    * associations from the database.

+ 54 - 0
ambari-server/src/main/java/org/apache/ambari/server/state/alert/AggregateSource.java

@@ -23,6 +23,9 @@ import com.google.gson.annotations.SerializedName;
  * Alert when the source type is defined as {@link SourceType#AGGREGATE}.
  * Aggregate alerts are alerts that are triggered by collecting the states of
  * all instances of the defined alert and calculating the overall state.
+ * <p/>
+ * Equality checking for instances of this class should be executed on every
+ * member to ensure that reconciling stack differences is correct.
  */
 public class AggregateSource extends Source {
 
@@ -35,4 +38,55 @@ public class AggregateSource extends Source {
   public String getAlertName() {
     return m_alertName;
   }
+
+  /**
+   * @param alertName
+   *          the unique name of the alert that will have its values aggregated.
+   */
+  public void setAlertName(String alertName) {
+    m_alertName = alertName;
+  }
+
+  /**
+   *
+   */
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = super.hashCode();
+
+    result = prime * result
+        + ((m_alertName == null) ? 0 : m_alertName.hashCode());
+
+    return result;
+  }
+
+  /**
+   *
+   */
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+
+    if (!super.equals(obj)) {
+      return false;
+    }
+
+    if (getClass() != obj.getClass()) {
+      return false;
+    }
+
+    AggregateSource other = (AggregateSource) obj;
+    if (m_alertName == null) {
+      if (other.m_alertName != null) {
+        return false;
+      }
+    } else if (!m_alertName.equals(other.m_alertName)) {
+      return false;
+    }
+
+    return true;
+  }
 }

+ 96 - 2
ambari-server/src/main/java/org/apache/ambari/server/state/alert/AlertDefinition.java

@@ -17,9 +17,20 @@
  */
 package org.apache.ambari.server.state.alert;
 
+import java.util.HashSet;
 
 /**
- * Represents information required to run and collection of alerts.
+ * The {@link AlertDefinition} class represents all of the necessary information
+ * to schedule, run, and collect alerts.
+ * <p/>
+ * Although all members of this class must define a complex {@code equals} and
+ * {@code hashCode} method, this class itself does not. Instead,
+ * {@link #equals(Object)} is defined as a name comparison only since name is
+ * unique to a definition. This allows us to easily insert instances of this
+ * class into a {@link HashSet} if necessary.
+ * <p/>
+ * When making comparisons for equality for things like stack/database merging,
+ * use {@link #deeplyEquals(Object)}.
  */
 public class AlertDefinition {
 
@@ -134,6 +145,87 @@ public class AlertDefinition {
     label = definitionLabel;
   }
 
+  /**
+   * Compares {@link #equals(Object)} of every field. This is used mainly for
+   * reconciling the stack versus the database.
+   *
+   * @param object
+   * @return
+   */
+  public boolean deeplyEquals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+
+    if (obj == null) {
+      return false;
+    }
+
+    if (getClass() != obj.getClass()) {
+      return false;
+    }
+
+    AlertDefinition other = (AlertDefinition) obj;
+    if (componentName == null) {
+      if (other.componentName != null) {
+        return false;
+      }
+    } else if (!componentName.equals(other.componentName)) {
+      return false;
+    }
+
+    if (enabled != other.enabled) {
+      return false;
+    }
+
+    if (interval != other.interval) {
+      return false;
+    }
+
+    if (label == null) {
+      if (other.label != null) {
+        return false;
+      }
+    } else if (!label.equals(other.label)) {
+      return false;
+    }
+
+    if (name == null) {
+      if (other.name != null) {
+        return false;
+      }
+    } else if (!name.equals(other.name)) {
+      return false;
+    }
+
+    if (scope != other.scope) {
+      return false;
+    }
+
+    if (serviceName == null) {
+      if (other.serviceName != null) {
+        return false;
+      }
+    } else if (!serviceName.equals(other.serviceName)) {
+      return false;
+    }
+
+    if (source == null) {
+      if (other.source != null) {
+        return false;
+      }
+    } else if (!source.equals(other.source)) {
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Gets equality based on name only.
+   *
+   * @see #deeplyEquals(Object)
+   */
   @Override
   public boolean equals(Object obj) {
     if (null == obj || !obj.getClass().equals(AlertDefinition.class)) {
@@ -143,6 +235,9 @@ public class AlertDefinition {
     return name.equals(((AlertDefinition) obj).name);
   }
 
+  /**
+   * Gets a hash based on name only.
+   */
   @Override
   public int hashCode() {
     return name.hashCode();
@@ -152,5 +247,4 @@ public class AlertDefinition {
   public String toString() {
     return name;
   }
-
 }

+ 68 - 0
ambari-server/src/main/java/org/apache/ambari/server/state/alert/AlertDefinitionFactory.java

@@ -25,6 +25,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.UUID;
 
 import org.apache.ambari.server.AmbariException;
 import org.apache.ambari.server.orm.entities.AlertDefinitionEntity;
@@ -140,11 +141,78 @@ public class AlertDefinitionFactory {
       LOG.error(
           "Unable to deserialized the alert definition source during coercion",
           exception);
+
+      return null;
     }
 
     return definition;
   }
 
+  /**
+   * Gets an {@link AlertDefinitionEntity} constructed from the specified
+   * {@link AlertDefinition}.
+   * <p/>
+   * The new entity will have a UUID already set.
+   * 
+   * @param clusterId
+   *          the ID of the cluster.
+   * @param definition
+   *          the definition to use to construct the
+   *          {@link AlertDefinitionEntity} (not {@code null}).
+   * @return the definiion or {@code null} if it could not be coerced.
+   */
+  public AlertDefinitionEntity coerce(long clusterId, AlertDefinition definition) {
+    if (null == definition) {
+      return null;
+    }
+
+    AlertDefinitionEntity entity = new AlertDefinitionEntity();
+    entity.setClusterId(clusterId);
+    return merge(definition, entity);
+  }
+
+  /**
+   * Merges the specified {@link AlertDefinition} into the
+   * {@link AlertDefinitionEntity}, leaving any fields not merged intact.
+   * <p/>
+   * The merged entity will have a new UUID.
+   *
+   * @param definition
+   *          the definition to merge into the entity (not {@code null}).
+   * @param entity
+   *          the entity to merge into (not {@code null}).
+   * @return a merged, but not yes persisted entity, or {@code null} if the
+   *         merge could not be done.
+   */
+  public AlertDefinitionEntity merge(AlertDefinition definition,
+      AlertDefinitionEntity entity) {
+
+    entity.setComponentName(definition.getComponentName());
+    entity.setDefinitionName(definition.getName());
+    entity.setEnabled(definition.isEnabled());
+    entity.setHash(UUID.randomUUID().toString());
+    entity.setLabel(definition.getLabel());
+    entity.setScheduleInterval(definition.getInterval());
+    entity.setScope(definition.getScope());
+    entity.setServiceName(definition.getServiceName());
+
+    Source source = definition.getSource();
+    entity.setSourceType(source.getType().name());
+
+    try {
+      String sourceJson = m_gson.toJson(source);
+      entity.setSource(sourceJson);
+    } catch (Exception exception) {
+      LOG.error(
+          "Unable to serialize the alert definition source during coercion",
+          exception);
+
+      return null;
+    }
+
+    return entity;
+  }
+
   /**
    * Gets an instance of {@link Gson} that can correctly serialize and
    * deserialize an {@link AlertDefinition}.

+ 69 - 8
ambari-server/src/main/java/org/apache/ambari/server/state/alert/MetricSource.java

@@ -21,37 +21,98 @@ import com.google.gson.annotations.SerializedName;
 
 /**
  * Alert when the source type is defined as {@link SourceType#METRIC}
+ * <p/>
+ * Equality checking for instances of this class should be executed on every
+ * member to ensure that reconciling stack differences is correct.
  */
 public class MetricSource extends Source {
-  
+
   private String host = null;
-  
+
   @SerializedName("jmx")
   private String jmxInfo = null;
-  
+
   @SerializedName("ganglia")
   private String gangliaInfo = null;
-  
+
   /**
    * @return the jmx info, if this metric is jmx-based
    */
   public String getJmxInfo() {
     return jmxInfo;
   }
-  
+
   /**
    * @return the ganglia info, if this metric is ganglia-based
    */
   public String getGangliaInfo() {
     return gangliaInfo;
   }
-  
+
   /**
    * @return the host info, which may include port information
    */
   public String getHost() {
     return host;
   }
-  
-  
+
+  /**
+   *
+   */
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = super.hashCode();
+    result = prime * result
+        + ((gangliaInfo == null) ? 0 : gangliaInfo.hashCode());
+    result = prime * result + ((host == null) ? 0 : host.hashCode());
+    result = prime * result + ((jmxInfo == null) ? 0 : jmxInfo.hashCode());
+
+    return result;
+  }
+
+  /**
+   *
+   */
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+
+    if (!super.equals(obj)) {
+      return false;
+    }
+
+    if (getClass() != obj.getClass()) {
+      return false;
+    }
+
+    MetricSource other = (MetricSource) obj;
+    if (gangliaInfo == null) {
+      if (other.gangliaInfo != null) {
+        return false;
+      }
+    } else if (!gangliaInfo.equals(other.gangliaInfo)) {
+      return false;
+    }
+
+    if (host == null) {
+      if (other.host != null) {
+        return false;
+      }
+    } else if (!host.equals(other.host)) {
+      return false;
+    }
+
+    if (jmxInfo == null) {
+      if (other.jmxInfo != null) {
+        return false;
+      }
+    } else if (!jmxInfo.equals(other.jmxInfo)) {
+      return false;
+    }
+
+    return true;
+  }
 }

+ 116 - 1
ambari-server/src/main/java/org/apache/ambari/server/state/alert/PercentSource.java

@@ -21,6 +21,9 @@ import com.google.gson.annotations.SerializedName;
 
 /**
  * Alert when the source type is defined as {@link SourceType#PERCENT}
+ * <p/>
+ * Equality checking for instances of this class should be executed on every
+ * member to ensure that reconciling stack differences is correct.
  */
 public class PercentSource extends Source {
 
@@ -32,7 +35,7 @@ public class PercentSource extends Source {
 
   /**
    * Gets the numerator for the percent calculation.
-   * 
+   *
    * @return a metric value representing the numerator (never {@code null}).
    */
   public MetricFractionPart getNumerator() {
@@ -48,9 +51,66 @@ public class PercentSource extends Source {
     return m_denominator;
   }
 
+  /**
+   *
+   */
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+
+    int result = super.hashCode();
+    result = prime * result
+        + ((m_denominator == null) ? 0 : m_denominator.hashCode());
+    result = prime * result
+        + ((m_numerator == null) ? 0 : m_numerator.hashCode());
+
+    return result;
+  }
+
+  /**
+   *
+   */
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+
+    if (!super.equals(obj)) {
+      return false;
+    }
+
+    if (getClass() != obj.getClass()) {
+      return false;
+    }
+
+    PercentSource other = (PercentSource) obj;
+
+    if (m_denominator == null) {
+      if (other.m_denominator != null) {
+        return false;
+      }
+    } else if (!m_denominator.equals(other.m_denominator)) {
+      return false;
+    }
+
+    if (m_numerator == null) {
+      if (other.m_numerator != null) {
+        return false;
+      }
+    } else if (!m_numerator.equals(other.m_numerator)) {
+      return false;
+    }
+
+    return true;
+  }
+
   /**
    * The {@link MetricFractionPart} class represents either the numerator or the
    * denominator of a fraction.
+   * <p/>
+   * Equality checking for instances of this class should be executed on every
+   * member to ensure that reconciling stack differences is correct.
    */
   public static final class MetricFractionPart {
     @SerializedName("jmx")
@@ -72,5 +132,60 @@ public class PercentSource extends Source {
     public String getGangliaInfo() {
       return m_gangliaInfo;
     }
+
+    /**
+     *
+     */
+    @Override
+    public int hashCode() {
+      final int prime = 31;
+
+      int result = 1;
+      result = prime * result
+          + ((m_gangliaInfo == null) ? 0 : m_gangliaInfo.hashCode());
+
+      result = prime * result
+          + ((m_jmxInfo == null) ? 0 : m_jmxInfo.hashCode());
+
+      return result;
+    }
+
+    /**
+     *
+     */
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+
+      if (obj == null) {
+        return false;
+      }
+
+      if (getClass() != obj.getClass()) {
+        return false;
+      }
+
+      MetricFractionPart other = (MetricFractionPart) obj;
+      if (m_gangliaInfo == null) {
+        if (other.m_gangliaInfo != null) {
+          return false;
+        }
+      } else if (!m_gangliaInfo.equals(other.m_gangliaInfo)) {
+        return false;
+      }
+
+      if (m_jmxInfo == null) {
+        if (other.m_jmxInfo != null) {
+          return false;
+        }
+      } else if (!m_jmxInfo.equals(other.m_jmxInfo)) {
+        return false;
+      }
+
+      return true;
+    }
+
   }
 }

+ 68 - 1
ambari-server/src/main/java/org/apache/ambari/server/state/alert/PortSource.java

@@ -21,13 +21,16 @@ import com.google.gson.annotations.SerializedName;
 
 /**
  * Alert when the source type is defined as {@link SourceType#PORT}
+ * <p/>
+ * Equality checking for instances of this class should be executed on every
+ * member to ensure that reconciling stack differences is correct.
  */
 public class PortSource extends Source {
 
   @SerializedName("uri")
   private String m_uri = null;
 
-  @SerializedName("port")
+  @SerializedName("default_port")
   private int m_port = 0;
 
   /**
@@ -37,10 +40,74 @@ public class PortSource extends Source {
     return m_uri;
   }
 
+  /**
+   * @param uri
+   *          the URI to check for a valid port
+   */
+  public void setUri(String uri) {
+    m_uri = uri;
+  }
+
   /**
    * @return the port to check on the given URI.
    */
   public int getPort() {
     return m_port;
   }
+
+  /**
+   * @param port
+   *          the port to check on the given URI.
+   */
+  public void setPort(int port) {
+    m_port = port;
+  }
+
+  /**
+   *
+   */
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = super.hashCode();
+    result = prime * result + m_port;
+    result = prime * result + ((m_uri == null) ? 0 : m_uri.hashCode());
+
+    return result;
+  }
+
+  /**
+   *
+   */
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+
+    if (!super.equals(obj)) {
+      return false;
+    }
+
+    if (getClass() != obj.getClass()) {
+      return false;
+    }
+
+    PortSource other = (PortSource) obj;
+
+    if (m_port != other.m_port) {
+      return false;
+    }
+
+    if (m_uri == null) {
+      if (other.m_uri != null) {
+        return false;
+      }
+    } else if (!m_uri.equals(other.m_uri)) {
+      return false;
+    }
+
+    return true;
+  }
+
 }

+ 158 - 3
ambari-server/src/main/java/org/apache/ambari/server/state/alert/Reporting.java

@@ -22,23 +22,26 @@ import com.google.gson.annotations.SerializedName;
 /**
  * The {@link Reporting} class represents the OK/WARNING/CRITICAL structures in
  * an {@link AlertDefinition}.
+ * <p/>
+ * Equality checking for instances of this class should be executed on every
+ * member to ensure that reconciling stack differences is correct.
  */
 public class Reporting {
 
   /**
-   *
+   * The OK template.
    */
   @SerializedName("ok")
   private ReportTemplate m_ok;
 
   /**
-   *
+   * The WARNING template.
    */
   @SerializedName("warning")
   private ReportTemplate m_warning;
 
   /**
-   *
+   * The CRITICAL template.
    */
   @SerializedName("critical")
   private ReportTemplate m_critical;
@@ -50,6 +53,14 @@ public class Reporting {
     return m_warning;
   }
 
+  /**
+   * @param warning
+   *          the WARNING structure or {@code null} if none.
+   */
+  public void setWarning(ReportTemplate warning) {
+    m_warning = warning;
+  }
+
   /**
    * @return the CRITICAL structure or {@code null} if none.
    */
@@ -57,6 +68,14 @@ public class Reporting {
     return m_critical;
   }
 
+  /**
+   * @param critical
+   *          the CRITICAL structure or {@code null} if none.
+   */
+  public void setCritical(ReportTemplate critical) {
+    m_critical = critical;
+  }
+
   /**
    * @return the OK structure or {@code null} if none.
    */
@@ -64,9 +83,80 @@ public class Reporting {
     return m_ok;
   }
 
+  /**
+   * @param ok
+   *          the OK structure or {@code null} if none.
+   */
+  public void setOk(ReportTemplate ok) {
+    m_ok = ok;
+  }
+
+  /**
+   *
+   */
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+
+    int result = 1;
+    result = prime * result
+        + ((m_critical == null) ? 0 : m_critical.hashCode());
+    result = prime * result + ((m_ok == null) ? 0 : m_ok.hashCode());
+    result = prime * result + ((m_warning == null) ? 0 : m_warning.hashCode());
+
+    return result;
+  }
+
+  /**
+   *
+   */
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+
+    if (obj == null) {
+      return false;
+    }
+
+    if (getClass() != obj.getClass()) {
+      return false;
+    }
+
+    Reporting other = (Reporting) obj;
+    if (m_critical == null) {
+      if (other.m_critical != null) {
+        return false;
+      }
+    } else if (!m_critical.equals(other.m_critical)) {
+      return false;
+    }
+
+    if (m_ok == null) {
+      if (other.m_ok != null) {
+        return false;
+      }
+    } else if (!m_ok.equals(other.m_ok)) {
+      return false;
+    }
+
+    if (m_warning == null) {
+      if (other.m_warning != null) {
+        return false;
+      }
+    } else if (!m_warning.equals(other.m_warning)) {
+      return false;
+    }
+    return true;
+  }
+
   /**
    * The {@link ReportTemplate} class is used to pair a label and threshhold
    * value.
+   * <p/>
+   * Equality checking for instances of this class should be executed on every
+   * member to ensure that reconciling stack differences is correct.
    */
   public static final class ReportTemplate {
     @SerializedName("text")
@@ -82,11 +172,76 @@ public class Reporting {
       return m_text;
     }
 
+    /**
+     * @param text
+     *          the parameterized text of this template or {@code null} if none.
+     */
+    public void setText(String text) {
+      m_text = text;
+    }
+
     /**
      * @return the threshold value for this template or {@code null} if none.
      */
     public Double getValue() {
       return m_value;
     }
+
+    /**
+     * @param value
+     *          the threshold value for this template or {@code null} if none.
+     */
+    public void setValue(Double value) {
+      m_value = value;
+    }
+
+    /**
+     *
+     */
+    @Override
+    public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + ((m_text == null) ? 0 : m_text.hashCode());
+      result = prime * result + ((m_value == null) ? 0 : m_value.hashCode());
+      return result;
+    }
+
+    /**
+     *
+     */
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+
+      if (obj == null) {
+        return false;
+      }
+
+      if (getClass() != obj.getClass()) {
+        return false;
+      }
+
+      ReportTemplate other = (ReportTemplate) obj;
+
+      if (m_text == null) {
+        if (other.m_text != null) {
+          return false;
+        }
+      } else if (!m_text.equals(other.m_text)) {
+        return false;
+      }
+
+      if (m_value == null) {
+        if (other.m_value != null) {
+          return false;
+        }
+      } else if (!m_value.equals(other.m_value)) {
+        return false;
+      }
+      return true;
+    }
   }
 }

+ 13 - 5
ambari-server/src/main/java/org/apache/ambari/server/state/alert/Scope.java

@@ -18,16 +18,24 @@
 package org.apache.ambari.server.state.alert;
 
 /**
- * Assigns a scope to an alert.
- * 
+ * The {@link Scope} enumeration is used to define the area that an alert has
+ * the most meaning. This is primary used as a way for a UI to determine the
+ * correct views that the alert should be visible in.
  */
 public enum Scope {
   /**
-   * Definition is scoped to a host only
+   * The alert only makes sense when viewing a host that the alert runs on.
    */
   HOST,
+
+  /**
+   * The alert only makes sense when viewing a service that the alert is defined
+   * for.
+   */
+  SERVICE,
+
   /**
-   * Definition is scoped to a service only
+   * The alert can be shown in any context (HOST or SERVICE).
    */
-  SERVICE
+  ANY
 }

+ 45 - 0
ambari-server/src/main/java/org/apache/ambari/server/state/alert/ScriptSource.java

@@ -21,6 +21,9 @@ import com.google.gson.annotations.SerializedName;
 
 /**
  * Alert when the source type is defined as {@link SourceType#SCRIPT}
+ * <p/>
+ * Equality checking for instances of this class should be executed on every
+ * member to ensure that reconciling stack differences is correct.
  */
 public class ScriptSource extends Source {
 
@@ -33,4 +36,46 @@ public class ScriptSource extends Source {
   public String getPath() {
     return m_path;
   }
+
+  /**
+   *
+   */
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = super.hashCode();
+    result = prime * result + ((m_path == null) ? 0 : m_path.hashCode());
+
+    return result;
+  }
+
+  /**
+   *
+   */
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+
+    if (!super.equals(obj)) {
+      return false;
+    }
+
+    if (getClass() != obj.getClass()) {
+      return false;
+    }
+
+    ScriptSource other = (ScriptSource) obj;
+
+    if (m_path == null) {
+      if (other.m_path != null) {
+        return false;
+      }
+    } else if (!m_path.equals(other.m_path)) {
+      return false;
+    }
+
+    return true;
+  }
 }

+ 58 - 0
ambari-server/src/main/java/org/apache/ambari/server/state/alert/Source.java

@@ -21,6 +21,9 @@ import com.google.gson.annotations.SerializedName;
 
 /**
  * Abstract class that all known alert sources should extend.
+ * <p/>
+ * Equality checking for instances of this class should be executed on every
+ * member to ensure that reconciling stack differences is correct.
  */
 public abstract class Source {
 
@@ -42,4 +45,59 @@ public abstract class Source {
   public Reporting getReporting() {
     return reporting;
   }
+
+  /**
+   * Sets the OK/WARNING/CRTICAL structures.
+   * 
+   * @param reporting
+   *          the reporting structure or {@code null} for none.
+   */
+  public void setReporting(Reporting reporting) {
+    this.reporting = reporting;
+  }
+
+  /**
+   *
+   */
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = 1;
+    result = prime * result + ((reporting == null) ? 0 : reporting.hashCode());
+    result = prime * result + ((type == null) ? 0 : type.hashCode());
+
+    return result;
+  }
+
+  /**
+   *
+   */
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+
+    if (obj == null) {
+      return false;
+    }
+
+    if (getClass() != obj.getClass()) {
+      return false;
+    }
+
+    Source other = (Source) obj;
+    if (reporting == null) {
+      if (other.reporting != null) {
+        return false;
+      }
+    } else if (!reporting.equals(other.reporting)) {
+      return false;
+    }
+
+    if (type != other.type) {
+      return false;
+    }
+    return true;
+  }
 }

+ 2 - 1
ambari-server/src/main/java/org/apache/ambari/server/upgrade/UpgradeCatalog170.java

@@ -72,6 +72,7 @@ import org.apache.ambari.server.state.Cluster;
 import org.apache.ambari.server.state.Clusters;
 import org.apache.ambari.server.state.Config;
 import org.apache.ambari.server.state.ConfigHelper;
+import org.apache.ambari.server.state.alert.Scope;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -573,7 +574,7 @@ public class UpgradeCatalog170 extends AbstractUpgradeCatalog {
     columns.add(new DBColumnInfo("definition_name", String.class, 255, null, false));
     columns.add(new DBColumnInfo("service_name", String.class, 255, null, false));
     columns.add(new DBColumnInfo("component_name", String.class, 255, null, true));
-    columns.add(new DBColumnInfo("scope", String.class, 255, null, true));
+    columns.add(new DBColumnInfo("scope", String.class, 255, Scope.ANY.name(), false));
     columns.add(new DBColumnInfo("label", String.class, 255, null, true));
     columns.add(new DBColumnInfo("enabled", Short.class, 1, 1, false));
     columns.add(new DBColumnInfo("schedule_interval", Integer.class, null, null, false));

+ 1 - 1
ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql

@@ -156,7 +156,7 @@ CREATE TABLE alert_definition (
   definition_name VARCHAR(255) NOT NULL,
   service_name VARCHAR(255) NOT NULL,
   component_name VARCHAR(255),
-  scope VARCHAR(255),
+  scope VARCHAR(255) DEFAULT 'ANY' NOT NULL,
   label VARCHAR(255),
   enabled SMALLINT DEFAULT 1 NOT NULL,
   schedule_interval INTEGER NOT NULL,

+ 1 - 1
ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql

@@ -147,7 +147,7 @@ CREATE TABLE alert_definition (
   definition_name VARCHAR2(255) NOT NULL,
   service_name VARCHAR2(255) NOT NULL,
   component_name VARCHAR2(255),
-  scope VARCHAR2(255),
+  scope VARCHAR2(255) DEFAULT 'ANY' NOT NULL,
   label VARCHAR2(255),
   enabled NUMBER(1) DEFAULT 1 NOT NULL,
   schedule_interval NUMBER(10) NOT NULL,

+ 1 - 1
ambari-server/src/main/resources/Ambari-DDL-Postgres-CREATE.sql

@@ -180,7 +180,7 @@ CREATE TABLE alert_definition (
   definition_name VARCHAR(255) NOT NULL,
   service_name VARCHAR(255) NOT NULL,
   component_name VARCHAR(255),
-  scope VARCHAR(255),
+  scope VARCHAR(255) DEFAULT 'ANY' NOT NULL,
   label VARCHAR(255),
   enabled SMALLINT DEFAULT 1 NOT NULL,
   schedule_interval INTEGER NOT NULL,

+ 1 - 1
ambari-server/src/main/resources/Ambari-DDL-Postgres-EMBEDDED-CREATE.sql

@@ -245,7 +245,7 @@ CREATE TABLE ambari.alert_definition (
   definition_name VARCHAR(255) NOT NULL,
   service_name VARCHAR(255) NOT NULL,
   component_name VARCHAR(255),
-  scope VARCHAR(255),
+  scope VARCHAR(255) DEFAULT 'ANY' NOT NULL,
   label VARCHAR(255),
   enabled SMALLINT DEFAULT 1 NOT NULL,
   schedule_interval INTEGER NOT NULL,

+ 67 - 38
ambari-server/src/main/resources/stacks/HDP/2.0.6/services/HDFS/alerts.json

@@ -1,58 +1,87 @@
 {
-  "service": [
-    // datanode space aggregate
-    // datanode process aggregate
-  ],
-  "SECONDARY_NAMENODE": [
+  "service": [],
+  "NAMENODE": [
     {
-      "name": "secondary_namenode_process",
-      "label": "Secondary NameNode process",
+      "name": "namenode_process",
+      "label": "NameNode Process",
       "interval": 1,
-      "scope": "service",
+      "scope": "any",
       "source": {
         "type": "PORT",
-        "config": "{{hdfs-site/dfs.namenode.secondary.http-address}}:50071",
-        "default": 50071
+        "uri": "{{hdfs-site/dfs.namenode.http-address}}",
+        "default_port": 50070,
+        "reporting": {
+          "ok": {
+            "text": "TCP OK - {0:.4f} response on port {1}"
+          },
+          "critical": {
+            "text": "Connection failed: {0} on host {1}:{2}"
+          }
+        }        
       }
     }
   ],
-  "NAMENODE": [
-    // name node cpu utilization (metric)
+  "SECONDARY_NAMENODE": [
     {
-      "name": "namenode_cpu",
-      "label": "NameNode host CPU Utilization",
-      "scope": "host",
+      "name": "secondary_namenode_process",
+      "label": "Secondary NameNode Process",
+      "interval": 1,
+      "scope": "any",
       "source": {
-        "type": "METRIC",
-        "jmx": "java.lang:type=OperatingSystem/SystemCpuLoad",
-        "host": "{{hdfs-site/dfs.namenode.secondary.http-address}}"
+        "type": "PORT",        
+        "uri": "{{hdfs-site/dfs.namenode.secondary.http-address}}",
+        "default_port": 50071,
+        "reporting": {
+          "ok": {
+            "text": "TCP OK - {0:.4f} response on port {1}"
+          },
+          "critical": {
+            "text": "Connection failed: {0} on host {1}:{2}"
+          }
+        }        
       }
-    },
-    // namenode process (port check)
+    }
+  ],
+  "JOURNALNODE": [
     {
-      "name": "namenode_process",
-      "label": "NameNode process",
+      "name": "journalnode_process",
+      "label": "JournalNode Process",
       "interval": 1,
       "scope": "host",
       "source": {
-        "type": "PORT",
-        "uri": "{{hdfs-site/dfs.namenode.http-address}}:50070"
-       }
-    },
+        "type": "PORT",        
+        "uri": "{{hdfs-site/dfs.journalnode.http-address}}",
+        "default_port": 8480,
+        "reporting": {
+          "ok": {
+            "text": "TCP OK - {0:.4f} response on port {1}"
+          },
+          "critical": {
+            "text": "Connection failed: {0} on host {1}:{2}"
+          }
+        }        
+      }
+    }
+  ],      
+  "DATANODE": [
     {
-      "name": "hdfs_last_checkpoint",
-      "label": "Last Checkpoint Time",
+      "name": "datanode_process",
+      "label": "DateNode Process",
       "interval": 1,
-      "scope": "service",
-      "enabled": false
+      "scope": "host",
       "source": {
-        "type": "SCRIPT",
-        "path": "scripts/alerts/last_checkpoint.py"
+        "type": "PORT",        
+        "uri": "{{hdfs-site/dfs.datanode.http.address}}",
+        "default_port": 50075,
+        "reporting": {
+          "ok": {
+            "text": "TCP OK - {0:.4f} response on port {1}"
+          },
+          "critical": {
+            "text": "Connection failed: {0} on host {1}:{2}"
+          }
+        }        
       }
-    }
-  ],
-  "DATANODE": [
-    // datanode process (port check)
-    // datanode space
+    }    
   ]
-}
+}

+ 76 - 3
ambari-server/src/test/java/org/apache/ambari/server/api/services/AmbariMetaInfoTest.java

@@ -18,6 +18,9 @@
 
 package org.apache.ambari.server.api.services;
 
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -27,7 +30,9 @@ import static org.junit.Assert.fail;
 
 import java.io.File;
 import java.lang.reflect.Method;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedList;
@@ -44,21 +49,31 @@ import org.apache.ambari.server.AmbariException;
 import org.apache.ambari.server.StackAccessException;
 import org.apache.ambari.server.api.util.StackExtensionHelper;
 import org.apache.ambari.server.metadata.ActionMetadata;
+import org.apache.ambari.server.orm.InMemoryDefaultTestModule;
+import org.apache.ambari.server.orm.dao.AlertDefinitionDAO;
+import org.apache.ambari.server.orm.entities.AlertDefinitionEntity;
 import org.apache.ambari.server.state.AutoDeployInfo;
+import org.apache.ambari.server.state.Cluster;
+import org.apache.ambari.server.state.Clusters;
 import org.apache.ambari.server.state.ComponentInfo;
 import org.apache.ambari.server.state.CustomCommandDefinition;
 import org.apache.ambari.server.state.DependencyInfo;
 import org.apache.ambari.server.state.OperatingSystemInfo;
 import org.apache.ambari.server.state.PropertyInfo;
 import org.apache.ambari.server.state.RepositoryInfo;
+import org.apache.ambari.server.state.Service;
 import org.apache.ambari.server.state.ServiceInfo;
 import org.apache.ambari.server.state.Stack;
+import org.apache.ambari.server.state.StackId;
 import org.apache.ambari.server.state.StackInfo;
 import org.apache.ambari.server.state.alert.AlertDefinition;
+import org.apache.ambari.server.state.alert.AlertDefinitionFactory;
+import org.apache.ambari.server.state.alert.PortSource;
 import org.apache.ambari.server.state.alert.Reporting;
 import org.apache.ambari.server.state.alert.Source;
 import org.apache.ambari.server.state.stack.MetricDefinition;
 import org.apache.commons.io.FileUtils;
+import org.easymock.EasyMock;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -69,6 +84,7 @@ import org.slf4j.LoggerFactory;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
+import com.google.inject.util.Modules;
 
 public class AmbariMetaInfoTest {
 
@@ -107,7 +123,8 @@ public class AmbariMetaInfoTest {
 
   @Before
   public void before() throws Exception {
-    injector = Guice.createInjector(new MockModule());
+    injector = Guice.createInjector(Modules.override(
+        new InMemoryDefaultTestModule()).with(new MockModule()));
 
     File stackRoot = new File("src/test/resources/stacks");
     LOG.info("Stacks file " + stackRoot.getAbsolutePath());
@@ -1444,10 +1461,10 @@ public class AmbariMetaInfoTest {
 
     assertEquals("NameNode host CPU Utilization", nameNodeCpu.getLabel());
 
+    // test namenode_process
     Source source = nameNodeProcess.getSource();
     assertNotNull(source);
-
-    // test namenode_process
+    assertNotNull(((PortSource) source).getPort());
     Reporting reporting = source.getReporting();
     assertNotNull(reporting);
     assertNotNull(reporting.getOk());
@@ -1460,6 +1477,7 @@ public class AmbariMetaInfoTest {
 
     // test namenode_cpu
     source = nameNodeCpu.getSource();
+    assertNotNull(source);
     reporting = source.getReporting();
     assertNotNull(reporting);
     assertNotNull(reporting.getOk());
@@ -1472,4 +1490,59 @@ public class AmbariMetaInfoTest {
     assertNotNull(reporting.getWarning().getText());
     assertNotNull(reporting.getWarning().getValue());
   }
+
+  /**
+   * @throws Exception
+   */
+  @Test
+  public void testAlertReconcile() throws Exception {
+    Clusters clusters = createMock(Clusters.class);
+    Cluster cluster = createMock(Cluster.class);
+    AlertDefinitionDAO dao = createMock(AlertDefinitionDAO.class);
+
+    metaInfo.alertDefinitionDao = dao;
+    Set<AlertDefinition> alertDefinitions = metaInfo.getAlertDefinitions(
+        STACK_NAME_HDP, "2.0.6", "HDFS");
+
+    assertEquals(4, alertDefinitions.size());
+
+    Map<String, Cluster> clustersMap = new HashMap<String, Cluster>();
+    clustersMap.put("c1", cluster);
+
+    StackId stackId = new StackId(STACK_NAME_HDP, "2.0.6");
+    Map<String, Service> clusterServiceMap = new HashMap<String, Service>();
+    clusterServiceMap.put("HDFS", null);
+
+    List<AlertDefinitionEntity> entities = new ArrayList<AlertDefinitionEntity>();
+
+    expect(clusters.getClusters()).andReturn(clustersMap).anyTimes();
+    expect(clusters.getCluster((String) anyObject())).andReturn(cluster).atLeastOnce();
+    expect(cluster.getClusterId()).andReturn(Long.valueOf(1)).anyTimes();
+    expect(cluster.getDesiredStackVersion()).andReturn(stackId).anyTimes();
+    expect(cluster.getServices()).andReturn(clusterServiceMap).anyTimes();
+    expect(dao.findAll(EasyMock.anyInt())).andReturn(entities);
+    dao.createOrUpdate(EasyMock.anyObject(AlertDefinitionEntity.class));
+    EasyMock.expectLastCall().times(4);
+
+    EasyMock.replay(clusters, cluster, dao);
+    metaInfo.reconcileAlertDefinitions(clusters);
+    EasyMock.verify(cluster, cluster, dao);
+
+    AlertDefinitionFactory alertDefinitionFactory = injector.getInstance(AlertDefinitionFactory.class);
+    for (AlertDefinition definition : alertDefinitions) {
+      entities.add(alertDefinitionFactory.coerce(1, definition));
+    }
+
+    EasyMock.reset(clusters, cluster, dao);
+    expect(clusters.getClusters()).andReturn(clustersMap).anyTimes();
+    expect(clusters.getCluster((String) anyObject())).andReturn(cluster).atLeastOnce();
+    expect(cluster.getClusterId()).andReturn(Long.valueOf(1)).anyTimes();
+    expect(cluster.getDesiredStackVersion()).andReturn(stackId).anyTimes();
+    expect(cluster.getServices()).andReturn(clusterServiceMap).anyTimes();
+    expect(dao.findAll(EasyMock.anyInt())).andReturn(entities);
+
+    EasyMock.replay(clusters, cluster, dao);
+    metaInfo.reconcileAlertDefinitions(clusters);
+    EasyMock.verify(cluster, cluster, dao);
+  }
 }

+ 2 - 1
ambari-server/src/test/java/org/apache/ambari/server/controller/internal/AlertDefinitionResourceProviderTest.java

@@ -55,6 +55,7 @@ import org.apache.ambari.server.state.Clusters;
 import org.apache.ambari.server.state.alert.AlertDefinition;
 import org.apache.ambari.server.state.alert.AlertDefinitionFactory;
 import org.apache.ambari.server.state.alert.AlertDefinitionHash;
+import org.apache.ambari.server.state.alert.Scope;
 import org.apache.ambari.server.state.alert.Source;
 import org.apache.ambari.server.state.alert.SourceType;
 import org.easymock.Capture;
@@ -261,7 +262,7 @@ public class AlertDefinitionResourceProviderTest {
     Assert.assertTrue(entity.getEnabled());
     Assert.assertNotNull(entity.getHash());
     Assert.assertEquals(Integer.valueOf(1), entity.getScheduleInterval());
-    Assert.assertNull(entity.getScope());
+    Assert.assertEquals(Scope.ANY, entity.getScope());
     Assert.assertEquals("HDFS", entity.getServiceName());
     Assert.assertEquals("METRIC", entity.getSourceType());
     Assert.assertEquals("Mock Label (Create)", entity.getLabel());

+ 157 - 0
ambari-server/src/test/java/org/apache/ambari/server/state/alerts/AlertDefinitionEqualityTest.java

@@ -0,0 +1,157 @@
+/**
+ * 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.state.alerts;
+
+import junit.framework.TestCase;
+
+import org.apache.ambari.server.state.AlertState;
+import org.apache.ambari.server.state.alert.AggregateSource;
+import org.apache.ambari.server.state.alert.AlertDefinition;
+import org.apache.ambari.server.state.alert.MetricSource;
+import org.apache.ambari.server.state.alert.PercentSource;
+import org.apache.ambari.server.state.alert.PortSource;
+import org.apache.ambari.server.state.alert.Reporting;
+import org.apache.ambari.server.state.alert.Reporting.ReportTemplate;
+import org.apache.ambari.server.state.alert.Scope;
+import org.apache.ambari.server.state.alert.ScriptSource;
+import org.apache.ambari.server.state.alert.Source;
+import org.apache.ambari.server.state.alert.SourceType;
+import org.junit.Test;
+
+/**
+ * Tests equality of {@link AlertDefinition} for hashing and merging purposes.
+ */
+public class AlertDefinitionEqualityTest extends TestCase {
+
+  @Test
+  public void testAlertDefinitionEquality() {
+    AlertDefinition ad1 = getAlertDefinition(SourceType.PORT);
+    AlertDefinition ad2 = getAlertDefinition(SourceType.PORT);
+
+    assertTrue(ad1.equals(ad2));
+    assertTrue(ad1.deeplyEquals(ad2));
+
+    // change 1 property and check that name equality still works
+    ad2.setInterval(2);
+    assertTrue(ad1.equals(ad2));
+    assertFalse(ad1.deeplyEquals(ad2));
+
+    // change the name and verify name equality is broken
+    ad2.setName(getName() + " foo");
+    assertFalse(ad1.equals(ad2));
+    assertFalse(ad1.deeplyEquals(ad2));
+
+    ad2 = getAlertDefinition(SourceType.AGGREGATE);
+    assertFalse(ad1.deeplyEquals(ad2));
+
+    ad2 = getAlertDefinition(SourceType.PORT);
+    assertTrue(ad1.deeplyEquals(ad2));
+
+    ad2.getSource().getReporting().getOk().setText("foo");
+    assertFalse(ad1.deeplyEquals(ad2));
+  }
+
+  /**
+   * @param sourceType
+   * @return
+   */
+  private AlertDefinition getAlertDefinition(SourceType sourceType) {
+    AlertDefinition definition = new AlertDefinition();
+    definition.setComponentName("component");
+    definition.setEnabled(true);
+    definition.setInterval(1);
+    definition.setName("Name");
+    definition.setScope(Scope.ANY);
+    definition.setServiceName("ServiceName");
+    definition.setLabel("Label");
+    definition.setSource(getSource(sourceType));
+
+    return definition;
+  }
+
+  /**
+   * @param type
+   * @return
+   */
+  private Source getSource(SourceType type) {
+    Source source = null;
+    switch (type) {
+      case AGGREGATE:
+        source = new AggregateSource();
+        ((AggregateSource) source).setAlertName("hdfs-foo");
+        break;
+      case METRIC:
+        source = new MetricSource();
+        break;
+      case PERCENT:
+        source = new PercentSource();
+        break;
+      case PORT:
+        source = new PortSource();
+        ((PortSource) source).setPort(80);
+        ((PortSource) source).setUri("uri://foo");
+        break;
+      case SCRIPT:
+        source = new ScriptSource();
+        break;
+      default:
+        break;
+    }
+
+    source.setReporting(getReporting());
+    return source;
+  }
+
+  /**
+   * @return
+   */
+  private Reporting getReporting() {
+    Reporting reporting = new Reporting();
+    reporting.setCritical(getReportingTemplate(AlertState.CRITICAL));
+    reporting.setWarning(getReportingTemplate(AlertState.WARNING));
+    reporting.setOk(getReportingTemplate(AlertState.OK));
+
+    return reporting;
+  }
+
+  /**
+   * @param state
+   * @return
+   */
+  private ReportTemplate getReportingTemplate(AlertState state) {
+    ReportTemplate template = new ReportTemplate();
+    switch (state) {
+      case CRITICAL:
+        template.setText("OH NO!");
+        template.setValue(80.0);
+        break;
+      case OK:
+        template.setText("No worries.");
+        break;
+      case UNKNOWN:
+        break;
+      case WARNING:
+        template.setText("Getting nervous...");
+        template.setValue(50.0);
+        break;
+      default:
+        break;
+    }
+    return template;
+  }
+}

+ 4 - 4
ambari-server/src/test/resources/stacks/HDP/2.0.5/services/HDFS/alerts.json

@@ -28,11 +28,11 @@
       "name": "namenode_process",
       "label": "NameNode process",
       "interval": 1,
-      "scope": "host",
+      "scope": "any",
       "source": {
         "type": "PORT",
         "uri": "{{hdfs-site/dfs.namenode.http-address}}",
-        "port": 50070,
+        "default_port": 50070,
         "reporting": {
           "ok": {
             "text": "TCP OK - {0:.4f} response on port {1}"
@@ -60,11 +60,11 @@
       "name": "secondary_namenode_process",
       "label": "Secondary NameNode process",
       "interval": 1,
-      "scope": "service",
+      "scope": "any",
       "source": {
         "type": "PORT",        
         "uri": "{{hdfs-site/dfs.namenode.secondary.http-address}}",
-        "port": 50070
+        "default_port": 50070
       }
     }
   ],