diff --git a/client/pom.xml b/client/pom.xml
index 7d37225dbbb3..d1fb1eab06c0 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -422,6 +422,11 @@
cloud-mom-webhook
${project.version}
+
+ org.apache.cloudstack
+ cloud-plugin-resource-alerts
+ ${project.version}
+
org.apache.cloudstack
cloud-framework-agent-lb
diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
index bd5ecbab21ca..6ff6da06b900 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
@@ -219,3 +219,46 @@ WHERE rule = 'quotaStatement' AND NOT EXISTS(SELECT 1 FROM cloud.role_permission
-- Add description for secondary IP addresses
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.nic_secondary_ips', 'description', 'VARCHAR(2048) DEFAULT NULL');
+
+-- resource_alert_rules: stores per-resource or generic metric threshold rules
+CREATE TABLE IF NOT EXISTS `cloud`.`resource_alert_rules` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `uuid` varchar(255) NOT NULL UNIQUE,
+ `name` varchar(255) NOT NULL,
+ `resource_type` varchar(64) NOT NULL COMMENT 'VirtualMachine, Volume, Host, StoragePool',
+ `resource_id` bigint unsigned DEFAULT NULL COMMENT 'null = applies to all resources of the type in scope',
+ `account_id` bigint unsigned NOT NULL,
+ `domain_id` bigint unsigned NOT NULL,
+ `metric` varchar(64) NOT NULL,
+ `condition_operator` varchar(8) NOT NULL COMMENT 'GT, GTE, LT, LTE, EQ',
+ `threshold` double NOT NULL,
+ `severity` varchar(32) NOT NULL COMMENT 'CRITICAL, HIGH, MEDIUM, LOW',
+ `message` varchar(4096) DEFAULT NULL,
+ `email` tinyint(1) NOT NULL DEFAULT 0,
+ `reset_interval` int unsigned NOT NULL DEFAULT 600 COMMENT 'minimum seconds between repeat firings of this rule',
+ `created` datetime DEFAULT NULL,
+ `updated` datetime DEFAULT NULL,
+ `removed` datetime DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ INDEX `i_resource_alert_rules__account_id`(`account_id`),
+ INDEX `i_resource_alert_rules__domain_id`(`domain_id`),
+ CONSTRAINT `fk_resource_alert_rules__account_id` FOREIGN KEY (`account_id`) REFERENCES `account`(`id`) ON DELETE CASCADE,
+ CONSTRAINT `fk_resource_alert_rules__domain_id` FOREIGN KEY (`domain_id`) REFERENCES `domain`(`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- resource_alerts: immutable log of fired alerts
+CREATE TABLE IF NOT EXISTS `cloud`.`resource_alerts` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `uuid` varchar(255) NOT NULL UNIQUE,
+ `alert_rule_id` bigint unsigned NOT NULL,
+ `resource_id` bigint unsigned DEFAULT NULL COMMENT 'the specific resource that triggered the alert',
+ `metric_type` varchar(64) NOT NULL,
+ `metric_value` double NOT NULL,
+ `severity` varchar(32) NOT NULL,
+ `message` varchar(4096) DEFAULT NULL,
+ `alert_timestamp` datetime NOT NULL,
+ PRIMARY KEY (`id`),
+ INDEX `i_resource_alerts__alert_rule_id`(`alert_rule_id`),
+ INDEX `i_resource_alerts__alert_timestamp`(`alert_timestamp`),
+ CONSTRAINT `fk_resource_alerts__alert_rule_id` FOREIGN KEY (`alert_rule_id`) REFERENCES `resource_alert_rules`(`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.resource_alert_rule_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.resource_alert_rule_view.sql
new file mode 100644
index 000000000000..e0f74dd34543
--- /dev/null
+++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.resource_alert_rule_view.sql
@@ -0,0 +1,48 @@
+-- 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.
+
+-- VIEW `cloud`.`resource_alert_rule_view`;
+
+DROP VIEW IF EXISTS `cloud`.`resource_alert_rule_view`;
+CREATE VIEW `cloud`.`resource_alert_rule_view` AS
+ SELECT
+ r.id,
+ r.uuid,
+ r.name,
+ r.resource_type,
+ r.resource_id,
+ r.metric,
+ r.condition_operator,
+ r.threshold,
+ r.severity,
+ r.message,
+ r.email,
+ r.reset_interval,
+ r.created,
+ r.updated,
+ r.removed,
+ a.id account_id,
+ a.uuid account_uuid,
+ a.account_name,
+ a.type account_type,
+ d.id domain_id,
+ d.uuid domain_uuid,
+ d.name domain_name,
+ d.path domain_path
+ FROM `cloud`.`resource_alert_rules` r
+ INNER JOIN `cloud`.`account` a ON r.account_id = a.id
+ INNER JOIN `cloud`.`domain` d ON r.domain_id = d.id;
diff --git a/plugins/pom.xml b/plugins/pom.xml
index 6e2f20c9a7d7..1f4f9b2bc021 100755
--- a/plugins/pom.xml
+++ b/plugins/pom.xml
@@ -98,6 +98,8 @@
metrics
+ resource-alerts
+
network-elements/bigswitch
network-elements/dns-notifier
network-elements/elastic-loadbalancer
diff --git a/plugins/resource-alerts/pom.xml b/plugins/resource-alerts/pom.xml
new file mode 100644
index 000000000000..83d28642320c
--- /dev/null
+++ b/plugins/resource-alerts/pom.xml
@@ -0,0 +1,37 @@
+
+
+ 4.0.0
+ cloud-plugin-resource-alerts
+ Apache CloudStack Plugin - Resource Alerts
+
+ org.apache.cloudstack
+ cloudstack-plugins
+ 4.23.0.0-SNAPSHOT
+ ../pom.xml
+
+
+
+ org.apache.cloudstack
+ cloud-engine-schema
+ ${project.version}
+
+
+
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/AlertCondition.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/AlertCondition.java
new file mode 100644
index 000000000000..b1ff4ffc3192
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/AlertCondition.java
@@ -0,0 +1,33 @@
+// 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.cloudstack.resourcealert;
+
+public enum AlertCondition {
+ GT, GTE, LT, LTE, EQ;
+
+ public boolean evaluate(double value, double threshold) {
+ switch (this) {
+ case GT: return value > threshold;
+ case GTE: return value >= threshold;
+ case LT: return value < threshold;
+ case LTE: return value <= threshold;
+ case EQ: return Double.compare(value, threshold) == 0;
+ default: return false;
+ }
+ }
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/AlertSeverity.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/AlertSeverity.java
new file mode 100644
index 000000000000..bf0c48e9e0d0
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/AlertSeverity.java
@@ -0,0 +1,22 @@
+// 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.cloudstack.resourcealert;
+
+public enum AlertSeverity {
+ CRITICAL, HIGH, MEDIUM, LOW
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlert.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlert.java
new file mode 100644
index 000000000000..7012f85db008
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlert.java
@@ -0,0 +1,34 @@
+// 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.cloudstack.resourcealert;
+
+import java.util.Date;
+
+import org.apache.cloudstack.api.Identity;
+import org.apache.cloudstack.api.InternalIdentity;
+
+public interface ResourceAlert extends Identity, InternalIdentity {
+
+ long getAlertRuleId();
+ Long getResourceId();
+ String getMetricType();
+ double getMetricValue();
+ AlertSeverity getSeverity();
+ String getMessage();
+ Date getAlertTimestamp();
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertManager.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertManager.java
new file mode 100644
index 000000000000..f055dea902fc
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertManager.java
@@ -0,0 +1,22 @@
+// 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.cloudstack.resourcealert;
+
+public interface ResourceAlertManager {
+ void evaluateRules();
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertManagerImpl.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertManagerImpl.java
new file mode 100644
index 000000000000..54adafc52d2b
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertManagerImpl.java
@@ -0,0 +1,406 @@
+// 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.cloudstack.resourcealert;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.function.ToDoubleFunction;
+import java.util.stream.Collectors;
+
+import javax.inject.Inject;
+import javax.naming.ConfigurationException;
+
+import org.apache.cloudstack.framework.config.ConfigKey;
+import org.apache.cloudstack.framework.config.Configurable;
+import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
+import org.apache.cloudstack.resourcealert.dao.ResourceAlertDao;
+import org.apache.cloudstack.resourcealert.dao.ResourceAlertRuleDao;
+import org.apache.cloudstack.resourcealert.vo.ResourceAlertRuleVO;
+import org.apache.cloudstack.resourcealert.vo.ResourceAlertVO;
+import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
+import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
+import org.apache.cloudstack.utils.mailing.MailAddress;
+import org.apache.cloudstack.utils.mailing.SMTPMailProperties;
+import org.apache.cloudstack.utils.mailing.SMTPMailSender;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import com.cloud.event.AlertGenerator;
+import com.cloud.host.Host;
+import com.cloud.host.HostStats;
+import com.cloud.host.HostVO;
+import com.cloud.host.dao.HostDao;
+import com.cloud.server.ResourceTag;
+import com.cloud.server.StatsCollector;
+import com.cloud.storage.StorageStats;
+import com.cloud.storage.VolumeVO;
+import com.cloud.storage.dao.VolumeDao;
+import com.cloud.tags.dao.ResourceTagDao;
+import com.cloud.utils.component.ManagerBase;
+import com.cloud.vm.UserVmVO;
+import com.cloud.vm.VirtualMachine;
+import com.cloud.vm.VmStats;
+import com.cloud.vm.dao.UserVmDao;
+
+public class ResourceAlertManagerImpl extends ManagerBase implements ResourceAlertManager, Configurable {
+
+ static final ConfigKey EVAL_INTERVAL = new ConfigKey<>("Advanced", Integer.class,
+ "resource.alert.evaluation.interval", "60",
+ "Interval in seconds between resource alert rule evaluations", true);
+
+ public static final ConfigKey RULES_PER_ACCOUNT_LIMIT = new ConfigKey<>("Advanced", Integer.class,
+ "resource.alert.rules.per.account", "20",
+ "Maximum number of resource alert rules per account; 0 = unlimited", true);
+
+ @Inject ResourceAlertRuleDao ruleDao;
+ @Inject ResourceAlertDao alertDao;
+ @Inject UserVmDao userVmDao;
+ @Inject HostDao hostDao;
+ @Inject PrimaryDataStoreDao storagePoolDao;
+ @Inject VolumeDao volumeDao;
+ @Inject StatsCollector statsCollector;
+ @Inject ConfigurationDao configDao;
+ @Inject ResourceTagDao resourceTagDao;
+
+ private ScheduledExecutorService executor;
+ ExecutorService emailExecutor = Executors.newCachedThreadPool(r -> {
+ Thread t = new Thread(r, "ResourceAlertEmailSender");
+ t.setDaemon(true);
+ return t;
+ });
+
+ private SMTPMailSender mailSender;
+ private String[] emailRecipients;
+ private String senderAddress;
+
+ @Override
+ public boolean configure(String name, Map params) throws ConfigurationException {
+ String emailList = configDao.getValue("alert.email.addresses");
+ if (StringUtils.isNotBlank(emailList)) {
+ emailRecipients = emailList.split(",");
+ }
+ senderAddress = configDao.getValue("alert.email.sender");
+
+ Map smtpConfigs = new HashMap<>();
+ for (String key : new String[]{
+ "alert.smtp.host", "alert.smtp.port", "alert.smtp.useAuth",
+ "alert.smtp.username", "alert.smtp.password", "alert.smtp.useStartTLS",
+ "alert.smtp.enabledSecurityProtocols", "alert.smtp.timeout", "alert.smtp.connectiontimeout"}) {
+ String val = configDao.getValue(key);
+ if (val != null) smtpConfigs.put(key, val);
+ }
+ mailSender = new SMTPMailSender(smtpConfigs, "alert.smtp");
+
+ return super.configure(name, params);
+ }
+
+ @Override
+ public boolean start() {
+ int interval = EVAL_INTERVAL.value();
+ executor = Executors.newSingleThreadScheduledExecutor(r -> {
+ Thread t = new Thread(r, "ResourceAlertEvaluator");
+ t.setDaemon(true);
+ return t;
+ });
+ executor.scheduleAtFixedRate(this::safeEvaluateRules, interval, interval, TimeUnit.SECONDS);
+ return true;
+ }
+
+ @Override
+ public boolean stop() {
+ if (executor != null) {
+ executor.shutdown();
+ }
+ emailExecutor.shutdown();
+ return true;
+ }
+
+ @Override
+ public void evaluateRules() {
+ List rules = ruleDao.listActive();
+ for (ResourceAlertRuleVO rule : rules) {
+ evaluateRule(rule);
+ }
+ }
+
+ private void safeEvaluateRules() {
+ try {
+ evaluateRules();
+ } catch (Exception ignored) {
+ }
+ }
+
+ private void evaluateRule(ResourceAlertRuleVO rule) {
+ ResourceAlertMetric metric = ResourceAlertMetric.valueOf(rule.getMetric());
+ boolean isGeneric = rule.getResourceId() == null;
+ for (Long resourceId : getResourceIds(rule)) {
+ try {
+ if (isGeneric) {
+ if (isOptedOut(rule.getResourceType(), resourceId)) continue;
+ if (ruleDao.existsSpecificRule(rule.getResourceType(), rule.getMetric(), resourceId)) continue;
+ }
+ Double value = getMetricValue(rule.getResourceType(), metric, resourceId);
+ if (value == null || value < 0) {
+ continue;
+ }
+ if (rule.getCondition().evaluate(value, rule.getThreshold())
+ && canFire(rule.getId(), resourceId, rule.getResetInterval())) {
+ fireAlert(rule, resourceId, value);
+ }
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ private boolean isOptedOut(ResourceAlertRule.ResourceType type, long resourceId) {
+ ResourceTag.ResourceObjectType objType = null;
+ if (type == ResourceAlertRule.ResourceType.VirtualMachine) {
+ objType = ResourceTag.ResourceObjectType.UserVm;
+ } else if (type == ResourceAlertRule.ResourceType.Volume) {
+ objType = ResourceTag.ResourceObjectType.Volume;
+ }
+ if (objType == null) return false;
+ ResourceTag tag = resourceTagDao.findByKey(resourceId, objType, "resource.alert.opt.out");
+ return tag != null && "true".equalsIgnoreCase(tag.getValue());
+ }
+
+ private List getResourceIds(ResourceAlertRuleVO rule) {
+ if (rule.getResourceId() != null) {
+ return Collections.singletonList(rule.getResourceId());
+ }
+ switch (rule.getResourceType()) {
+ case VirtualMachine:
+ return userVmDao.listByAccountId(rule.getAccountId()).stream()
+ .filter(vm -> VirtualMachine.State.Running.equals(vm.getState()))
+ .map(vm -> vm.getId())
+ .collect(Collectors.toList());
+ case Volume:
+ return volumeDao.findByAccount(rule.getAccountId()).stream()
+ .map(v -> v.getId())
+ .collect(Collectors.toList());
+ case Host:
+ return hostDao.listAll().stream()
+ .filter(h -> Host.Type.Routing.equals(h.getType()))
+ .map(h -> h.getId())
+ .collect(Collectors.toList());
+ case StoragePool:
+ return storagePoolDao.listAll().stream()
+ .map(p -> p.getId())
+ .collect(Collectors.toList());
+ default:
+ return Collections.emptyList();
+ }
+ }
+
+ private Double getMetricValue(ResourceAlertRule.ResourceType type, ResourceAlertMetric metric, long resourceId) {
+ switch (metric) {
+ case CPU_UTILIZATION:
+ if (type == ResourceAlertRule.ResourceType.VirtualMachine) {
+ VmStats s = statsCollector.getVmStats(resourceId, false);
+ return s != null ? s.getCPUUtilization() : null;
+ }
+ if (type == ResourceAlertRule.ResourceType.Host) {
+ HostStats s = statsCollector.getHostStats(resourceId);
+ return s != null ? s.getCpuUtilization() : null;
+ }
+ break;
+ case MEMORY_UTILIZATION:
+ if (type == ResourceAlertRule.ResourceType.VirtualMachine) {
+ VmStats s = statsCollector.getVmStats(resourceId, false);
+ if (s == null) return null;
+ double total = s.getMemoryKBs();
+ double free = s.getIntFreeMemoryKBs();
+ // free is -1 when VM has no balloon driver
+ if (total <= 0 || free < 0) return null;
+ return (1.0 - free / total) * 100.0;
+ }
+ if (type == ResourceAlertRule.ResourceType.Host) {
+ HostStats s = statsCollector.getHostStats(resourceId);
+ if (s == null) return null;
+ double total = s.getTotalMemoryKBs();
+ double free = s.getFreeMemoryKBs();
+ if (total <= 0) return null;
+ return ((total - free) / total) * 100.0;
+ }
+ break;
+ case DISK_READ_IOPS:
+ return getVmDiskStat(type, resourceId, s -> s.getDiskReadIOs());
+ case DISK_WRITE_IOPS:
+ return getVmDiskStat(type, resourceId, s -> s.getDiskWriteIOs());
+ case DISK_READ_KBPS:
+ return getVmDiskStat(type, resourceId, s -> s.getDiskReadKBs());
+ case DISK_WRITE_KBPS:
+ return getVmDiskStat(type, resourceId, s -> s.getDiskWriteKBs());
+ case NETWORK_READ_KBPS: {
+ VmStats s = statsCollector.getVmStats(resourceId, false);
+ return s != null ? s.getNetworkReadKBs() : null;
+ }
+ case NETWORK_WRITE_KBPS: {
+ VmStats s = statsCollector.getVmStats(resourceId, false);
+ return s != null ? s.getNetworkWriteKBs() : null;
+ }
+ case STORAGE_UTILIZATION: {
+ StorageStats pool = statsCollector.getStoragePoolStats(resourceId);
+ if (pool == null || pool.getCapacityBytes() <= 0) return null;
+ return ((double) pool.getByteUsed() / pool.getCapacityBytes()) * 100.0;
+ }
+ default:
+ break;
+ }
+ return null;
+ }
+
+ // For volume rules, resolve the attached VM and use its aggregate disk stats.
+ private Double getVmDiskStat(ResourceAlertRule.ResourceType type, long resourceId, ToDoubleFunction extractor) {
+ long vmId = resourceId;
+ if (type == ResourceAlertRule.ResourceType.Volume) {
+ VolumeVO vol = volumeDao.findById(resourceId);
+ if (vol == null || vol.getInstanceId() == null) return null;
+ vmId = vol.getInstanceId();
+ }
+ VmStats s = statsCollector.getVmStats(vmId, false);
+ return s != null ? extractor.applyAsDouble(s) : null;
+ }
+
+ private boolean canFire(long ruleId, Long resourceId, int resetInterval) {
+ ResourceAlertVO last = alertDao.findLastFiredForRule(ruleId, resourceId);
+ if (last == null) return true;
+ long secondsSinceLast = (System.currentTimeMillis() - last.getAlertTimestamp().getTime()) / 1000;
+ return secondsSinceLast >= resetInterval;
+ }
+
+ private void fireAlert(ResourceAlertRuleVO rule, Long resourceId, double value) {
+ ResourceAlertVO alert = new ResourceAlertVO(
+ rule.getId(), resourceId, rule.getMetric(), value, rule.getSeverity(),
+ rule.getMessage(), new Date());
+ alertDao.persist(alert);
+
+ String subject = buildSubject(rule, resourceId, value);
+ String body = buildBody(rule, resourceId, value);
+ long dcId = getDataCenterId(rule.getResourceType(), resourceId);
+ publishAlertEvent(dcId, subject, body);
+
+ if (rule.isEmail()) {
+ sendEmail(subject, body);
+ }
+
+ logger.warn("Alert fired: rule={} metric={} resource={} value={} threshold={}",
+ rule.getUuid(), rule.getMetric(), resourceId, value, rule.getThreshold());
+ }
+
+ private String buildSubject(ResourceAlertRuleVO rule, Long resourceId, double value) {
+ return String.format("[%s] Resource Alert: %s %s %.2f on %s %s",
+ rule.getSeverity().name(),
+ rule.getMetric(),
+ rule.getCondition().name(),
+ rule.getThreshold(),
+ rule.getResourceType().name(),
+ resourceId);
+ }
+
+ private String buildBody(ResourceAlertRuleVO rule, Long resourceId, double value) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Rule: ").append(rule.getName()).append('\n');
+ sb.append("Resource Type: ").append(rule.getResourceType().name()).append('\n');
+ sb.append("Resource ID: ").append(resourceId).append('\n');
+ sb.append("Metric: ").append(rule.getMetric()).append('\n');
+ sb.append(String.format("Condition: %s %.2f%n", rule.getCondition().name(), rule.getThreshold()));
+ sb.append(String.format("Current Value: %.2f%n", value));
+ sb.append("Severity: ").append(rule.getSeverity().name()).append('\n');
+ if (StringUtils.isNotBlank(rule.getMessage())) {
+ sb.append("Message: ").append(rule.getMessage()).append('\n');
+ }
+ return sb.toString();
+ }
+
+ private long getDataCenterId(ResourceAlertRule.ResourceType type, long resourceId) {
+ try {
+ switch (type) {
+ case VirtualMachine: {
+ UserVmVO vm = userVmDao.findById(resourceId);
+ return vm != null ? vm.getDataCenterId() : 0L;
+ }
+ case Volume: {
+ VolumeVO vol = volumeDao.findById(resourceId);
+ return vol != null ? vol.getDataCenterId() : 0L;
+ }
+ case Host: {
+ HostVO host = hostDao.findById(resourceId);
+ return host != null ? host.getDataCenterId() : 0L;
+ }
+ case StoragePool: {
+ StoragePoolVO pool = storagePoolDao.findById(resourceId);
+ return pool != null ? pool.getDataCenterId() : 0L;
+ }
+ default:
+ return 0L;
+ }
+ } catch (Exception e) {
+ return 0L;
+ }
+ }
+
+ private void sendEmail(String subject, String body) {
+ if (mailSender == null || ArrayUtils.isEmpty(emailRecipients)) {
+ return;
+ }
+ SMTPMailProperties mailProps = new SMTPMailProperties();
+ if (StringUtils.isNotBlank(senderAddress)) {
+ mailProps.setSender(new MailAddress(senderAddress));
+ }
+ mailProps.setSubject(subject);
+ mailProps.setContent(body);
+ mailProps.setContentType("text/plain");
+
+ Set addresses = new HashSet<>();
+ for (String recipient : emailRecipients) {
+ if (StringUtils.isNotBlank(recipient)) {
+ addresses.add(new MailAddress(recipient.trim()));
+ }
+ }
+ mailProps.setRecipients(addresses);
+ emailExecutor.execute(() -> mailSender.sendMail(mailProps));
+ }
+
+ // package-private so tests can stub it without needing a Spring context
+ void publishAlertEvent(long dcId, String subject, String body) {
+ try {
+ AlertGenerator.publishAlertOnEventBus("RESOURCE.ALERT", dcId, null, subject, body);
+ } catch (Exception ignored) {
+ }
+ }
+
+ @Override
+ public String getConfigComponentName() {
+ return ResourceAlertManagerImpl.class.getSimpleName();
+ }
+
+ @Override
+ public ConfigKey>[] getConfigKeys() {
+ return new ConfigKey>[]{EVAL_INTERVAL, RULES_PER_ACCOUNT_LIMIT};
+ }
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertMetric.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertMetric.java
new file mode 100644
index 000000000000..fefa05989cec
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertMetric.java
@@ -0,0 +1,44 @@
+// 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.cloudstack.resourcealert;
+
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.Set;
+
+public enum ResourceAlertMetric {
+ CPU_UTILIZATION(ResourceAlertRule.ResourceType.VirtualMachine, ResourceAlertRule.ResourceType.Host),
+ MEMORY_UTILIZATION(ResourceAlertRule.ResourceType.VirtualMachine, ResourceAlertRule.ResourceType.Host),
+ DISK_READ_IOPS(ResourceAlertRule.ResourceType.VirtualMachine, ResourceAlertRule.ResourceType.Volume),
+ DISK_WRITE_IOPS(ResourceAlertRule.ResourceType.VirtualMachine, ResourceAlertRule.ResourceType.Volume),
+ DISK_READ_KBPS(ResourceAlertRule.ResourceType.VirtualMachine, ResourceAlertRule.ResourceType.Volume),
+ DISK_WRITE_KBPS(ResourceAlertRule.ResourceType.VirtualMachine, ResourceAlertRule.ResourceType.Volume),
+ STORAGE_UTILIZATION(ResourceAlertRule.ResourceType.StoragePool),
+ NETWORK_READ_KBPS(ResourceAlertRule.ResourceType.VirtualMachine),
+ NETWORK_WRITE_KBPS(ResourceAlertRule.ResourceType.VirtualMachine);
+
+ private final Set applicableTypes;
+
+ ResourceAlertMetric(ResourceAlertRule.ResourceType... types) {
+ this.applicableTypes = EnumSet.copyOf(Arrays.asList(types));
+ }
+
+ public boolean appliesTo(ResourceAlertRule.ResourceType type) {
+ return applicableTypes.contains(type);
+ }
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertRule.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertRule.java
new file mode 100644
index 000000000000..6b45a4b20762
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertRule.java
@@ -0,0 +1,43 @@
+// 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.cloudstack.resourcealert;
+
+import java.util.Date;
+
+import org.apache.cloudstack.acl.ControlledEntity;
+import org.apache.cloudstack.api.Identity;
+import org.apache.cloudstack.api.InternalIdentity;
+
+public interface ResourceAlertRule extends ControlledEntity, Identity, InternalIdentity {
+
+ enum ResourceType {
+ VirtualMachine, Volume, Host, StoragePool
+ }
+
+ String getName();
+ ResourceType getResourceType();
+ Long getResourceId();
+ String getMetric();
+ AlertCondition getCondition();
+ double getThreshold();
+ AlertSeverity getSeverity();
+ String getMessage();
+ boolean isEmail();
+ int getResetInterval();
+ Date getCreated();
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertService.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertService.java
new file mode 100644
index 000000000000..48709da985e2
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertService.java
@@ -0,0 +1,38 @@
+// 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.cloudstack.resourcealert;
+
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.resourcealert.api.command.admin.CreateResourceAlertRuleCmd;
+import org.apache.cloudstack.resourcealert.api.command.admin.DeleteResourceAlertRuleCmd;
+import org.apache.cloudstack.resourcealert.api.command.admin.ListResourceAlertRulesCmd;
+import org.apache.cloudstack.resourcealert.api.command.admin.ListResourceAlertsCmd;
+import org.apache.cloudstack.resourcealert.api.command.admin.UpdateResourceAlertRuleCmd;
+import org.apache.cloudstack.resourcealert.api.response.ResourceAlertResponse;
+import org.apache.cloudstack.resourcealert.api.response.ResourceAlertRuleResponse;
+
+import com.cloud.utils.component.PluggableService;
+
+public interface ResourceAlertService extends PluggableService {
+
+ ResourceAlertRuleResponse createResourceAlertRule(CreateResourceAlertRuleCmd cmd);
+ ListResponse listResourceAlertRules(ListResourceAlertRulesCmd cmd);
+ ResourceAlertRuleResponse updateResourceAlertRule(UpdateResourceAlertRuleCmd cmd);
+ boolean deleteResourceAlertRule(DeleteResourceAlertRuleCmd cmd);
+ ListResponse listResourceAlerts(ListResourceAlertsCmd cmd);
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertServiceImpl.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertServiceImpl.java
new file mode 100644
index 000000000000..65b839d7fa4e
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertServiceImpl.java
@@ -0,0 +1,269 @@
+// 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.cloudstack.resourcealert;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.resourcealert.api.command.admin.CreateResourceAlertRuleCmd;
+import org.apache.cloudstack.resourcealert.api.command.admin.DeleteResourceAlertRuleCmd;
+import org.apache.cloudstack.resourcealert.api.command.admin.ListResourceAlertRulesCmd;
+import org.apache.cloudstack.resourcealert.api.command.admin.ListResourceAlertsCmd;
+import org.apache.cloudstack.resourcealert.api.command.admin.UpdateResourceAlertRuleCmd;
+import org.apache.cloudstack.resourcealert.api.response.ResourceAlertResponse;
+import org.apache.cloudstack.resourcealert.api.response.ResourceAlertRuleResponse;
+import org.apache.cloudstack.resourcealert.dao.ResourceAlertDao;
+import org.apache.cloudstack.resourcealert.dao.ResourceAlertRuleDao;
+import org.apache.cloudstack.resourcealert.dao.ResourceAlertRuleJoinDao;
+import org.apache.cloudstack.resourcealert.vo.ResourceAlertRuleJoinVO;
+import org.apache.cloudstack.resourcealert.vo.ResourceAlertRuleVO;
+import org.apache.cloudstack.resourcealert.vo.ResourceAlertVO;
+import org.apache.commons.lang3.EnumUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import com.cloud.domain.Domain;
+import com.cloud.domain.dao.DomainDao;
+import com.cloud.exception.InvalidParameterValueException;
+import com.cloud.user.Account;
+import com.cloud.user.AccountManager;
+import com.cloud.utils.component.ManagerBase;
+
+import org.apache.cloudstack.context.CallContext;
+
+public class ResourceAlertServiceImpl extends ManagerBase implements ResourceAlertService {
+
+ @Inject
+ AccountManager accountManager;
+ @Inject
+ DomainDao domainDao;
+ @Inject
+ ResourceAlertRuleDao ruleDao;
+ @Inject
+ ResourceAlertRuleJoinDao ruleJoinDao;
+ @Inject
+ ResourceAlertDao alertDao;
+
+ @Override
+ public ResourceAlertRuleResponse createResourceAlertRule(CreateResourceAlertRuleCmd cmd) {
+ ResourceAlertRule.ResourceType resourceType = parseResourceType(cmd.getResourceType());
+ AlertCondition condition = parseCondition(cmd.getCondition());
+ AlertSeverity severity = parseSeverity(cmd.getSeverity());
+ ResourceAlertMetric metric = parseMetric(cmd.getMetric(), resourceType);
+
+ int resetInterval = cmd.getResetInterval() != null ? cmd.getResetInterval() : 600;
+ boolean email = cmd.getEmail() != null && cmd.getEmail();
+
+ Account owner = resolveOwner(cmd.getAccountName(), cmd.getDomainId());
+
+ int limit = ResourceAlertManagerImpl.RULES_PER_ACCOUNT_LIMIT.value();
+ if (limit > 0 && ruleDao.countActiveByAccountId(owner.getId()) >= limit) {
+ throw new InvalidParameterValueException(
+ "Account has reached the maximum of " + limit + " resource alert rules");
+ }
+ long domainId = owner.getDomainId();
+
+ ResourceAlertRuleVO rule = new ResourceAlertRuleVO(
+ cmd.getName(), resourceType, cmd.getResourceId(),
+ owner.getId(), domainId,
+ metric.name(), condition, cmd.getThreshold(), severity,
+ cmd.getMessage(), email, resetInterval);
+
+ ruleDao.persist(rule);
+ return toRuleResponse(ruleJoinDao.findById(rule.getId()));
+ }
+
+ @Override
+ public ListResponse listResourceAlertRules(ListResourceAlertRulesCmd cmd) {
+ Long offset = cmd.getStartIndex();
+ Long limit = cmd.getPageSizeVal();
+
+ List rules = ruleJoinDao.searchByFilters(
+ cmd.getId(), cmd.getRuleName(), cmd.getResourceType(),
+ cmd.getResourceId(), cmd.getAccountName(), cmd.getDomainId(),
+ offset, limit);
+
+ int count = ruleJoinDao.countByFilters(
+ cmd.getId(), cmd.getRuleName(), cmd.getResourceType(),
+ cmd.getResourceId(), cmd.getAccountName(), cmd.getDomainId());
+
+ List responses = rules.stream()
+ .map(this::toRuleResponse)
+ .collect(Collectors.toList());
+
+ ListResponse response = new ListResponse<>();
+ response.setResponses(responses, count);
+ return response;
+ }
+
+ @Override
+ public ResourceAlertRuleResponse updateResourceAlertRule(UpdateResourceAlertRuleCmd cmd) {
+ ResourceAlertRuleVO rule = ruleDao.findById(cmd.getId());
+ if (rule == null || rule.getRemoved() != null) {
+ throw new InvalidParameterValueException("Alert rule not found");
+ }
+
+ if (StringUtils.isNotBlank(cmd.getName())) rule.setName(cmd.getName());
+ if (StringUtils.isNotBlank(cmd.getCondition())) rule.setCondition(parseCondition(cmd.getCondition()));
+ if (cmd.getThreshold() != null) rule.setThreshold(cmd.getThreshold());
+ if (StringUtils.isNotBlank(cmd.getSeverity())) rule.setSeverity(parseSeverity(cmd.getSeverity()));
+ if (cmd.getMessage() != null) rule.setMessage(cmd.getMessage());
+ if (cmd.getEmail() != null) rule.setEmail(cmd.getEmail());
+ if (cmd.getResetInterval() != null) rule.setResetInterval(cmd.getResetInterval());
+ rule.setUpdated(new Date());
+
+ ruleDao.update(rule.getId(), rule);
+ return toRuleResponse(ruleJoinDao.findById(rule.getId()));
+ }
+
+ @Override
+ public boolean deleteResourceAlertRule(DeleteResourceAlertRuleCmd cmd) {
+ ResourceAlertRuleVO rule = ruleDao.findById(cmd.getId());
+ if (rule == null || rule.getRemoved() != null) {
+ throw new InvalidParameterValueException("Alert rule not found");
+ }
+ return ruleDao.remove(cmd.getId());
+ }
+
+ @Override
+ public ListResponse listResourceAlerts(ListResourceAlertsCmd cmd) {
+ Long alertRuleInternalId = null;
+ if (cmd.getAlertRuleId() != null) {
+ ResourceAlertRuleVO rule = ruleDao.findByUuid(cmd.getAlertRuleId());
+ if (rule == null) {
+ throw new InvalidParameterValueException("Alert rule not found: " + cmd.getAlertRuleId());
+ }
+ alertRuleInternalId = rule.getId();
+ }
+ List alerts = alertDao.listByFilters(
+ alertRuleInternalId, cmd.getResourceId(),
+ cmd.getSeverity(), cmd.getStartDate(), cmd.getEndDate());
+
+ List responses = alerts.stream()
+ .map(this::toAlertResponse)
+ .collect(Collectors.toList());
+
+ ListResponse response = new ListResponse<>();
+ response.setResponses(responses, responses.size());
+ return response;
+ }
+
+ @Override
+ public List> getCommands() {
+ List> cmds = new ArrayList<>();
+ cmds.add(CreateResourceAlertRuleCmd.class);
+ cmds.add(ListResourceAlertRulesCmd.class);
+ cmds.add(UpdateResourceAlertRuleCmd.class);
+ cmds.add(DeleteResourceAlertRuleCmd.class);
+ cmds.add(ListResourceAlertsCmd.class);
+ return cmds;
+ }
+
+ private ResourceAlertRuleResponse toRuleResponse(ResourceAlertRuleJoinVO vo) {
+ if (vo == null) return null;
+ ResourceAlertRuleResponse r = new ResourceAlertRuleResponse();
+ r.setObjectName("resourcealertrule");
+ r.setId(vo.getUuid());
+ r.setName(vo.getName());
+ r.setResourceType(vo.getResourceType() != null ? vo.getResourceType().name() : null);
+ r.setResourceId(vo.getResourceId() != null ? String.valueOf(vo.getResourceId()) : null);
+ r.setMetric(vo.getMetric());
+ r.setCondition(vo.getCondition() != null ? vo.getCondition().name() : null);
+ r.setThreshold(vo.getThreshold());
+ r.setSeverity(vo.getSeverity() != null ? vo.getSeverity().name() : null);
+ r.setMessage(vo.getMessage());
+ r.setEmail(vo.isEmail());
+ r.setResetInterval(vo.getResetInterval());
+ r.setAccountName(vo.getAccountName());
+ r.setDomainId(vo.getDomainUuid());
+ r.setDomainName(vo.getDomainName());
+ r.setCreated(vo.getCreated());
+ return r;
+ }
+
+ private ResourceAlertResponse toAlertResponse(ResourceAlertVO vo) {
+ ResourceAlertResponse r = new ResourceAlertResponse();
+ r.setObjectName("resourcealert");
+ r.setId(vo.getUuid());
+ ResourceAlertRuleVO rule = ruleDao.findById(vo.getAlertRuleId());
+ r.setAlertRuleId(rule != null ? rule.getUuid() : null);
+ r.setResourceId(vo.getResourceId() != null ? String.valueOf(vo.getResourceId()) : null);
+ r.setMetricType(vo.getMetricType());
+ r.setMetricValue(vo.getMetricValue());
+ r.setSeverity(vo.getSeverity() != null ? vo.getSeverity().name() : null);
+ r.setMessage(vo.getMessage());
+ r.setAlertTimestamp(vo.getAlertTimestamp());
+ return r;
+ }
+
+ private Account resolveOwner(String accountName, Long domainId) {
+ if (StringUtils.isNotBlank(accountName) && domainId != null) {
+ Domain domain = domainDao.findById(domainId);
+ if (domain == null) {
+ throw new InvalidParameterValueException("Domain not found");
+ }
+ Account account = accountManager.getActiveAccountByName(accountName, domainId);
+ if (account == null) {
+ throw new InvalidParameterValueException("Account not found in the specified domain");
+ }
+ return account;
+ }
+ return accountManager.getActiveAccountById(
+ CallContext.current().getCallingAccountId());
+ }
+
+ private ResourceAlertRule.ResourceType parseResourceType(String value) {
+ ResourceAlertRule.ResourceType type = EnumUtils.getEnum(ResourceAlertRule.ResourceType.class, value);
+ if (type == null) {
+ throw new InvalidParameterValueException("Invalid resourcetype: " + value);
+ }
+ return type;
+ }
+
+ private AlertCondition parseCondition(String value) {
+ AlertCondition cond = EnumUtils.getEnum(AlertCondition.class, value != null ? value.toUpperCase() : null);
+ if (cond == null) {
+ throw new InvalidParameterValueException("Invalid condition: " + value + ". Valid values: GT, GTE, LT, LTE, EQ");
+ }
+ return cond;
+ }
+
+ private AlertSeverity parseSeverity(String value) {
+ AlertSeverity sev = EnumUtils.getEnum(AlertSeverity.class, value != null ? value.toUpperCase() : null);
+ if (sev == null) {
+ throw new InvalidParameterValueException("Invalid severity: " + value + ". Valid values: CRITICAL, HIGH, MEDIUM, LOW");
+ }
+ return sev;
+ }
+
+ private ResourceAlertMetric parseMetric(String value, ResourceAlertRule.ResourceType resourceType) {
+ ResourceAlertMetric metric = EnumUtils.getEnum(ResourceAlertMetric.class, value != null ? value.toUpperCase() : null);
+ if (metric == null) {
+ throw new InvalidParameterValueException("Invalid metric: " + value);
+ }
+ if (!metric.appliesTo(resourceType)) {
+ throw new InvalidParameterValueException(
+ "Metric " + metric.name() + " does not apply to resource type " + resourceType.name());
+ }
+ return metric;
+ }
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/admin/CreateResourceAlertRuleCmd.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/admin/CreateResourceAlertRuleCmd.java
new file mode 100644
index 000000000000..ea383941c4c5
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/admin/CreateResourceAlertRuleCmd.java
@@ -0,0 +1,127 @@
+// 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.cloudstack.resourcealert.api.command.admin;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.resourcealert.ResourceAlertRule;
+import org.apache.cloudstack.resourcealert.ResourceAlertService;
+import org.apache.cloudstack.resourcealert.api.response.ResourceAlertRuleResponse;
+
+import com.cloud.utils.exception.CloudRuntimeException;
+
+@APICommand(name = "createResourceAlertRule",
+ description = "Creates a resource alert rule",
+ responseObject = ResourceAlertRuleResponse.class,
+ entityType = {ResourceAlertRule.class},
+ authorized = {RoleType.Admin},
+ since = "4.23.0")
+public class CreateResourceAlertRuleCmd extends BaseCmd {
+
+ @Inject
+ ResourceAlertService resourceAlertService;
+
+ @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true,
+ description = "name of the alert rule")
+ private String name;
+
+ @Parameter(name = "resourcetype", type = CommandType.STRING, required = true,
+ description = "type of resource to monitor: VirtualMachine, Volume, Host, StoragePool")
+ private String resourceType;
+
+ @Parameter(name = "resourceid", type = CommandType.LONG,
+ description = "ID of the specific resource to monitor; omit for a generic rule covering all resources of this type")
+ private Long resourceId;
+
+ @Parameter(name = "metric", type = CommandType.STRING, required = true,
+ description = "metric to monitor (e.g. CPU_UTILIZATION, MEMORY_UTILIZATION)")
+ private String metric;
+
+ @Parameter(name = "condition", type = CommandType.STRING, required = true,
+ description = "comparison operator: GT, GTE, LT, LTE, EQ")
+ private String condition;
+
+ @Parameter(name = "threshold", type = CommandType.DOUBLE, required = true,
+ description = "threshold value that triggers the alert")
+ private Double threshold;
+
+ @Parameter(name = "severity", type = CommandType.STRING, required = true,
+ description = "alert severity: CRITICAL, HIGH, MEDIUM, LOW")
+ private String severity;
+
+ @Parameter(name = ApiConstants.MESSAGE, type = CommandType.STRING,
+ description = "custom message to include in the alert")
+ private String message;
+
+ @Parameter(name = "email", type = CommandType.BOOLEAN,
+ description = "true to send email notification when this rule fires (admin SMTP must be configured)")
+ private Boolean email;
+
+ @Parameter(name = "resetinterval", type = CommandType.INTEGER,
+ description = "minimum seconds between repeat firings of this rule (default: 600)")
+ private Integer resetInterval;
+
+ @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING,
+ description = "account to associate this rule with (defaults to caller)")
+ private String accountName;
+
+ @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID,
+ entityType = org.apache.cloudstack.api.response.DomainResponse.class,
+ description = "domain to associate this rule with")
+ private Long domainId;
+
+ public String getName() { return name; }
+ public String getResourceType() { return resourceType; }
+ public Long getResourceId() { return resourceId; }
+ public String getMetric() { return metric; }
+ public String getCondition() { return condition; }
+ public Double getThreshold() { return threshold; }
+ public String getSeverity() { return severity; }
+ public String getMessage() { return message; }
+ public Boolean getEmail() { return email; }
+ public Integer getResetInterval() { return resetInterval; }
+ public String getAccountName() { return accountName; }
+ public Long getDomainId() { return domainId; }
+
+ @Override
+ public long getEntityOwnerId() {
+ return CallContext.current().getCallingAccountId();
+ }
+
+ @Override
+ public void execute() throws ServerApiException {
+ try {
+ ResourceAlertRuleResponse response = resourceAlertService.createResourceAlertRule(this);
+ if (response == null) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create resource alert rule");
+ }
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ } catch (CloudRuntimeException e) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
+ }
+ }
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/admin/DeleteResourceAlertRuleCmd.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/admin/DeleteResourceAlertRuleCmd.java
new file mode 100644
index 000000000000..4650f7374136
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/admin/DeleteResourceAlertRuleCmd.java
@@ -0,0 +1,74 @@
+// 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.cloudstack.resourcealert.api.command.admin;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.SuccessResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.resourcealert.ResourceAlertRule;
+import org.apache.cloudstack.resourcealert.ResourceAlertService;
+import org.apache.cloudstack.resourcealert.api.response.ResourceAlertRuleResponse;
+
+import com.cloud.utils.exception.CloudRuntimeException;
+
+@APICommand(name = "deleteResourceAlertRule",
+ description = "Deletes a resource alert rule",
+ responseObject = SuccessResponse.class,
+ entityType = {ResourceAlertRule.class},
+ authorized = {RoleType.Admin},
+ since = "4.23.0")
+public class DeleteResourceAlertRuleCmd extends BaseCmd {
+
+ @Inject
+ ResourceAlertService resourceAlertService;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID,
+ entityType = ResourceAlertRuleResponse.class,
+ required = true,
+ description = "the ID of the alert rule to delete")
+ private Long id;
+
+ public Long getId() { return id; }
+
+ @Override
+ public long getEntityOwnerId() {
+ return CallContext.current().getCallingAccountId();
+ }
+
+ @Override
+ public void execute() throws ServerApiException {
+ try {
+ boolean result = resourceAlertService.deleteResourceAlertRule(this);
+ if (!result) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete resource alert rule");
+ }
+ SuccessResponse response = new SuccessResponse(getCommandName());
+ setResponseObject(response);
+ } catch (CloudRuntimeException e) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
+ }
+ }
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/admin/ListResourceAlertRulesCmd.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/admin/ListResourceAlertRulesCmd.java
new file mode 100644
index 000000000000..aa1d405ab7ad
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/admin/ListResourceAlertRulesCmd.java
@@ -0,0 +1,83 @@
+// 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.cloudstack.resourcealert.api.command.admin;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseListCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.resourcealert.ResourceAlertRule;
+import org.apache.cloudstack.resourcealert.ResourceAlertService;
+import org.apache.cloudstack.resourcealert.api.response.ResourceAlertRuleResponse;
+
+@APICommand(name = "listResourceAlertRules",
+ description = "Lists resource alert rules",
+ responseObject = ResourceAlertRuleResponse.class,
+ entityType = {ResourceAlertRule.class},
+ authorized = {RoleType.Admin},
+ since = "4.23.0")
+public class ListResourceAlertRulesCmd extends BaseListCmd {
+
+ @Inject
+ ResourceAlertService resourceAlertService;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID,
+ entityType = ResourceAlertRuleResponse.class,
+ description = "the ID of the alert rule")
+ private Long id;
+
+ @Parameter(name = "resourcetype", type = CommandType.STRING,
+ description = "filter by resource type: VirtualMachine, Volume, Host, StoragePool")
+ private String resourceType;
+
+ @Parameter(name = "resourceid", type = CommandType.LONG,
+ description = "filter by specific resource ID")
+ private Long resourceId;
+
+ @Parameter(name = ApiConstants.NAME, type = CommandType.STRING,
+ description = "filter by rule name")
+ private String name;
+
+ @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING,
+ description = "filter by account name")
+ private String accountName;
+
+ @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID,
+ entityType = org.apache.cloudstack.api.response.DomainResponse.class,
+ description = "filter by domain")
+ private Long domainId;
+
+ public Long getId() { return id; }
+ public String getResourceType() { return resourceType; }
+ public Long getResourceId() { return resourceId; }
+ public String getRuleName() { return name; }
+ public String getAccountName() { return accountName; }
+ public Long getDomainId() { return domainId; }
+
+ @Override
+ public void execute() throws ServerApiException {
+ ListResponse response = resourceAlertService.listResourceAlertRules(this);
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/admin/ListResourceAlertsCmd.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/admin/ListResourceAlertsCmd.java
new file mode 100644
index 000000000000..3db754e025bb
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/admin/ListResourceAlertsCmd.java
@@ -0,0 +1,78 @@
+// 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.cloudstack.resourcealert.api.command.admin;
+
+import java.util.Date;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseListCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.resourcealert.ResourceAlert;
+import org.apache.cloudstack.resourcealert.ResourceAlertService;
+import org.apache.cloudstack.resourcealert.api.response.ResourceAlertResponse;
+
+@APICommand(name = "listResourceAlerts",
+ description = "Lists fired resource alerts",
+ responseObject = ResourceAlertResponse.class,
+ entityType = {ResourceAlert.class},
+ authorized = {RoleType.Admin},
+ since = "4.23.0")
+public class ListResourceAlertsCmd extends BaseListCmd {
+
+ @Inject
+ ResourceAlertService resourceAlertService;
+
+ @Parameter(name = "alertruleid", type = CommandType.STRING,
+ description = "UUID of the alert rule to filter by")
+ private String alertRuleId;
+
+ @Parameter(name = "resourceid", type = CommandType.LONG,
+ description = "filter by the resource that triggered the alert")
+ private Long resourceId;
+
+ @Parameter(name = "severity", type = CommandType.STRING,
+ description = "filter by severity: CRITICAL, HIGH, MEDIUM, LOW")
+ private String severity;
+
+ @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE,
+ description = "filter alerts fired on or after this date")
+ private Date startDate;
+
+ @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE,
+ description = "filter alerts fired on or before this date")
+ private Date endDate;
+
+ public String getAlertRuleId() { return alertRuleId; }
+ public Long getResourceId() { return resourceId; }
+ public String getSeverity() { return severity; }
+ public Date getStartDate() { return startDate; }
+ public Date getEndDate() { return endDate; }
+
+ @Override
+ public void execute() throws ServerApiException {
+ ListResponse response = resourceAlertService.listResourceAlerts(this);
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/admin/UpdateResourceAlertRuleCmd.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/admin/UpdateResourceAlertRuleCmd.java
new file mode 100644
index 000000000000..9c734e157c8a
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/admin/UpdateResourceAlertRuleCmd.java
@@ -0,0 +1,108 @@
+// 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.cloudstack.resourcealert.api.command.admin;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.resourcealert.ResourceAlertRule;
+import org.apache.cloudstack.resourcealert.ResourceAlertService;
+import org.apache.cloudstack.resourcealert.api.response.ResourceAlertRuleResponse;
+
+import com.cloud.utils.exception.CloudRuntimeException;
+
+@APICommand(name = "updateResourceAlertRule",
+ description = "Updates a resource alert rule",
+ responseObject = ResourceAlertRuleResponse.class,
+ entityType = {ResourceAlertRule.class},
+ authorized = {RoleType.Admin},
+ since = "4.23.0")
+public class UpdateResourceAlertRuleCmd extends BaseCmd {
+
+ @Inject
+ ResourceAlertService resourceAlertService;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID,
+ entityType = ResourceAlertRuleResponse.class,
+ required = true,
+ description = "the ID of the alert rule to update")
+ private Long id;
+
+ @Parameter(name = ApiConstants.NAME, type = CommandType.STRING,
+ description = "new name for the rule")
+ private String name;
+
+ @Parameter(name = "condition", type = CommandType.STRING,
+ description = "new comparison operator: GT, GTE, LT, LTE, EQ")
+ private String condition;
+
+ @Parameter(name = "threshold", type = CommandType.DOUBLE,
+ description = "new threshold value")
+ private Double threshold;
+
+ @Parameter(name = "severity", type = CommandType.STRING,
+ description = "new severity: CRITICAL, HIGH, MEDIUM, LOW")
+ private String severity;
+
+ @Parameter(name = ApiConstants.MESSAGE, type = CommandType.STRING,
+ description = "new alert message")
+ private String message;
+
+ @Parameter(name = "email", type = CommandType.BOOLEAN,
+ description = "enable or disable email notification")
+ private Boolean email;
+
+ @Parameter(name = "resetinterval", type = CommandType.INTEGER,
+ description = "new minimum seconds between repeat firings")
+ private Integer resetInterval;
+
+ public Long getId() { return id; }
+ public String getName() { return name; }
+ public String getCondition() { return condition; }
+ public Double getThreshold() { return threshold; }
+ public String getSeverity() { return severity; }
+ public String getMessage() { return message; }
+ public Boolean getEmail() { return email; }
+ public Integer getResetInterval() { return resetInterval; }
+
+ @Override
+ public long getEntityOwnerId() {
+ return CallContext.current().getCallingAccountId();
+ }
+
+ @Override
+ public void execute() throws ServerApiException {
+ try {
+ ResourceAlertRuleResponse response = resourceAlertService.updateResourceAlertRule(this);
+ if (response == null) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update resource alert rule");
+ }
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ } catch (CloudRuntimeException e) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
+ }
+ }
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/response/ResourceAlertResponse.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/response/ResourceAlertResponse.java
new file mode 100644
index 000000000000..62e45e53e210
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/response/ResourceAlertResponse.java
@@ -0,0 +1,73 @@
+// 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.cloudstack.resourcealert.api.response;
+
+import java.util.Date;
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+import org.apache.cloudstack.api.EntityReference;
+import org.apache.cloudstack.resourcealert.ResourceAlert;
+
+import com.cloud.serializer.Param;
+import com.google.gson.annotations.SerializedName;
+
+@EntityReference(value = {ResourceAlert.class})
+public class ResourceAlertResponse extends BaseResponse {
+
+ @SerializedName(ApiConstants.ID)
+ @Param(description = "the ID of the fired alert")
+ private String id;
+
+ @SerializedName("alertruleid")
+ @Param(description = "the ID of the rule that triggered this alert")
+ private String alertRuleId;
+
+ @SerializedName("resourceid")
+ @Param(description = "the ID of the resource that triggered this alert")
+ private String resourceId;
+
+ @SerializedName("metrictype")
+ @Param(description = "the metric that crossed the threshold")
+ private String metricType;
+
+ @SerializedName("metricvalue")
+ @Param(description = "the observed metric value at the time of firing")
+ private double metricValue;
+
+ @SerializedName("severity")
+ @Param(description = "the severity of the alert")
+ private String severity;
+
+ @SerializedName(ApiConstants.MESSAGE)
+ @Param(description = "the alert message")
+ private String message;
+
+ @SerializedName("alerttimestamp")
+ @Param(description = "the time the alert was fired")
+ private Date alertTimestamp;
+
+ public void setId(String id) { this.id = id; }
+ public void setAlertRuleId(String alertRuleId) { this.alertRuleId = alertRuleId; }
+ public void setResourceId(String resourceId) { this.resourceId = resourceId; }
+ public void setMetricType(String metricType) { this.metricType = metricType; }
+ public void setMetricValue(double metricValue) { this.metricValue = metricValue; }
+ public void setSeverity(String severity) { this.severity = severity; }
+ public void setMessage(String message) { this.message = message; }
+ public void setAlertTimestamp(Date alertTimestamp) { this.alertTimestamp = alertTimestamp; }
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/response/ResourceAlertRuleResponse.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/response/ResourceAlertRuleResponse.java
new file mode 100644
index 000000000000..2edeb2133635
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/response/ResourceAlertRuleResponse.java
@@ -0,0 +1,108 @@
+// 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.cloudstack.resourcealert.api.response;
+
+import java.util.Date;
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+import org.apache.cloudstack.api.EntityReference;
+import org.apache.cloudstack.resourcealert.ResourceAlertRule;
+
+import com.cloud.serializer.Param;
+import com.google.gson.annotations.SerializedName;
+
+@EntityReference(value = {ResourceAlertRule.class})
+public class ResourceAlertRuleResponse extends BaseResponse {
+
+ @SerializedName(ApiConstants.ID)
+ @Param(description = "the ID of the alert rule")
+ private String id;
+
+ @SerializedName(ApiConstants.NAME)
+ @Param(description = "the name of the alert rule")
+ private String name;
+
+ @SerializedName("resourcetype")
+ @Param(description = "the type of resource this rule monitors")
+ private String resourceType;
+
+ @SerializedName("resourceid")
+ @Param(description = "the specific resource ID; absent for generic rules")
+ private String resourceId;
+
+ @SerializedName("metric")
+ @Param(description = "the metric being monitored")
+ private String metric;
+
+ @SerializedName("condition")
+ @Param(description = "the comparison operator (GT, GTE, LT, LTE, EQ)")
+ private String condition;
+
+ @SerializedName("threshold")
+ @Param(description = "the threshold value that triggers this rule")
+ private double threshold;
+
+ @SerializedName("severity")
+ @Param(description = "the severity of the alert (CRITICAL, HIGH, MEDIUM, LOW)")
+ private String severity;
+
+ @SerializedName(ApiConstants.MESSAGE)
+ @Param(description = "the message sent with the alert")
+ private String message;
+
+ @SerializedName("email")
+ @Param(description = "whether email notification is enabled for this rule")
+ private boolean email;
+
+ @SerializedName("resetinterval")
+ @Param(description = "minimum seconds between repeat firings of this rule")
+ private int resetInterval;
+
+ @SerializedName(ApiConstants.ACCOUNT)
+ @Param(description = "the account that owns this rule")
+ private String accountName;
+
+ @SerializedName(ApiConstants.DOMAIN_ID)
+ @Param(description = "the ID of the domain this rule belongs to")
+ private String domainId;
+
+ @SerializedName(ApiConstants.DOMAIN)
+ @Param(description = "the name of the domain this rule belongs to")
+ private String domainName;
+
+ @SerializedName(ApiConstants.CREATED)
+ @Param(description = "the date this rule was created")
+ private Date created;
+
+ public void setId(String id) { this.id = id; }
+ public void setName(String name) { this.name = name; }
+ public void setResourceType(String resourceType) { this.resourceType = resourceType; }
+ public void setResourceId(String resourceId) { this.resourceId = resourceId; }
+ public void setMetric(String metric) { this.metric = metric; }
+ public void setCondition(String condition) { this.condition = condition; }
+ public void setThreshold(double threshold) { this.threshold = threshold; }
+ public void setSeverity(String severity) { this.severity = severity; }
+ public void setMessage(String message) { this.message = message; }
+ public void setEmail(boolean email) { this.email = email; }
+ public void setResetInterval(int resetInterval) { this.resetInterval = resetInterval; }
+ public void setAccountName(String accountName) { this.accountName = accountName; }
+ public void setDomainId(String domainId) { this.domainId = domainId; }
+ public void setDomainName(String domainName) { this.domainName = domainName; }
+ public void setCreated(Date created) { this.created = created; }
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertDao.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertDao.java
new file mode 100644
index 000000000000..afe7c77f7ebd
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertDao.java
@@ -0,0 +1,35 @@
+// 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.cloudstack.resourcealert.dao;
+
+import java.util.Date;
+import java.util.List;
+
+import org.apache.cloudstack.resourcealert.vo.ResourceAlertVO;
+
+import com.cloud.utils.db.GenericDao;
+
+public interface ResourceAlertDao extends GenericDao {
+
+ List listByAlertRuleId(long alertRuleId);
+
+ // Returns the most recent firing of a rule for a specific resource; used for reset-interval enforcement.
+ ResourceAlertVO findLastFiredForRule(long alertRuleId, Long resourceId);
+
+ List listByFilters(Long alertRuleId, Long resourceId, String severity, Date startDate, Date endDate);
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertDaoImpl.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertDaoImpl.java
new file mode 100644
index 000000000000..017c662779ce
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertDaoImpl.java
@@ -0,0 +1,91 @@
+// 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.cloudstack.resourcealert.dao;
+
+import java.util.Date;
+import java.util.List;
+
+import org.apache.cloudstack.resourcealert.vo.ResourceAlertVO;
+import org.apache.commons.lang3.StringUtils;
+
+import com.cloud.utils.db.Filter;
+import com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+
+public class ResourceAlertDaoImpl extends GenericDaoBase implements ResourceAlertDao {
+
+ private final SearchBuilder alertRuleIdSearch;
+
+ public ResourceAlertDaoImpl() {
+ alertRuleIdSearch = createSearchBuilder();
+ alertRuleIdSearch.and("alertRuleId", alertRuleIdSearch.entity().getAlertRuleId(), SearchCriteria.Op.EQ);
+ alertRuleIdSearch.done();
+ }
+
+ @Override
+ public List listByAlertRuleId(long alertRuleId) {
+ SearchCriteria sc = alertRuleIdSearch.create();
+ sc.setParameters("alertRuleId", alertRuleId);
+ return listBy(sc);
+ }
+
+ @Override
+ public ResourceAlertVO findLastFiredForRule(long alertRuleId, Long resourceId) {
+ SearchBuilder sb = createSearchBuilder();
+ sb.and("alertRuleId", sb.entity().getAlertRuleId(), SearchCriteria.Op.EQ);
+ if (resourceId != null) {
+ sb.and("resourceId", sb.entity().getResourceId(), SearchCriteria.Op.EQ);
+ }
+ Filter filter = new Filter(ResourceAlertVO.class, "alertTimestamp", false, 0L, 1L);
+ SearchCriteria sc = sb.create();
+ sc.setParameters("alertRuleId", alertRuleId);
+ if (resourceId != null) {
+ sc.setParameters("resourceId", resourceId);
+ }
+ List results = listBy(sc, filter);
+ return results.isEmpty() ? null : results.get(0);
+ }
+
+ @Override
+ public List listByFilters(Long alertRuleId, Long resourceId, String severity, Date startDate, Date endDate) {
+ SearchBuilder sb = createSearchBuilder();
+ if (alertRuleId != null) {
+ sb.and("alertRuleId", sb.entity().getAlertRuleId(), SearchCriteria.Op.EQ);
+ }
+ if (resourceId != null) {
+ sb.and("resourceId", sb.entity().getResourceId(), SearchCriteria.Op.EQ);
+ }
+ if (StringUtils.isNotBlank(severity)) {
+ sb.and("severity", sb.entity().getSeverity(), SearchCriteria.Op.EQ);
+ }
+ if (startDate != null) {
+ sb.and("startDate", sb.entity().getAlertTimestamp(), SearchCriteria.Op.GTEQ);
+ }
+ if (endDate != null) {
+ sb.and("endDate", sb.entity().getAlertTimestamp(), SearchCriteria.Op.LTEQ);
+ }
+ SearchCriteria sc = sb.create();
+ if (alertRuleId != null) sc.setParameters("alertRuleId", alertRuleId);
+ if (resourceId != null) sc.setParameters("resourceId", resourceId);
+ if (StringUtils.isNotBlank(severity)) sc.setParameters("severity", severity);
+ if (startDate != null) sc.setParameters("startDate", startDate);
+ if (endDate != null) sc.setParameters("endDate", endDate);
+ return listBy(sc);
+ }
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleDao.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleDao.java
new file mode 100644
index 000000000000..9a73628c732f
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleDao.java
@@ -0,0 +1,40 @@
+// 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.cloudstack.resourcealert.dao;
+
+import java.util.List;
+
+import org.apache.cloudstack.resourcealert.ResourceAlertRule;
+import org.apache.cloudstack.resourcealert.vo.ResourceAlertRuleVO;
+
+import com.cloud.utils.db.GenericDao;
+
+public interface ResourceAlertRuleDao extends GenericDao {
+
+ ResourceAlertRuleVO findByUuid(String uuid);
+
+ List listActive();
+
+ List listByAccountId(long accountId);
+
+ List listByResourceTypeAndId(ResourceAlertRule.ResourceType resourceType, Long resourceId);
+
+ int countActiveByAccountId(long accountId);
+
+ boolean existsSpecificRule(ResourceAlertRule.ResourceType resourceType, String metric, long resourceId);
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleDaoImpl.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleDaoImpl.java
new file mode 100644
index 000000000000..1515701086de
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleDaoImpl.java
@@ -0,0 +1,113 @@
+// 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.cloudstack.resourcealert.dao;
+
+import java.util.List;
+
+import org.apache.cloudstack.resourcealert.ResourceAlertRule;
+import org.apache.cloudstack.resourcealert.vo.ResourceAlertRuleVO;
+
+import com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+
+public class ResourceAlertRuleDaoImpl extends GenericDaoBase implements ResourceAlertRuleDao {
+
+ private final SearchBuilder activeSearch;
+ private final SearchBuilder accountIdSearch;
+ private final SearchBuilder resourceTypeAndIdSearch;
+ private final SearchBuilder activeByAccountSearch;
+ private final SearchBuilder specificRuleSearch;
+
+ public ResourceAlertRuleDaoImpl() {
+ activeSearch = createSearchBuilder();
+ activeSearch.and("removed", activeSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
+ activeSearch.done();
+
+ accountIdSearch = createSearchBuilder();
+ accountIdSearch.and("accountId", accountIdSearch.entity().getAccountId(), SearchCriteria.Op.EQ);
+ accountIdSearch.done();
+
+ resourceTypeAndIdSearch = createSearchBuilder();
+ resourceTypeAndIdSearch.and("resourceType", resourceTypeAndIdSearch.entity().getResourceType(), SearchCriteria.Op.EQ);
+ resourceTypeAndIdSearch.and("resourceId", resourceTypeAndIdSearch.entity().getResourceId(), SearchCriteria.Op.EQ);
+ resourceTypeAndIdSearch.done();
+
+ activeByAccountSearch = createSearchBuilder();
+ activeByAccountSearch.and("accountId", activeByAccountSearch.entity().getAccountId(), SearchCriteria.Op.EQ);
+ activeByAccountSearch.and("removed", activeByAccountSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
+ activeByAccountSearch.done();
+
+ specificRuleSearch = createSearchBuilder();
+ specificRuleSearch.and("resourceType", specificRuleSearch.entity().getResourceType(), SearchCriteria.Op.EQ);
+ specificRuleSearch.and("metric", specificRuleSearch.entity().getMetric(), SearchCriteria.Op.EQ);
+ specificRuleSearch.and("resourceId", specificRuleSearch.entity().getResourceId(), SearchCriteria.Op.EQ);
+ specificRuleSearch.and("removed", specificRuleSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
+ specificRuleSearch.done();
+ }
+
+ @Override
+ public List listActive() {
+ SearchCriteria sc = activeSearch.create();
+ return listBy(sc);
+ }
+
+ @Override
+ public ResourceAlertRuleVO findByUuid(String uuid) {
+ SearchBuilder sb = createSearchBuilder();
+ sb.and("uuid", sb.entity().getUuid(), SearchCriteria.Op.EQ);
+ SearchCriteria sc = sb.create();
+ sc.setParameters("uuid", uuid);
+ return findOneBy(sc);
+ }
+
+ @Override
+ public List listByAccountId(long accountId) {
+ SearchCriteria sc = accountIdSearch.create();
+ sc.setParameters("accountId", accountId);
+ return listBy(sc);
+ }
+
+ @Override
+ public List listByResourceTypeAndId(ResourceAlertRule.ResourceType resourceType, Long resourceId) {
+ SearchCriteria sc = resourceTypeAndIdSearch.create();
+ sc.setParameters("resourceType", resourceType);
+ if (resourceId != null) {
+ sc.setParameters("resourceId", resourceId);
+ } else {
+ sc.setParameters("resourceId", (Object) null);
+ }
+ return listBy(sc);
+ }
+
+ @Override
+ public int countActiveByAccountId(long accountId) {
+ SearchCriteria sc = activeByAccountSearch.create();
+ sc.setParameters("accountId", accountId);
+ return getCount(sc);
+ }
+
+ @Override
+ public boolean existsSpecificRule(ResourceAlertRule.ResourceType resourceType, String metric, long resourceId) {
+ SearchCriteria sc = specificRuleSearch.create();
+ sc.setParameters("resourceType", resourceType);
+ sc.setParameters("metric", metric);
+ sc.setParameters("resourceId", resourceId);
+ return getCount(sc) > 0;
+ }
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleJoinDao.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleJoinDao.java
new file mode 100644
index 000000000000..1f84da704d3f
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleJoinDao.java
@@ -0,0 +1,35 @@
+// 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.cloudstack.resourcealert.dao;
+
+import java.util.List;
+
+import org.apache.cloudstack.resourcealert.vo.ResourceAlertRuleJoinVO;
+
+import com.cloud.utils.db.GenericDao;
+
+public interface ResourceAlertRuleJoinDao extends GenericDao {
+
+ ResourceAlertRuleJoinVO findByUuid(String uuid);
+
+ List searchByFilters(Long id, String name, String resourceType, Long resourceId,
+ String accountName, Long domainId, Long offset, Long limit);
+
+ int countByFilters(Long id, String name, String resourceType, Long resourceId,
+ String accountName, Long domainId);
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleJoinDaoImpl.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleJoinDaoImpl.java
new file mode 100644
index 000000000000..b902a895a184
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleJoinDaoImpl.java
@@ -0,0 +1,77 @@
+// 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.cloudstack.resourcealert.dao;
+
+import java.util.List;
+
+import org.apache.cloudstack.resourcealert.vo.ResourceAlertRuleJoinVO;
+import org.apache.commons.lang3.StringUtils;
+
+import com.cloud.utils.db.Filter;
+import com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+
+public class ResourceAlertRuleJoinDaoImpl extends GenericDaoBase implements ResourceAlertRuleJoinDao {
+
+ @Override
+ public ResourceAlertRuleJoinVO findByUuid(String uuid) {
+ SearchBuilder sb = createSearchBuilder();
+ sb.and("uuid", sb.entity().getUuid(), SearchCriteria.Op.EQ);
+ SearchCriteria sc = sb.create();
+ sc.setParameters("uuid", uuid);
+ return findOneBy(sc);
+ }
+
+ @Override
+ public List searchByFilters(Long id, String name, String resourceType, Long resourceId,
+ String accountName, Long domainId, Long offset, Long limit) {
+ SearchCriteria sc = buildFilterCriteria(id, name, resourceType, resourceId, accountName, domainId);
+ Filter filter = new Filter(ResourceAlertRuleJoinVO.class, "id", true, offset, limit);
+ return listBy(sc, filter);
+ }
+
+ @Override
+ public int countByFilters(Long id, String name, String resourceType, Long resourceId,
+ String accountName, Long domainId) {
+ SearchCriteria sc = buildFilterCriteria(id, name, resourceType, resourceId, accountName, domainId);
+ return getCount(sc);
+ }
+
+ private SearchCriteria buildFilterCriteria(Long id, String name, String resourceType,
+ Long resourceId, String accountName, Long domainId) {
+ SearchBuilder sb = createSearchBuilder();
+ if (id != null) sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ);
+ if (StringUtils.isNotBlank(name)) sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ);
+ if (StringUtils.isNotBlank(resourceType)) sb.and("resourceType", sb.entity().getResourceType(), SearchCriteria.Op.EQ);
+ if (resourceId != null) sb.and("resourceId", sb.entity().getResourceId(), SearchCriteria.Op.EQ);
+ if (StringUtils.isNotBlank(accountName)) sb.and("accountName", sb.entity().getAccountName(), SearchCriteria.Op.EQ);
+ if (domainId != null) sb.and("domainId", sb.entity().getDomainId(), SearchCriteria.Op.EQ);
+ // exclude soft-deleted rules
+ sb.and("removed", sb.entity().getRemoved(), SearchCriteria.Op.NULL);
+
+ SearchCriteria sc = sb.create();
+ if (id != null) sc.setParameters("id", id);
+ if (StringUtils.isNotBlank(name)) sc.setParameters("name", name);
+ if (StringUtils.isNotBlank(resourceType)) sc.setParameters("resourceType", resourceType);
+ if (resourceId != null) sc.setParameters("resourceId", resourceId);
+ if (StringUtils.isNotBlank(accountName)) sc.setParameters("accountName", accountName);
+ if (domainId != null) sc.setParameters("domainId", domainId);
+ return sc;
+ }
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/vo/ResourceAlertRuleJoinVO.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/vo/ResourceAlertRuleJoinVO.java
new file mode 100644
index 000000000000..b2a3e64eae9b
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/vo/ResourceAlertRuleJoinVO.java
@@ -0,0 +1,141 @@
+// 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.cloudstack.resourcealert.vo;
+
+import java.util.Date;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+
+import org.apache.cloudstack.resourcealert.AlertCondition;
+import org.apache.cloudstack.resourcealert.AlertSeverity;
+import org.apache.cloudstack.resourcealert.ResourceAlertRule;
+
+import com.cloud.user.Account;
+
+@Entity
+@Table(name = "resource_alert_rule_view")
+public class ResourceAlertRuleJoinVO {
+
+ @Id
+ @Column(name = "id", updatable = false, nullable = false)
+ private long id;
+
+ @Column(name = "uuid")
+ private String uuid;
+
+ @Column(name = "name")
+ private String name;
+
+ @Column(name = "resource_type")
+ @Enumerated(value = EnumType.STRING)
+ private ResourceAlertRule.ResourceType resourceType;
+
+ @Column(name = "resource_id")
+ private Long resourceId;
+
+ @Column(name = "metric")
+ private String metric;
+
+ @Column(name = "condition_operator")
+ @Enumerated(value = EnumType.STRING)
+ private AlertCondition condition;
+
+ @Column(name = "threshold")
+ private double threshold;
+
+ @Column(name = "severity")
+ @Enumerated(value = EnumType.STRING)
+ private AlertSeverity severity;
+
+ @Column(name = "message", length = 4096)
+ private String message;
+
+ @Column(name = "email")
+ private boolean email;
+
+ @Column(name = "reset_interval")
+ private int resetInterval;
+
+ @Column(name = "created")
+ private Date created;
+
+ @Column(name = "updated")
+ @Temporal(value = TemporalType.TIMESTAMP)
+ private Date updated;
+
+ @Column(name = "removed")
+ private Date removed;
+
+ @Column(name = "account_id")
+ private long accountId;
+
+ @Column(name = "account_uuid")
+ private String accountUuid;
+
+ @Column(name = "account_name")
+ private String accountName;
+
+ @Column(name = "account_type")
+ @Enumerated(value = EnumType.STRING)
+ private Account.Type accountType;
+
+ @Column(name = "domain_id")
+ private long domainId;
+
+ @Column(name = "domain_uuid")
+ private String domainUuid;
+
+ @Column(name = "domain_name")
+ private String domainName;
+
+ @Column(name = "domain_path")
+ private String domainPath;
+
+ public ResourceAlertRuleJoinVO() {}
+
+ public long getId() { return id; }
+ public String getUuid() { return uuid; }
+ public String getName() { return name; }
+ public ResourceAlertRule.ResourceType getResourceType() { return resourceType; }
+ public Long getResourceId() { return resourceId; }
+ public String getMetric() { return metric; }
+ public AlertCondition getCondition() { return condition; }
+ public double getThreshold() { return threshold; }
+ public AlertSeverity getSeverity() { return severity; }
+ public String getMessage() { return message; }
+ public boolean isEmail() { return email; }
+ public int getResetInterval() { return resetInterval; }
+ public Date getCreated() { return created; }
+ public Date getUpdated() { return updated; }
+ public Date getRemoved() { return removed; }
+ public long getAccountId() { return accountId; }
+ public String getAccountUuid() { return accountUuid; }
+ public String getAccountName() { return accountName; }
+ public Account.Type getAccountType() { return accountType; }
+ public long getDomainId() { return domainId; }
+ public String getDomainUuid() { return domainUuid; }
+ public String getDomainName() { return domainName; }
+ public String getDomainPath() { return domainPath; }
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/vo/ResourceAlertRuleVO.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/vo/ResourceAlertRuleVO.java
new file mode 100644
index 000000000000..bafe1ba40bd8
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/vo/ResourceAlertRuleVO.java
@@ -0,0 +1,156 @@
+// 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.cloudstack.resourcealert.vo;
+
+import java.util.Date;
+import java.util.UUID;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+
+import org.apache.cloudstack.resourcealert.AlertCondition;
+import org.apache.cloudstack.resourcealert.AlertSeverity;
+import org.apache.cloudstack.resourcealert.ResourceAlertRule;
+
+import com.cloud.utils.db.GenericDao;
+
+@Entity
+@Table(name = "resource_alert_rules")
+public class ResourceAlertRuleVO implements ResourceAlertRule {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private long id;
+
+ @Column(name = "uuid")
+ private String uuid;
+
+ @Column(name = "name")
+ private String name;
+
+ @Column(name = "resource_type")
+ @Enumerated(value = EnumType.STRING)
+ private ResourceType resourceType;
+
+ @Column(name = "resource_id")
+ private Long resourceId;
+
+ @Column(name = "account_id")
+ private long accountId;
+
+ @Column(name = "domain_id")
+ private long domainId;
+
+ @Column(name = "metric")
+ private String metric;
+
+ @Column(name = "condition_operator")
+ @Enumerated(value = EnumType.STRING)
+ private AlertCondition condition;
+
+ @Column(name = "threshold")
+ private double threshold;
+
+ @Column(name = "severity")
+ @Enumerated(value = EnumType.STRING)
+ private AlertSeverity severity;
+
+ @Column(name = "message", length = 4096)
+ private String message;
+
+ @Column(name = "email")
+ private boolean email;
+
+ @Column(name = "reset_interval")
+ private int resetInterval;
+
+ @Column(name = GenericDao.CREATED_COLUMN)
+ private Date created;
+
+ @Column(name = "updated")
+ @Temporal(value = TemporalType.TIMESTAMP)
+ private Date updated;
+
+ @Column(name = GenericDao.REMOVED_COLUMN)
+ private Date removed;
+
+ public ResourceAlertRuleVO() {
+ this.uuid = UUID.randomUUID().toString();
+ }
+
+ public ResourceAlertRuleVO(String name, ResourceType resourceType, Long resourceId,
+ long accountId, long domainId, String metric, AlertCondition condition,
+ double threshold, AlertSeverity severity, String message, boolean email, int resetInterval) {
+ this.uuid = UUID.randomUUID().toString();
+ this.name = name;
+ this.resourceType = resourceType;
+ this.resourceId = resourceId;
+ this.accountId = accountId;
+ this.domainId = domainId;
+ this.metric = metric;
+ this.condition = condition;
+ this.threshold = threshold;
+ this.severity = severity;
+ this.message = message;
+ this.email = email;
+ this.resetInterval = resetInterval;
+ }
+
+ @Override public long getId() { return id; }
+ @Override public String getUuid() { return uuid; }
+ @Override public String getName() { return name; }
+ @Override public ResourceType getResourceType() { return resourceType; }
+ @Override public Long getResourceId() { return resourceId; }
+ @Override public long getAccountId() { return accountId; }
+ @Override public long getDomainId() { return domainId; }
+ @Override public String getMetric() { return metric; }
+ @Override public AlertCondition getCondition() { return condition; }
+ @Override public double getThreshold() { return threshold; }
+ @Override public AlertSeverity getSeverity() { return severity; }
+ @Override public String getMessage() { return message; }
+ @Override public boolean isEmail() { return email; }
+ @Override public int getResetInterval() { return resetInterval; }
+ @Override public Date getCreated() { return created; }
+
+ @Override
+ public Class> getEntityType() {
+ return ResourceAlertRule.class;
+ }
+
+ public Date getRemoved() { return removed; }
+ public Date getUpdated() { return updated; }
+
+ public void setName(String name) { this.name = name; }
+ public void setCondition(AlertCondition condition) { this.condition = condition; }
+ public void setThreshold(double threshold) { this.threshold = threshold; }
+ public void setSeverity(AlertSeverity severity) { this.severity = severity; }
+ public void setMessage(String message) { this.message = message; }
+ public void setEmail(boolean email) { this.email = email; }
+ public void setResetInterval(int resetInterval) { this.resetInterval = resetInterval; }
+ public void setUpdated(Date updated) { this.updated = updated; }
+ public void setRemoved(Date removed) { this.removed = removed; }
+}
diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/vo/ResourceAlertVO.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/vo/ResourceAlertVO.java
new file mode 100644
index 000000000000..3a2da89bc756
--- /dev/null
+++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/vo/ResourceAlertVO.java
@@ -0,0 +1,97 @@
+// 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.cloudstack.resourcealert.vo;
+
+import java.util.Date;
+import java.util.UUID;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+
+import org.apache.cloudstack.resourcealert.AlertSeverity;
+import org.apache.cloudstack.resourcealert.ResourceAlert;
+
+@Entity
+@Table(name = "resource_alerts")
+public class ResourceAlertVO implements ResourceAlert {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private long id;
+
+ @Column(name = "uuid")
+ private String uuid;
+
+ @Column(name = "alert_rule_id")
+ private long alertRuleId;
+
+ @Column(name = "resource_id")
+ private Long resourceId;
+
+ @Column(name = "metric_type")
+ private String metricType;
+
+ @Column(name = "metric_value")
+ private double metricValue;
+
+ @Column(name = "severity")
+ @Enumerated(value = EnumType.STRING)
+ private AlertSeverity severity;
+
+ @Column(name = "message", length = 4096)
+ private String message;
+
+ @Column(name = "alert_timestamp")
+ @Temporal(value = TemporalType.TIMESTAMP)
+ private Date alertTimestamp;
+
+ public ResourceAlertVO() {
+ this.uuid = UUID.randomUUID().toString();
+ }
+
+ public ResourceAlertVO(long alertRuleId, Long resourceId, String metricType,
+ double metricValue, AlertSeverity severity, String message, Date alertTimestamp) {
+ this.uuid = UUID.randomUUID().toString();
+ this.alertRuleId = alertRuleId;
+ this.resourceId = resourceId;
+ this.metricType = metricType;
+ this.metricValue = metricValue;
+ this.severity = severity;
+ this.message = message;
+ this.alertTimestamp = alertTimestamp;
+ }
+
+ @Override public long getId() { return id; }
+ @Override public String getUuid() { return uuid; }
+ @Override public long getAlertRuleId() { return alertRuleId; }
+ @Override public Long getResourceId() { return resourceId; }
+ @Override public String getMetricType() { return metricType; }
+ @Override public double getMetricValue() { return metricValue; }
+ @Override public AlertSeverity getSeverity() { return severity; }
+ @Override public String getMessage() { return message; }
+ @Override public Date getAlertTimestamp() { return alertTimestamp; }
+}
diff --git a/plugins/resource-alerts/src/main/resources/META-INF/cloudstack/resource-alerts/module.properties b/plugins/resource-alerts/src/main/resources/META-INF/cloudstack/resource-alerts/module.properties
new file mode 100644
index 000000000000..28f110acb319
--- /dev/null
+++ b/plugins/resource-alerts/src/main/resources/META-INF/cloudstack/resource-alerts/module.properties
@@ -0,0 +1,18 @@
+# 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.
+name=resource-alerts
+parent=api
diff --git a/plugins/resource-alerts/src/main/resources/META-INF/cloudstack/resource-alerts/spring-resource-alerts-context.xml b/plugins/resource-alerts/src/main/resources/META-INF/cloudstack/resource-alerts/spring-resource-alerts-context.xml
new file mode 100644
index 000000000000..9d6adb3333f5
--- /dev/null
+++ b/plugins/resource-alerts/src/main/resources/META-INF/cloudstack/resource-alerts/spring-resource-alerts-context.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/resource-alerts/src/test/java/org/apache/cloudstack/resourcealert/AlertConditionTest.java b/plugins/resource-alerts/src/test/java/org/apache/cloudstack/resourcealert/AlertConditionTest.java
new file mode 100644
index 000000000000..2a34ba71ea3b
--- /dev/null
+++ b/plugins/resource-alerts/src/test/java/org/apache/cloudstack/resourcealert/AlertConditionTest.java
@@ -0,0 +1,96 @@
+// 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.cloudstack.resourcealert;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class AlertConditionTest {
+
+ @Test
+ public void testGtFiresAbove() {
+ assertTrue(AlertCondition.GT.evaluate(81.0, 80.0));
+ }
+
+ @Test
+ public void testGtSilentAtBoundary() {
+ assertFalse(AlertCondition.GT.evaluate(80.0, 80.0));
+ }
+
+ @Test
+ public void testGtSilentBelow() {
+ assertFalse(AlertCondition.GT.evaluate(79.0, 80.0));
+ }
+
+ @Test
+ public void testGteFiresAbove() {
+ assertTrue(AlertCondition.GTE.evaluate(81.0, 80.0));
+ }
+
+ @Test
+ public void testGteFiresAtBoundary() {
+ assertTrue(AlertCondition.GTE.evaluate(80.0, 80.0));
+ }
+
+ @Test
+ public void testGteSilentBelow() {
+ assertFalse(AlertCondition.GTE.evaluate(79.0, 80.0));
+ }
+
+ @Test
+ public void testLtFiresBelow() {
+ assertTrue(AlertCondition.LT.evaluate(10.0, 20.0));
+ }
+
+ @Test
+ public void testLtSilentAtBoundary() {
+ assertFalse(AlertCondition.LT.evaluate(20.0, 20.0));
+ }
+
+ @Test
+ public void testLtSilentAbove() {
+ assertFalse(AlertCondition.LT.evaluate(21.0, 20.0));
+ }
+
+ @Test
+ public void testLteFiresAtBoundary() {
+ assertTrue(AlertCondition.LTE.evaluate(20.0, 20.0));
+ }
+
+ @Test
+ public void testLteFiresBelow() {
+ assertTrue(AlertCondition.LTE.evaluate(19.0, 20.0));
+ }
+
+ @Test
+ public void testLteSilentAbove() {
+ assertFalse(AlertCondition.LTE.evaluate(21.0, 20.0));
+ }
+
+ @Test
+ public void testEqFiresOnExactMatch() {
+ assertTrue(AlertCondition.EQ.evaluate(75.0, 75.0));
+ }
+
+ @Test
+ public void testEqSilentOnMismatch() {
+ assertFalse(AlertCondition.EQ.evaluate(75.001, 75.0));
+ }
+}
diff --git a/plugins/resource-alerts/src/test/java/org/apache/cloudstack/resourcealert/ResourceAlertManagerImplTest.java b/plugins/resource-alerts/src/test/java/org/apache/cloudstack/resourcealert/ResourceAlertManagerImplTest.java
new file mode 100644
index 000000000000..f5dec1a98cc2
--- /dev/null
+++ b/plugins/resource-alerts/src/test/java/org/apache/cloudstack/resourcealert/ResourceAlertManagerImplTest.java
@@ -0,0 +1,579 @@
+// 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.cloudstack.resourcealert;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.AbstractExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
+import org.apache.cloudstack.resourcealert.dao.ResourceAlertDao;
+import org.apache.cloudstack.resourcealert.dao.ResourceAlertRuleDao;
+import org.apache.cloudstack.resourcealert.vo.ResourceAlertRuleVO;
+import org.apache.cloudstack.resourcealert.vo.ResourceAlertVO;
+import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
+import org.apache.cloudstack.utils.mailing.SMTPMailProperties;
+import org.apache.cloudstack.utils.mailing.SMTPMailSender;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import com.cloud.host.HostStats;
+import com.cloud.host.dao.HostDao;
+import com.cloud.server.ResourceTag;
+import com.cloud.server.StatsCollector;
+import com.cloud.storage.StorageStats;
+import com.cloud.storage.dao.VolumeDao;
+import com.cloud.tags.dao.ResourceTagDao;
+import com.cloud.vm.UserVmVO;
+import com.cloud.vm.VirtualMachine;
+import com.cloud.vm.VmStats;
+import com.cloud.vm.dao.UserVmDao;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ResourceAlertManagerImplTest {
+
+ @Spy @InjectMocks
+ ResourceAlertManagerImpl manager;
+
+ @Mock ResourceAlertRuleDao ruleDao;
+ @Mock ResourceAlertDao alertDao;
+ @Mock UserVmDao userVmDao;
+ @Mock HostDao hostDao;
+ @Mock PrimaryDataStoreDao storagePoolDao;
+ @Mock VolumeDao volumeDao;
+ @Mock StatsCollector statsCollector;
+ @Mock ConfigurationDao configDao;
+ @Mock ResourceTagDao resourceTagDao;
+ @Mock SMTPMailSender mailSender;
+
+ @Captor ArgumentCaptor alertCaptor;
+ @Captor ArgumentCaptor mailCaptor;
+
+ private static final long VM_ID = 101L;
+ private static final long HOST_ID = 201L;
+ private static final long POOL_ID = 301L;
+
+ @Before
+ public void setUp() throws Exception {
+ // stub out the AlertGenerator static call (needs Spring context in real env)
+ doNothing().when(manager).publishAlertEvent(anyLong(), anyString(), anyString());
+ }
+
+ private ResourceAlertRuleVO vmCpuRule(Long resourceId) {
+ return vmCpuRuleWithEmail(resourceId, false);
+ }
+
+ private ResourceAlertRuleVO vmCpuRuleWithEmail(Long resourceId, boolean email) {
+ return new ResourceAlertRuleVO("test", ResourceAlertRule.ResourceType.VirtualMachine,
+ resourceId, 1L, 1L, "CPU_UTILIZATION", AlertCondition.GT, 80.0,
+ AlertSeverity.HIGH, "CPU high", email, 600);
+ }
+
+ private void injectMailSender(String... recipients) throws Exception {
+ Field f = ResourceAlertManagerImpl.class.getDeclaredField("mailSender");
+ f.setAccessible(true);
+ f.set(manager, mailSender);
+
+ Field r = ResourceAlertManagerImpl.class.getDeclaredField("emailRecipients");
+ r.setAccessible(true);
+ r.set(manager, recipients);
+
+ Field s = ResourceAlertManagerImpl.class.getDeclaredField("senderAddress");
+ s.setAccessible(true);
+ s.set(manager, "alerts@example.com");
+
+ // replace async executor with a synchronous one so verify() works immediately
+ manager.emailExecutor = new AbstractExecutorService() {
+ @Override public void execute(Runnable command) { command.run(); }
+ @Override public void shutdown() {}
+ @Override public List shutdownNow() { return Collections.emptyList(); }
+ @Override public boolean isShutdown() { return false; }
+ @Override public boolean isTerminated() { return false; }
+ @Override public boolean awaitTermination(long t, TimeUnit u) { return true; }
+ };
+ }
+
+ @Test
+ public void testVmCpuRuleFiresWhenThresholdBreached() {
+ ResourceAlertRuleVO rule = vmCpuRule(VM_ID);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ VmStats stats = mock(VmStats.class);
+ when(stats.getCPUUtilization()).thenReturn(85.0);
+ when(statsCollector.getVmStats(VM_ID, false)).thenReturn(stats);
+ when(alertDao.findLastFiredForRule(anyLong(), eq(VM_ID))).thenReturn(null);
+
+ manager.evaluateRules();
+
+ verify(alertDao).persist(alertCaptor.capture());
+ ResourceAlertVO fired = alertCaptor.getValue();
+ assertEquals(VM_ID, (long) fired.getResourceId());
+ assertEquals("CPU_UTILIZATION", fired.getMetricType());
+ assertEquals(85.0, fired.getMetricValue(), 0.001);
+ assertEquals(AlertSeverity.HIGH, fired.getSeverity());
+ }
+
+ @Test
+ public void testVmCpuRuleDoesNotFireWhenBelowThreshold() {
+ ResourceAlertRuleVO rule = vmCpuRule(VM_ID);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ VmStats stats = mock(VmStats.class);
+ when(stats.getCPUUtilization()).thenReturn(75.0);
+ when(statsCollector.getVmStats(VM_ID, false)).thenReturn(stats);
+
+ manager.evaluateRules();
+
+ verify(alertDao, never()).persist(any());
+ }
+
+ @Test
+ public void testRuleDoesNotFireWithinResetInterval() {
+ ResourceAlertRuleVO rule = vmCpuRule(VM_ID);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ VmStats stats = mock(VmStats.class);
+ when(stats.getCPUUtilization()).thenReturn(85.0);
+ when(statsCollector.getVmStats(VM_ID, false)).thenReturn(stats);
+
+ ResourceAlertVO recentAlert = mock(ResourceAlertVO.class);
+ when(recentAlert.getAlertTimestamp()).thenReturn(new Date(System.currentTimeMillis() - 10_000L));
+ when(alertDao.findLastFiredForRule(anyLong(), eq(VM_ID))).thenReturn(recentAlert);
+
+ manager.evaluateRules();
+
+ verify(alertDao, never()).persist(any());
+ }
+
+ @Test
+ public void testRuleFiresAfterResetIntervalExpires() {
+ ResourceAlertRuleVO rule = vmCpuRule(VM_ID);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ VmStats stats = mock(VmStats.class);
+ when(stats.getCPUUtilization()).thenReturn(85.0);
+ when(statsCollector.getVmStats(VM_ID, false)).thenReturn(stats);
+
+ ResourceAlertVO oldAlert = mock(ResourceAlertVO.class);
+ when(oldAlert.getAlertTimestamp()).thenReturn(new Date(System.currentTimeMillis() - 700_000L));
+ when(alertDao.findLastFiredForRule(anyLong(), eq(VM_ID))).thenReturn(oldAlert);
+
+ manager.evaluateRules();
+
+ verify(alertDao).persist(any());
+ }
+
+ @Test
+ public void testNullStatsSkipped() {
+ ResourceAlertRuleVO rule = vmCpuRule(VM_ID);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+ when(statsCollector.getVmStats(VM_ID, false)).thenReturn(null);
+
+ manager.evaluateRules();
+
+ verify(alertDao, never()).persist(any());
+ }
+
+ @Test
+ public void testVmMemorySkippedWhenNoBalloonDriver() {
+ ResourceAlertRuleVO rule = new ResourceAlertRuleVO("test",
+ ResourceAlertRule.ResourceType.VirtualMachine, VM_ID, 1L, 1L,
+ "MEMORY_UTILIZATION", AlertCondition.GT, 50.0, AlertSeverity.MEDIUM, null, false, 600);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ VmStats stats = mock(VmStats.class);
+ when(stats.getMemoryKBs()).thenReturn(8192.0);
+ when(stats.getIntFreeMemoryKBs()).thenReturn(-1.0);
+ when(statsCollector.getVmStats(VM_ID, false)).thenReturn(stats);
+
+ manager.evaluateRules();
+
+ verify(alertDao, never()).persist(any());
+ }
+
+ @Test
+ public void testVmMemoryUtilizationCalculation() {
+ ResourceAlertRuleVO rule = new ResourceAlertRuleVO("test",
+ ResourceAlertRule.ResourceType.VirtualMachine, VM_ID, 1L, 1L,
+ "MEMORY_UTILIZATION", AlertCondition.GT, 70.0, AlertSeverity.HIGH, null, false, 600);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ VmStats stats = mock(VmStats.class);
+ when(stats.getMemoryKBs()).thenReturn(8192.0);
+ when(stats.getIntFreeMemoryKBs()).thenReturn(2048.0); // 75% used
+ when(statsCollector.getVmStats(VM_ID, false)).thenReturn(stats);
+ when(alertDao.findLastFiredForRule(anyLong(), eq(VM_ID))).thenReturn(null);
+
+ manager.evaluateRules();
+
+ verify(alertDao).persist(alertCaptor.capture());
+ assertEquals(75.0, alertCaptor.getValue().getMetricValue(), 0.001);
+ }
+
+ @Test
+ public void testStorageUtilizationCalculation() {
+ ResourceAlertRuleVO rule = new ResourceAlertRuleVO("test",
+ ResourceAlertRule.ResourceType.StoragePool, POOL_ID, 1L, 1L,
+ "STORAGE_UTILIZATION", AlertCondition.GT, 65.0, AlertSeverity.HIGH, null, false, 600);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ StorageStats poolStats = mock(StorageStats.class);
+ when(poolStats.getCapacityBytes()).thenReturn(10000L);
+ when(poolStats.getByteUsed()).thenReturn(7000L); // 70%
+ when(statsCollector.getStoragePoolStats(POOL_ID)).thenReturn(poolStats);
+ when(alertDao.findLastFiredForRule(anyLong(), eq(POOL_ID))).thenReturn(null);
+
+ manager.evaluateRules();
+
+ verify(alertDao).persist(alertCaptor.capture());
+ assertEquals(70.0, alertCaptor.getValue().getMetricValue(), 0.001);
+ }
+
+ @Test
+ public void testGenericVmRuleFansOutToAllRunningVms() {
+ ResourceAlertRuleVO rule = vmCpuRule(null);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ UserVmVO vm1 = mock(UserVmVO.class);
+ when(vm1.getId()).thenReturn(101L);
+ when(vm1.getState()).thenReturn(VirtualMachine.State.Running);
+
+ UserVmVO vm2 = mock(UserVmVO.class);
+ when(vm2.getId()).thenReturn(102L);
+ when(vm2.getState()).thenReturn(VirtualMachine.State.Running);
+
+ when(userVmDao.listByAccountId(1L)).thenReturn(Arrays.asList(vm1, vm2));
+
+ VmStats stats = mock(VmStats.class);
+ when(stats.getCPUUtilization()).thenReturn(85.0);
+ when(statsCollector.getVmStats(101L, false)).thenReturn(stats);
+ when(statsCollector.getVmStats(102L, false)).thenReturn(stats);
+ when(alertDao.findLastFiredForRule(anyLong(), anyLong())).thenReturn(null);
+
+ manager.evaluateRules();
+
+ verify(alertDao, times(2)).persist(any());
+ }
+
+ @Test
+ public void testGenericVmRuleSkipsStoppedVms() {
+ ResourceAlertRuleVO rule = vmCpuRule(null);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ UserVmVO stopped = mock(UserVmVO.class);
+ when(stopped.getState()).thenReturn(VirtualMachine.State.Stopped);
+ when(userVmDao.listByAccountId(1L)).thenReturn(Collections.singletonList(stopped));
+
+ manager.evaluateRules();
+
+ verify(alertDao, never()).persist(any());
+ }
+
+ @Test
+ public void testHostCpuRuleUsesHostStats() {
+ ResourceAlertRuleVO rule = new ResourceAlertRuleVO("test",
+ ResourceAlertRule.ResourceType.Host, HOST_ID, 1L, 1L,
+ "CPU_UTILIZATION", AlertCondition.GT, 85.0, AlertSeverity.CRITICAL, null, false, 600);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ HostStats hostStats = mock(HostStats.class);
+ when(hostStats.getCpuUtilization()).thenReturn(90.0);
+ when(statsCollector.getHostStats(HOST_ID)).thenReturn(hostStats);
+ when(alertDao.findLastFiredForRule(anyLong(), eq(HOST_ID))).thenReturn(null);
+
+ manager.evaluateRules();
+
+ verify(alertDao).persist(alertCaptor.capture());
+ assertEquals(HOST_ID, (long) alertCaptor.getValue().getResourceId());
+ assertEquals(90.0, alertCaptor.getValue().getMetricValue(), 0.001);
+ }
+
+ @Test
+ public void testHostMemoryUtilizationCalculation() {
+ ResourceAlertRuleVO rule = new ResourceAlertRuleVO("test",
+ ResourceAlertRule.ResourceType.Host, HOST_ID, 1L, 1L,
+ "MEMORY_UTILIZATION", AlertCondition.GT, 80.0, AlertSeverity.HIGH, null, false, 600);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ HostStats hostStats = mock(HostStats.class);
+ when(hostStats.getTotalMemoryKBs()).thenReturn(16384.0);
+ when(hostStats.getFreeMemoryKBs()).thenReturn(1638.4); // ~90% used
+ when(statsCollector.getHostStats(HOST_ID)).thenReturn(hostStats);
+ when(alertDao.findLastFiredForRule(anyLong(), eq(HOST_ID))).thenReturn(null);
+
+ manager.evaluateRules();
+
+ verify(alertDao).persist(alertCaptor.capture());
+ assertEquals(90.0, alertCaptor.getValue().getMetricValue(), 0.01);
+ }
+
+ @Test
+ public void testEventBusPublishedOnFiring() {
+ ResourceAlertRuleVO rule = vmCpuRule(VM_ID);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ VmStats stats = mock(VmStats.class);
+ when(stats.getCPUUtilization()).thenReturn(85.0);
+ when(statsCollector.getVmStats(VM_ID, false)).thenReturn(stats);
+ when(alertDao.findLastFiredForRule(anyLong(), eq(VM_ID))).thenReturn(null);
+
+ UserVmVO vm = mock(UserVmVO.class);
+ when(vm.getDataCenterId()).thenReturn(1L);
+ when(userVmDao.findById(VM_ID)).thenReturn(vm);
+
+ manager.evaluateRules();
+
+ verify(manager).publishAlertEvent(eq(1L), anyString(), anyString());
+ }
+
+ @Test
+ public void testEventBusNotPublishedWhenNoFiring() {
+ ResourceAlertRuleVO rule = vmCpuRule(VM_ID);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ VmStats stats = mock(VmStats.class);
+ when(stats.getCPUUtilization()).thenReturn(75.0); // below threshold
+ when(statsCollector.getVmStats(VM_ID, false)).thenReturn(stats);
+
+ manager.evaluateRules();
+
+ verify(manager, never()).publishAlertEvent(anyLong(), anyString(), anyString());
+ }
+
+ @Test
+ public void testEmailSentWhenRuleHasEmailEnabled() throws Exception {
+ injectMailSender("admin@example.com");
+
+ ResourceAlertRuleVO rule = vmCpuRuleWithEmail(VM_ID, true);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ VmStats stats = mock(VmStats.class);
+ when(stats.getCPUUtilization()).thenReturn(85.0);
+ when(statsCollector.getVmStats(VM_ID, false)).thenReturn(stats);
+ when(alertDao.findLastFiredForRule(anyLong(), eq(VM_ID))).thenReturn(null);
+
+ manager.evaluateRules();
+
+ verify(mailSender).sendMail(mailCaptor.capture());
+ SMTPMailProperties mail = mailCaptor.getValue();
+ assertTrue(mail.getSubject().contains("CPU_UTILIZATION"));
+ assertTrue(mail.getSubject().contains("HIGH"));
+ assertTrue(mail.getContent().toString().contains("85."));
+ }
+
+ @Test
+ public void testEmailSkippedWhenRuleHasEmailDisabled() throws Exception {
+ injectMailSender("admin@example.com");
+
+ ResourceAlertRuleVO rule = vmCpuRuleWithEmail(VM_ID, false);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ VmStats stats = mock(VmStats.class);
+ when(stats.getCPUUtilization()).thenReturn(85.0);
+ when(statsCollector.getVmStats(VM_ID, false)).thenReturn(stats);
+ when(alertDao.findLastFiredForRule(anyLong(), eq(VM_ID))).thenReturn(null);
+
+ manager.evaluateRules();
+
+ verify(mailSender, never()).sendMail(any());
+ }
+
+ @Test
+ public void testEmailSkippedWhenNoRecipientsConfigured() throws Exception {
+ // mailSender injected but no recipients → should not attempt to send
+ injectMailSender(/* no recipients */);
+
+ ResourceAlertRuleVO rule = vmCpuRuleWithEmail(VM_ID, true);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ VmStats stats = mock(VmStats.class);
+ when(stats.getCPUUtilization()).thenReturn(85.0);
+ when(statsCollector.getVmStats(VM_ID, false)).thenReturn(stats);
+ when(alertDao.findLastFiredForRule(anyLong(), eq(VM_ID))).thenReturn(null);
+
+ manager.evaluateRules();
+
+ verify(mailSender, never()).sendMail(any());
+ }
+
+ @Test
+ public void testSubjectContainsKeyAlertFields() throws Exception {
+ injectMailSender("admin@example.com");
+
+ ResourceAlertRuleVO rule = vmCpuRuleWithEmail(VM_ID, true);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ VmStats stats = mock(VmStats.class);
+ when(stats.getCPUUtilization()).thenReturn(85.0);
+ when(statsCollector.getVmStats(VM_ID, false)).thenReturn(stats);
+ when(alertDao.findLastFiredForRule(anyLong(), eq(VM_ID))).thenReturn(null);
+
+ manager.evaluateRules();
+
+ verify(mailSender).sendMail(mailCaptor.capture());
+ String subject = mailCaptor.getValue().getSubject();
+ assertTrue(subject.contains("HIGH"));
+ assertTrue(subject.contains("CPU_UTILIZATION"));
+ assertTrue(subject.contains("GT"));
+ assertTrue(subject.contains("VirtualMachine"));
+ }
+
+ @Test
+ public void testGetDataCenterIdUsesVmDao() {
+ ResourceAlertRuleVO rule = vmCpuRule(VM_ID);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ VmStats stats = mock(VmStats.class);
+ when(stats.getCPUUtilization()).thenReturn(85.0);
+ when(statsCollector.getVmStats(VM_ID, false)).thenReturn(stats);
+ when(alertDao.findLastFiredForRule(anyLong(), eq(VM_ID))).thenReturn(null);
+
+ UserVmVO vm = mock(UserVmVO.class);
+ when(vm.getDataCenterId()).thenReturn(42L);
+ when(userVmDao.findById(VM_ID)).thenReturn(vm);
+
+ manager.evaluateRules();
+
+ verify(manager).publishAlertEvent(eq(42L), anyString(), anyString());
+ }
+
+ @Test
+ public void testGenericRuleSkipsOptedOutVm() {
+ ResourceAlertRuleVO rule = vmCpuRule(null);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ UserVmVO vm = mock(UserVmVO.class);
+ when(vm.getId()).thenReturn(VM_ID);
+ when(vm.getState()).thenReturn(VirtualMachine.State.Running);
+ when(userVmDao.listByAccountId(1L)).thenReturn(Collections.singletonList(vm));
+
+ ResourceTag optOutTag = mock(ResourceTag.class);
+ when(optOutTag.getValue()).thenReturn("true");
+ when(resourceTagDao.findByKey(VM_ID, ResourceTag.ResourceObjectType.UserVm, "resource.alert.opt.out"))
+ .thenReturn(optOutTag);
+
+ manager.evaluateRules();
+
+ verify(alertDao, never()).persist(any());
+ }
+
+ @Test
+ public void testGenericRuleDoesNotSkipVmWithOptOutTagValueFalse() {
+ ResourceAlertRuleVO rule = vmCpuRule(null);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ UserVmVO vm = mock(UserVmVO.class);
+ when(vm.getId()).thenReturn(VM_ID);
+ when(vm.getState()).thenReturn(VirtualMachine.State.Running);
+ when(userVmDao.listByAccountId(1L)).thenReturn(Collections.singletonList(vm));
+
+ ResourceTag tag = mock(ResourceTag.class);
+ when(tag.getValue()).thenReturn("false");
+ when(resourceTagDao.findByKey(VM_ID, ResourceTag.ResourceObjectType.UserVm, "resource.alert.opt.out"))
+ .thenReturn(tag);
+ when(ruleDao.existsSpecificRule(ResourceAlertRule.ResourceType.VirtualMachine, "CPU_UTILIZATION", VM_ID))
+ .thenReturn(false);
+
+ VmStats stats = mock(VmStats.class);
+ when(stats.getCPUUtilization()).thenReturn(85.0);
+ when(statsCollector.getVmStats(VM_ID, false)).thenReturn(stats);
+ when(alertDao.findLastFiredForRule(anyLong(), eq(VM_ID))).thenReturn(null);
+
+ manager.evaluateRules();
+
+ verify(alertDao).persist(any());
+ }
+
+ @Test
+ public void testGenericRuleSkipsVmWithSpecificRuleForSameMetric() {
+ ResourceAlertRuleVO rule = vmCpuRule(null);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ UserVmVO vm = mock(UserVmVO.class);
+ when(vm.getId()).thenReturn(VM_ID);
+ when(vm.getState()).thenReturn(VirtualMachine.State.Running);
+ when(userVmDao.listByAccountId(1L)).thenReturn(Collections.singletonList(vm));
+
+ when(resourceTagDao.findByKey(VM_ID, ResourceTag.ResourceObjectType.UserVm, "resource.alert.opt.out"))
+ .thenReturn(null);
+ when(ruleDao.existsSpecificRule(ResourceAlertRule.ResourceType.VirtualMachine, "CPU_UTILIZATION", VM_ID))
+ .thenReturn(true);
+
+ manager.evaluateRules();
+
+ verify(alertDao, never()).persist(any());
+ }
+
+ @Test
+ public void testSpecificRuleIgnoresOptOutAndPrecedenceChecks() {
+ // specific rule (non-null resourceId) must not check opt-out or precedence
+ ResourceAlertRuleVO rule = vmCpuRule(VM_ID);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ VmStats stats = mock(VmStats.class);
+ when(stats.getCPUUtilization()).thenReturn(85.0);
+ when(statsCollector.getVmStats(VM_ID, false)).thenReturn(stats);
+ when(alertDao.findLastFiredForRule(anyLong(), eq(VM_ID))).thenReturn(null);
+
+ manager.evaluateRules();
+
+ verify(alertDao).persist(any());
+ verify(resourceTagDao, never()).findByKey(anyLong(), any(), anyString());
+ verify(ruleDao, never()).existsSpecificRule(any(), anyString(), anyLong());
+ }
+
+ @Test
+ public void testGetDataCenterIdFallsBackToZeroWhenVmNotFound() {
+ ResourceAlertRuleVO rule = vmCpuRule(VM_ID);
+ when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule));
+
+ VmStats stats = mock(VmStats.class);
+ when(stats.getCPUUtilization()).thenReturn(85.0);
+ when(statsCollector.getVmStats(VM_ID, false)).thenReturn(stats);
+ when(alertDao.findLastFiredForRule(anyLong(), eq(VM_ID))).thenReturn(null);
+ when(userVmDao.findById(VM_ID)).thenReturn(null);
+
+ manager.evaluateRules();
+
+ verify(manager).publishAlertEvent(eq(0L), anyString(), anyString());
+ }
+}
diff --git a/plugins/resource-alerts/src/test/java/org/apache/cloudstack/resourcealert/ResourceAlertMetricTest.java b/plugins/resource-alerts/src/test/java/org/apache/cloudstack/resourcealert/ResourceAlertMetricTest.java
new file mode 100644
index 000000000000..84b95c06f9a9
--- /dev/null
+++ b/plugins/resource-alerts/src/test/java/org/apache/cloudstack/resourcealert/ResourceAlertMetricTest.java
@@ -0,0 +1,73 @@
+// 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.cloudstack.resourcealert;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class ResourceAlertMetricTest {
+
+ @Test
+ public void testCpuAppliesToVmAndHost() {
+ assertTrue(ResourceAlertMetric.CPU_UTILIZATION.appliesTo(ResourceAlertRule.ResourceType.VirtualMachine));
+ assertTrue(ResourceAlertMetric.CPU_UTILIZATION.appliesTo(ResourceAlertRule.ResourceType.Host));
+ assertFalse(ResourceAlertMetric.CPU_UTILIZATION.appliesTo(ResourceAlertRule.ResourceType.StoragePool));
+ assertFalse(ResourceAlertMetric.CPU_UTILIZATION.appliesTo(ResourceAlertRule.ResourceType.Volume));
+ }
+
+ @Test
+ public void testMemoryAppliesToVmAndHost() {
+ assertTrue(ResourceAlertMetric.MEMORY_UTILIZATION.appliesTo(ResourceAlertRule.ResourceType.VirtualMachine));
+ assertTrue(ResourceAlertMetric.MEMORY_UTILIZATION.appliesTo(ResourceAlertRule.ResourceType.Host));
+ assertFalse(ResourceAlertMetric.MEMORY_UTILIZATION.appliesTo(ResourceAlertRule.ResourceType.StoragePool));
+ assertFalse(ResourceAlertMetric.MEMORY_UTILIZATION.appliesTo(ResourceAlertRule.ResourceType.Volume));
+ }
+
+ @Test
+ public void testDiskMetricsApplyToVmAndVolume() {
+ for (ResourceAlertMetric m : new ResourceAlertMetric[]{
+ ResourceAlertMetric.DISK_READ_IOPS, ResourceAlertMetric.DISK_WRITE_IOPS,
+ ResourceAlertMetric.DISK_READ_KBPS, ResourceAlertMetric.DISK_WRITE_KBPS}) {
+ assertTrue(m.name(), m.appliesTo(ResourceAlertRule.ResourceType.VirtualMachine));
+ assertTrue(m.name(), m.appliesTo(ResourceAlertRule.ResourceType.Volume));
+ assertFalse(m.name(), m.appliesTo(ResourceAlertRule.ResourceType.Host));
+ assertFalse(m.name(), m.appliesTo(ResourceAlertRule.ResourceType.StoragePool));
+ }
+ }
+
+ @Test
+ public void testNetworkMetricsApplyToVmOnly() {
+ for (ResourceAlertMetric m : new ResourceAlertMetric[]{
+ ResourceAlertMetric.NETWORK_READ_KBPS, ResourceAlertMetric.NETWORK_WRITE_KBPS}) {
+ assertTrue(m.name(), m.appliesTo(ResourceAlertRule.ResourceType.VirtualMachine));
+ assertFalse(m.name(), m.appliesTo(ResourceAlertRule.ResourceType.Host));
+ assertFalse(m.name(), m.appliesTo(ResourceAlertRule.ResourceType.StoragePool));
+ assertFalse(m.name(), m.appliesTo(ResourceAlertRule.ResourceType.Volume));
+ }
+ }
+
+ @Test
+ public void testStorageUtilizationAppliesToStoragePoolOnly() {
+ assertTrue(ResourceAlertMetric.STORAGE_UTILIZATION.appliesTo(ResourceAlertRule.ResourceType.StoragePool));
+ assertFalse(ResourceAlertMetric.STORAGE_UTILIZATION.appliesTo(ResourceAlertRule.ResourceType.VirtualMachine));
+ assertFalse(ResourceAlertMetric.STORAGE_UTILIZATION.appliesTo(ResourceAlertRule.ResourceType.Host));
+ assertFalse(ResourceAlertMetric.STORAGE_UTILIZATION.appliesTo(ResourceAlertRule.ResourceType.Volume));
+ }
+}
diff --git a/plugins/resource-alerts/src/test/java/org/apache/cloudstack/resourcealert/ResourceAlertServiceImplTest.java b/plugins/resource-alerts/src/test/java/org/apache/cloudstack/resourcealert/ResourceAlertServiceImplTest.java
new file mode 100644
index 000000000000..31d1442a4521
--- /dev/null
+++ b/plugins/resource-alerts/src/test/java/org/apache/cloudstack/resourcealert/ResourceAlertServiceImplTest.java
@@ -0,0 +1,154 @@
+// 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.cloudstack.resourcealert;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.cloud.domain.DomainVO;
+import com.cloud.user.Account;
+
+import org.apache.cloudstack.resourcealert.api.command.admin.CreateResourceAlertRuleCmd;
+import org.apache.cloudstack.resourcealert.api.command.admin.DeleteResourceAlertRuleCmd;
+import org.apache.cloudstack.resourcealert.api.command.admin.ListResourceAlertsCmd;
+import org.apache.cloudstack.resourcealert.api.command.admin.UpdateResourceAlertRuleCmd;
+import org.apache.cloudstack.resourcealert.dao.ResourceAlertDao;
+import org.apache.cloudstack.resourcealert.dao.ResourceAlertRuleDao;
+import org.apache.cloudstack.resourcealert.dao.ResourceAlertRuleJoinDao;
+import org.apache.cloudstack.resourcealert.vo.ResourceAlertRuleVO;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import com.cloud.domain.dao.DomainDao;
+import com.cloud.exception.InvalidParameterValueException;
+import com.cloud.user.AccountManager;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ResourceAlertServiceImplTest {
+
+ @InjectMocks
+ ResourceAlertServiceImpl service;
+
+ @Mock AccountManager accountManager;
+ @Mock DomainDao domainDao;
+ @Mock ResourceAlertRuleDao ruleDao;
+ @Mock ResourceAlertRuleJoinDao ruleJoinDao;
+ @Mock ResourceAlertDao alertDao;
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testCreateFailsOnInvalidCondition() {
+ CreateResourceAlertRuleCmd cmd = mock(CreateResourceAlertRuleCmd.class);
+ when(cmd.getResourceType()).thenReturn("VirtualMachine");
+ when(cmd.getCondition()).thenReturn("GREATER_THAN");
+
+ service.createResourceAlertRule(cmd);
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testCreateFailsOnInvalidSeverity() {
+ CreateResourceAlertRuleCmd cmd = mock(CreateResourceAlertRuleCmd.class);
+ when(cmd.getResourceType()).thenReturn("VirtualMachine");
+ when(cmd.getCondition()).thenReturn("GT");
+ when(cmd.getSeverity()).thenReturn("URGENT");
+
+ service.createResourceAlertRule(cmd);
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testCreateFailsOnInvalidResourceType() {
+ CreateResourceAlertRuleCmd cmd = mock(CreateResourceAlertRuleCmd.class);
+ when(cmd.getResourceType()).thenReturn("Database");
+
+ service.createResourceAlertRule(cmd);
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testCreateFailsWhenMetricDoesNotApplyToResourceType() {
+ CreateResourceAlertRuleCmd cmd = mock(CreateResourceAlertRuleCmd.class);
+ when(cmd.getResourceType()).thenReturn("VirtualMachine");
+ when(cmd.getCondition()).thenReturn("GT");
+ when(cmd.getSeverity()).thenReturn("HIGH");
+ when(cmd.getMetric()).thenReturn("STORAGE_UTILIZATION"); // only applies to StoragePool
+
+ service.createResourceAlertRule(cmd);
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testUpdateFailsWhenRuleNotFound() {
+ UpdateResourceAlertRuleCmd cmd = mock(UpdateResourceAlertRuleCmd.class);
+ when(cmd.getId()).thenReturn(999L);
+ when(ruleDao.findById(999L)).thenReturn(null);
+
+ service.updateResourceAlertRule(cmd);
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testUpdateFailsWhenRuleAlreadyDeleted() {
+ UpdateResourceAlertRuleCmd cmd = mock(UpdateResourceAlertRuleCmd.class);
+ when(cmd.getId()).thenReturn(1L);
+
+ ResourceAlertRuleVO deletedRule = mock(ResourceAlertRuleVO.class);
+ when(deletedRule.getRemoved()).thenReturn(new java.util.Date());
+ when(ruleDao.findById(1L)).thenReturn(deletedRule);
+
+ service.updateResourceAlertRule(cmd);
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testDeleteFailsWhenRuleNotFound() {
+ DeleteResourceAlertRuleCmd cmd = mock(DeleteResourceAlertRuleCmd.class);
+ when(cmd.getId()).thenReturn(999L);
+ when(ruleDao.findById(999L)).thenReturn(null);
+
+ service.deleteResourceAlertRule(cmd);
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testCreateFailsWhenAccountAtRuleLimit() {
+ CreateResourceAlertRuleCmd cmd = mock(CreateResourceAlertRuleCmd.class);
+ when(cmd.getResourceType()).thenReturn("VirtualMachine");
+ when(cmd.getCondition()).thenReturn("GT");
+ when(cmd.getSeverity()).thenReturn("HIGH");
+ when(cmd.getMetric()).thenReturn("CPU_UTILIZATION");
+ when(cmd.getAccountName()).thenReturn("testuser");
+ when(cmd.getDomainId()).thenReturn(1L);
+
+ when(domainDao.findById(1L)).thenReturn(mock(DomainVO.class));
+
+ Account account = mock(Account.class);
+ when(account.getId()).thenReturn(42L);
+ when(accountManager.getActiveAccountByName("testuser", 1L)).thenReturn(account);
+
+ // default limit is 20; returning 20 means account is at the limit
+ when(ruleDao.countActiveByAccountId(42L)).thenReturn(20);
+
+ service.createResourceAlertRule(cmd);
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testListAlertsFailsWithUnknownRuleUuid() {
+ ListResourceAlertsCmd cmd = mock(ListResourceAlertsCmd.class);
+ when(cmd.getAlertRuleId()).thenReturn("no-such-uuid");
+ when(ruleDao.findByUuid("no-such-uuid")).thenReturn(null);
+
+ service.listResourceAlerts(cmd);
+ }
+}
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 14f2fe6597fa..89f5825833cf 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -2215,6 +2215,20 @@
"label.routeripv6": "IPv6 address for the VR in this Network.",
"label.routing.firewall": "IPv4 Routing Firewall",
"label.resourcegroup": "Resource group",
+"label.resource.alert.rules": "Resource Alert Rules",
+"label.resource.alerts": "Resource Alerts",
+"label.create.resource.alert.rule": "Create Resource Alert Rule",
+"label.firedalerts": "Fired Alerts",
+"label.resourcealerts": "Resource Alerts",
+"label.metric": "Metric",
+"label.condition": "Condition",
+"label.severity": "Severity",
+"label.message": "Message",
+"label.resetinterval": "Reset Interval (seconds)",
+"label.alerttimestamp": "Alert Time",
+"label.metrictype": "Metric",
+"label.metricvalue": "Value",
+"message.confirm.delete.resource.alert.rule": "Are you sure you want to delete this alert rule?",
"label.routingmode": "Routing mode",
"label.routing.policy": "Routing policy",
"label.routing.policy.terms": "Routing policy terms",
diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue
index 56fe109099d2..3b2b96e582b0 100644
--- a/ui/src/components/view/ListView.vue
+++ b/ui/src/components/view/ListView.vue
@@ -1219,7 +1219,7 @@ export default {
'/zone', '/pod', '/cluster', '/host', '/storagepool', '/imagestore', '/systemvm', '/router', '/ilbvm', '/annotation',
'/computeoffering', '/systemoffering', '/diskoffering', '/backupoffering', '/networkoffering', '/vpcoffering',
'/tungstenfabric', '/oauthsetting', '/guestos', '/guestoshypervisormapping', '/webhook', 'webhookdeliveries', 'webhookfilters', '/quotatariff', '/sharedfs',
- '/ipv4subnets', '/managementserver', '/gpucard', '/gpudevices', '/vgpuprofile', '/extension', '/snapshotpolicy', '/backupschedule'].join('|'))
+ '/ipv4subnets', '/managementserver', '/gpucard', '/gpudevices', '/vgpuprofile', '/extension', '/snapshotpolicy', '/backupschedule', '/resourcealertrule'].join('|'))
.test(this.$route.path)
},
enableGroupAction () {
@@ -1227,7 +1227,8 @@ export default {
'vmsnapshot', 'backup', 'guestnetwork', 'vpc', 'publicip', 'vpnuser', 'vpncustomergateway', 'vnfapp',
'project', 'account', 'systemvm', 'router', 'computeoffering', 'systemoffering',
'diskoffering', 'backupoffering', 'networkoffering', 'vpcoffering', 'ilbvm', 'kubernetes', 'comment', 'buckets',
- 'webhook', 'webhookdeliveries', 'sharedfs', 'ipv4subnets', 'asnumbers', 'guestos', 'gpucard', 'gpudevices', 'vgpuprofile'
+ 'webhook', 'webhookdeliveries', 'sharedfs', 'ipv4subnets', 'asnumbers', 'guestos', 'gpucard', 'gpudevices', 'vgpuprofile',
+ 'resourcealertrule'
].includes(this.$route.name)
},
getDateAtTimeZone (date, timezone) {
diff --git a/ui/src/components/view/ResourceAlertFiredTab.vue b/ui/src/components/view/ResourceAlertFiredTab.vue
new file mode 100644
index 000000000000..af3a407dbd23
--- /dev/null
+++ b/ui/src/components/view/ResourceAlertFiredTab.vue
@@ -0,0 +1,94 @@
+// 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.
+
+
+
+
+
+
+ {{ $toLocaleDate(text) }}
+
+
+ {{ text }}
+
+
+
+
+
+
+
diff --git a/ui/src/components/view/ResourceAlertsTab.vue b/ui/src/components/view/ResourceAlertsTab.vue
new file mode 100644
index 000000000000..95ee54ebb093
--- /dev/null
+++ b/ui/src/components/view/ResourceAlertsTab.vue
@@ -0,0 +1,93 @@
+// 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.
+
+
+
+
+
+
+ {{ $toLocaleDate(text) }}
+
+
+ {{ text }}
+
+
+
+
+
+
+
diff --git a/ui/src/config/section/infra.js b/ui/src/config/section/infra.js
index dc365b74c930..8e346a896b22 100644
--- a/ui/src/config/section/infra.js
+++ b/ui/src/config/section/infra.js
@@ -29,6 +29,7 @@ import systemVms from '@/config/section/infra/systemVms'
import routers from '@/config/section/infra/routers'
import ilbvms from '@/config/section/infra/ilbvms'
import managementServers from '@/config/section/infra/managementServers'
+import resourceAlertRules from '@/config/section/infra/resourceAlertRules'
export default {
name: 'infra',
@@ -94,6 +95,7 @@ export default {
permission: ['listDbMetrics', 'listUsageServerMetrics'],
component: () => import('@/views/infra/Metrics.vue')
},
+ resourceAlertRules,
{
name: 'alert',
title: 'label.alerts',
diff --git a/ui/src/config/section/infra/hosts.js b/ui/src/config/section/infra/hosts.js
index 48e850a22fbe..863af47fd53e 100644
--- a/ui/src/config/section/infra/hosts.js
+++ b/ui/src/config/section/infra/hosts.js
@@ -53,6 +53,11 @@ export default {
name: 'gpu',
resourceType: 'Host',
component: shallowRef(defineAsyncComponent(() => import('@/components/view/GPUTab.vue')))
+ }, {
+ name: 'resourcealerts',
+ resourceType: 'Host',
+ component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceAlertsTab.vue'))),
+ show: () => { return 'listResourceAlerts' in store.getters.apis }
}, {
name: 'events',
resourceType: 'Host',
diff --git a/ui/src/config/section/infra/primaryStorages.js b/ui/src/config/section/infra/primaryStorages.js
index f127a0853b9e..bb7bd3fde703 100644
--- a/ui/src/config/section/infra/primaryStorages.js
+++ b/ui/src/config/section/infra/primaryStorages.js
@@ -66,6 +66,11 @@ export default {
name: 'browser',
resourceType: 'PrimaryStorage',
component: shallowRef(defineAsyncComponent(() => import('@/views/infra/StorageBrowser.vue')))
+ }, {
+ name: 'resourcealerts',
+ resourceType: 'StoragePool',
+ component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceAlertsTab.vue'))),
+ show: () => { return 'listResourceAlerts' in store.getters.apis }
}, {
name: 'events',
resourceType: 'StoragePool',
diff --git a/ui/src/config/section/infra/resourceAlertRules.js b/ui/src/config/section/infra/resourceAlertRules.js
new file mode 100644
index 000000000000..bab6d45df9ac
--- /dev/null
+++ b/ui/src/config/section/infra/resourceAlertRules.js
@@ -0,0 +1,71 @@
+// 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.
+
+import { shallowRef, defineAsyncComponent } from 'vue'
+import store from '@/store'
+
+export default {
+ name: 'resourcealertrule',
+ title: 'label.resource.alert.rules',
+ icon: 'BellOutlined',
+ permission: ['listResourceAlertRules'],
+ columns: ['name', 'resourcetype', 'metric', 'condition', 'threshold', 'severity', 'email'],
+ details: ['name', 'id', 'resourcetype', 'resourceid', 'metric', 'condition', 'threshold', 'severity', 'message', 'email', 'resetinterval', 'account', 'domain', 'created'],
+ searchFilters: ['name', 'resourcetype'],
+ tabs: [{
+ name: 'details',
+ component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue')))
+ }, {
+ name: 'firedalerts',
+ component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceAlertFiredTab.vue'))),
+ show: () => { return 'listResourceAlerts' in store.getters.apis }
+ }],
+ actions: [
+ {
+ api: 'createResourceAlertRule',
+ icon: 'plus-outlined',
+ label: 'label.create.resource.alert.rule',
+ listView: true,
+ popup: true,
+ component: shallowRef(defineAsyncComponent(() => import('@/views/resourcealert/CreateResourceAlertRule.vue')))
+ },
+ {
+ api: 'updateResourceAlertRule',
+ icon: 'edit-outlined',
+ label: 'label.edit',
+ dataView: true,
+ args: ['name', 'condition', 'threshold', 'severity', 'message', 'email', 'resetinterval'],
+ mapping: {
+ condition: {
+ options: ['GT', 'GTE', 'LT', 'LTE', 'EQ']
+ },
+ severity: {
+ options: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']
+ }
+ }
+ },
+ {
+ api: 'deleteResourceAlertRule',
+ icon: 'delete-outlined',
+ label: 'label.delete',
+ message: 'message.confirm.delete.resource.alert.rule',
+ dataView: true,
+ groupAction: true,
+ groupMap: (selection) => { return selection.map(x => { return { id: x.id } }) }
+ }
+ ]
+}
diff --git a/ui/src/config/section/storage.js b/ui/src/config/section/storage.js
index 75432314b034..cd3bb88e4cb5 100644
--- a/ui/src/config/section/storage.js
+++ b/ui/src/config/section/storage.js
@@ -80,6 +80,12 @@ export default {
component: shallowRef(defineAsyncComponent(() => import('@/components/view/StatsTab.vue'))),
show: (record) => { return store.getters.features.instancesdisksstatsretentionenabled }
},
+ {
+ name: 'resourcealerts',
+ resourceType: 'Volume',
+ component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceAlertsTab.vue'))),
+ show: () => { return 'listResourceAlerts' in store.getters.apis }
+ },
{
name: 'events',
resourceType: 'Volume',
diff --git a/ui/src/views/compute/InstanceTab.vue b/ui/src/views/compute/InstanceTab.vue
index 9576e70c8d58..631835e232a9 100644
--- a/ui/src/views/compute/InstanceTab.vue
+++ b/ui/src/views/compute/InstanceTab.vue
@@ -95,6 +95,9 @@
+
+
+
@@ -151,6 +154,7 @@ import TooltipButton from '@/components/widgets/TooltipButton'
import ResourceIcon from '@/components/view/ResourceIcon'
import AnnotationsTab from '@/components/view/AnnotationsTab'
import VolumesTab from '@/components/view/VolumesTab.vue'
+import ResourceAlertsTab from '@/components/view/ResourceAlertsTab.vue'
import SecurityGroupSelection from '@views/compute/wizard/SecurityGroupSelection'
import GPUTab from '@/components/view/GPUTab.vue'
@@ -168,6 +172,7 @@ export default {
InstanceSchedules,
ListResourceTable,
SecurityGroupSelection,
+ ResourceAlertsTab,
TooltipButton,
ResourceIcon,
AnnotationsTab,
diff --git a/ui/src/views/resourcealert/CreateResourceAlertRule.vue b/ui/src/views/resourcealert/CreateResourceAlertRule.vue
new file mode 100644
index 000000000000..77e76fea01d5
--- /dev/null
+++ b/ui/src/views/resourcealert/CreateResourceAlertRule.vue
@@ -0,0 +1,170 @@
+// 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.
+
+
+
+
+
+
+ {{ $t('label.name') }}
+
+
+
+
+ {{ $t('label.resourcetype') }}
+
+ {{ rt }}
+
+
+
+
+ {{ $t('label.metric') }}
+
+ {{ m }}
+
+
+
+
+ {{ $t('label.condition') }}
+
+ {{ c }}
+
+
+
+
+ {{ $t('label.threshold') }}
+
+
+
+
+ {{ $t('label.severity') }}
+
+ {{ s }}
+
+
+
+
+ {{ $t('label.message') }}
+
+
+
+
+ {{ $t('label.email') }}
+
+
+
+
+ {{ $t('label.resetinterval') }}
+
+
+
+
+
+
+
+
+
+
+