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. + + + + 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. + + + + 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. + + + + + +