diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index f7d13343d469..912f970ea0f4 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -822,6 +822,11 @@ public class EventTypes { public static final String EVENT_QUOTA_TARIFF_DELETE = "QUOTA.TARIFF.DELETE"; public static final String EVENT_QUOTA_TARIFF_UPDATE = "QUOTA.TARIFF.UPDATE"; + // Resource alert rules + public static final String EVENT_RESOURCE_ALERT_RULE_CREATE = "RESOURCE.ALERT.RULE.CREATE"; + public static final String EVENT_RESOURCE_ALERT_RULE_UPDATE = "RESOURCE.ALERT.RULE.UPDATE"; + public static final String EVENT_RESOURCE_ALERT_RULE_DELETE = "RESOURCE.ALERT.RULE.DELETE"; + // Routing public static final String EVENT_ZONE_IP4_SUBNET_CREATE = "ZONE.IP4.SUBNET.CREATE"; public static final String EVENT_ZONE_IP4_SUBNET_UPDATE = "ZONE.IP4.SUBNET.UPDATE"; diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index f74c46161180..2bbfc8abec55 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1089,6 +1089,13 @@ public class ApiConstants { public static final String AGGR_FUNCTION = "aggrfunction"; public static final String AGGR_VALUE = "aggrvalue"; public static final String THRESHOLD = "threshold"; + public static final String METRIC = "metric"; + public static final String CONDITION = "condition"; + public static final String SEVERITY = "severity"; + public static final String RESET_INTERVAL = "resetinterval"; + public static final String WEBHOOK_IDS = "webhookids"; + public static final String CLEANUP_WEBHOOKS = "cleanupwebhooks"; + public static final String ALERT_RULE_ID = "alertruleid"; public static final String RELATIONAL_OPERATOR = "relationaloperator"; public static final String OTHER_DEPLOY_PARAMS = "otherdeployparams"; public static final String MIN_MEMBERS = "minmembers"; diff --git a/client/pom.xml b/client/pom.xml index cc031a4912b1..f28b3ca9434d 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -432,6 +432,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/java/com/cloud/storage/dao/VolumeDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java index 4cd9a8e23bfc..31167621563d 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java @@ -34,6 +34,8 @@ public interface VolumeDao extends GenericDao, StateDao findByAccount(long accountId); + List listIdsByAccountOrDomainsAndState(Long accountId, List domainIds, Volume.State state); + List findIncludingRemovedByAccount(long accountId); Pair getCountAndTotalByPool(long poolId); diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java index 1d5ff5e93402..42093c5ff4f7 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java @@ -75,6 +75,7 @@ public class VolumeDaoImpl extends GenericDaoBase implements Vol private final SearchBuilder storeAndInstallPathSearch; private final SearchBuilder volumeIdSearch; protected GenericSearchBuilder CountByAccount; + protected final GenericSearchBuilder IdsByAccountOrDomainsAndStateSearch; protected final SearchBuilder ExternalUuidSearch; protected GenericSearchBuilder primaryStorageSearch; protected GenericSearchBuilder primaryStorageSearch2; @@ -118,6 +119,21 @@ public List findByAccount(long accountId) { return listBy(sc); } + @Override + public List listIdsByAccountOrDomainsAndState(Long accountId, List domainIds, Volume.State state) { + SearchCriteria sc = IdsByAccountOrDomainsAndStateSearch.create(); + if (accountId != null) { + sc.setParameters("accountId", accountId); + } + if (domainIds != null) { + sc.setParameters("domainIds", domainIds.toArray()); + } + if (state != null) { + sc.setParameters("state", state); + } + return customSearch(sc, null); + } + @Override public List findIncludingRemovedByAccount(long accountId) { SearchCriteria sc = AllFieldsSearch.create(); @@ -420,6 +436,13 @@ public VolumeDaoImpl() { AllFieldsSearch.and("kmsWrappedKeyId", AllFieldsSearch.entity().getKmsWrappedKeyId(), Op.EQ); AllFieldsSearch.done(); + IdsByAccountOrDomainsAndStateSearch = createSearchBuilder(Long.class); + IdsByAccountOrDomainsAndStateSearch.selectFields(IdsByAccountOrDomainsAndStateSearch.entity().getId()); + IdsByAccountOrDomainsAndStateSearch.and("accountId", IdsByAccountOrDomainsAndStateSearch.entity().getAccountId(), Op.EQ); + IdsByAccountOrDomainsAndStateSearch.and("domainIds", IdsByAccountOrDomainsAndStateSearch.entity().getDomainId(), Op.IN); + IdsByAccountOrDomainsAndStateSearch.and("state", IdsByAccountOrDomainsAndStateSearch.entity().getState(), Op.EQ); + IdsByAccountOrDomainsAndStateSearch.done(); + RootDiskStateSearch = createSearchBuilder(); RootDiskStateSearch.and("state", RootDiskStateSearch.entity().getState(), Op.IN); RootDiskStateSearch.and("vType", RootDiskStateSearch.entity().getVolumeType(), Op.EQ); diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDao.java index 7de543e69d31..efb1e49f3290 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDao.java @@ -31,6 +31,8 @@ public interface UserVmDao extends GenericDao { List listByAccountId(long id); + List listIdsByAccountOrDomainsAndState(Long accountId, List domainIds, State state); + List listByAccountAndPod(long accountId, long podId); List listByAccountAndDataCenter(long accountId, long dcId); diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java index 761053a89f0c..3cfd4fa6acb9 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java @@ -82,6 +82,7 @@ public class UserVmDaoImpl extends GenericDaoBase implements Use protected SearchBuilder AccountDataCenterVirtualSearch; protected GenericSearchBuilder CountByAccountPod; protected GenericSearchBuilder CountByAccount; + protected GenericSearchBuilder IdsByAccountOrDomainsAndStateSearch; protected GenericSearchBuilder CountActiveAccount; protected GenericSearchBuilder PodsHavingVmsForAccount; @@ -143,6 +144,13 @@ void init() { AccountSearch.and("account", AccountSearch.entity().getAccountId(), SearchCriteria.Op.EQ); AccountSearch.done(); + IdsByAccountOrDomainsAndStateSearch = createSearchBuilder(Long.class); + IdsByAccountOrDomainsAndStateSearch.selectFields(IdsByAccountOrDomainsAndStateSearch.entity().getId()); + IdsByAccountOrDomainsAndStateSearch.and("accountId", IdsByAccountOrDomainsAndStateSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + IdsByAccountOrDomainsAndStateSearch.and("domainIds", IdsByAccountOrDomainsAndStateSearch.entity().getDomainId(), SearchCriteria.Op.IN); + IdsByAccountOrDomainsAndStateSearch.and("state", IdsByAccountOrDomainsAndStateSearch.entity().getState(), SearchCriteria.Op.EQ); + IdsByAccountOrDomainsAndStateSearch.done(); + IdsSearch = createSearchBuilder(); IdsSearch.and("ids", IdsSearch.entity().getId(), SearchCriteria.Op.IN); IdsSearch.done(); @@ -318,6 +326,21 @@ public List listByAccountId(long id) { return listBy(sc); } + @Override + public List listIdsByAccountOrDomainsAndState(Long accountId, List domainIds, State state) { + SearchCriteria sc = IdsByAccountOrDomainsAndStateSearch.create(); + if (accountId != null) { + sc.setParameters("accountId", accountId); + } + if (domainIds != null) { + sc.setParameters("domainIds", domainIds.toArray()); + } + if (state != null) { + sc.setParameters("state", state); + } + return customSearch(sc, null); + } + @Override public List listByHostId(Long id) { SearchCriteria sc = HostSearch.create(); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql index 7c11013a17d2..9f4fbf1d2ad9 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql @@ -18,3 +18,57 @@ --; -- Schema upgrade from 4.23.0.0 to 24.0.0 --; + +-- 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; + +-- resource_alert_rules_webhook: webhooks a rule delivers its alerts to +CREATE TABLE IF NOT EXISTS `cloud`.`resource_alert_rules_webhook` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `resource_alert_rule_id` bigint unsigned NOT NULL, + `webhook_id` bigint unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uc_resource_alert_rules_webhook__rule_webhook`(`resource_alert_rule_id`, `webhook_id`), + CONSTRAINT `fk_resource_alert_rules_webhook__rule_id` FOREIGN KEY (`resource_alert_rule_id`) REFERENCES `resource_alert_rules`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_resource_alert_rules_webhook__webhook_id` FOREIGN KEY (`webhook_id`) REFERENCES `webhook`(`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/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookServiceImpl.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookServiceImpl.java index 85befa13a926..6f8c2d834a86 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookServiceImpl.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookServiceImpl.java @@ -121,13 +121,27 @@ protected WebhookDeliveryThread getDeliveryJob(Event event, Webhook webhook, Del .setContext(context); WebhookDeliveryThread job = new WebhookDeliveryThread(webhook, event, caller); job = ComponentContext.inject(job); + applyDeliveryConfig(job, config); + return job; + } + + protected DeliveryConfig getDeliveryConfig(long domainId) { + return new DeliveryConfig( + WebhookDeliveryTries.valueIn(domainId), + WebhookDeliveryTimeout.valueIn(domainId), + WebhookDeliveryBlocklist.valueIn(domainId), + WebhookDeliveryBlockLocalAddresses.value(), + WebhookDeliveryAllowRedirects.valueIn(domainId), + WebhookDeliveryAllowHttp.valueIn(domainId)); + } + + protected void applyDeliveryConfig(WebhookDeliveryThread job, DeliveryConfig config) { job.setDeliveryTries(config.tries); job.setDeliveryTimeout(config.timeout); job.setDestinationBlocklist(config.blocklist); job.setBlockLocalAddresses(config.blockLocalAddresses); job.setAllowRedirects(config.allowRedirects); job.setAllowHttp(config.allowHttp); - return job; } protected String getEventValueByFilterType(Event event, WebhookFilter.Type filterType) { @@ -214,17 +228,7 @@ protected List getDeliveryJobs(Event event) throws EventBusException { logger.debug("Skipping delivering {} to {} as it doesn't match filters", event, webhook); continue; } - if (!domainConfigs.containsKey(webhook.getDomainId())) { - domainConfigs.put(webhook.getDomainId(), - new DeliveryConfig( - WebhookDeliveryTries.valueIn(webhook.getDomainId()), - WebhookDeliveryTimeout.valueIn(webhook.getDomainId()), - WebhookDeliveryBlocklist.valueIn(webhook.getDomainId()), - WebhookDeliveryBlockLocalAddresses.value(), - WebhookDeliveryAllowRedirects.valueIn(webhook.getDomainId()), - WebhookDeliveryAllowHttp.valueIn(webhook.getDomainId()))); - } - DeliveryConfig config = domainConfigs.get(webhook.getDomainId()); + DeliveryConfig config = domainConfigs.computeIfAbsent(webhook.getDomainId(), this::getDeliveryConfig); WebhookDeliveryThread job = getDeliveryJob(event, webhook, config); jobs.add(job); } @@ -274,6 +278,54 @@ protected Runnable getManualDeliveryJob(WebhookDelivery existingDelivery, Webhoo return job; } + protected List getDirectDeliveryJobs(List webhookIds, long accountId, String eventType, + String payload) { + List jobs = new ArrayList<>(); + if (CollectionUtils.isEmpty(webhookIds)) { + return jobs; + } + Account account = accountManager.getAccount(accountId); + Event event = new Event(ManagementService.Name, EventCategory.ALERT_EVENT.getName(), eventType, null, null); + event.setEventUuid(UUID.randomUUID().toString()); + event.setDescription(payload); + event.setResourceAccountUuid(account != null ? account.getUuid() : null); + for (Long webhookId : webhookIds) { + WebhookVO webhook = webhookDao.findById(webhookId); + if (webhook == null || !Webhook.State.Enabled.equals(webhook.getState())) { + logger.debug("Skipping delivering {} to webhook ID: {} as it is missing or disabled", event, webhookId); + continue; + } + if (!isEventMatchingFilters(event, webhookFiltersCache.get(webhook.getId()))) { + logger.debug("Skipping delivering {} to {} as it doesn't match filters", event, webhook); + continue; + } + WebhookDeliveryThread.WebhookDeliveryContext context = + new WebhookDeliveryThread.WebhookDeliveryContext<>(null, null, webhook.getId()); + AsyncCallbackDispatcher caller = + AsyncCallbackDispatcher.create(this); + caller.setCallback(caller.getTarget().directDeliveryCompleteCallback(null, null)) + .setContext(context); + WebhookDeliveryThread job = new WebhookDeliveryThread(webhook, event, caller); + job = ComponentContext.inject(job); + applyDeliveryConfig(job, getDeliveryConfig(webhook.getDomainId())); + jobs.add(job); + } + return jobs; + } + + // Not persisted: webhook_delivery.event_id must reference a row in the event table. + protected Void directDeliveryCompleteCallback( + AsyncCallbackDispatcher callback, + WebhookDeliveryThread.WebhookDeliveryContext context) { + WebhookDeliveryThread.WebhookDeliveryResult result = callback.getResult(); + if (result.isSuccess()) { + logger.debug("Delivered alert to webhook ID: {}", context.getRuleId()); + } else { + logger.warn("Failed to deliver alert to webhook ID: {} due to: {}", context.getRuleId(), result.getResult()); + } + return null; + } + protected Void deliveryCompleteCallback( AsyncCallbackDispatcher callback, WebhookDeliveryThread.WebhookDeliveryContext context) { @@ -386,6 +438,35 @@ public List listWebhooksByAccount(long accountId) { return webhookDao.listByAccount(accountId); } + @Override + public ControlledEntity findWebhookByUuid(String uuid) { + return webhookDao.findByUuid(uuid); + } + + @Override + public String getWebhookUuid(long webhookId) { + WebhookVO webhook = webhookDao.findByIdIncludingRemoved(webhookId); + return webhook != null ? webhook.getUuid() : null; + } + + @Override + public void deliverToWebhooks(List webhookIds, long accountId, String eventType, String payload) { + for (Runnable job : getDirectDeliveryJobs(webhookIds, accountId, eventType, payload)) { + webhookJobExecutor.submit(loggingFailures(job, eventType)); + } + } + + // The executor swallows exceptions, e.g. a payload URL rejected by the blocklist at delivery time. + protected Runnable loggingFailures(Runnable job, String eventType) { + return () -> { + try { + job.run(); + } catch (Exception e) { + logger.warn("Failed to deliver {} to webhook: {}", eventType, e.getMessage()); + } + }; + } + @Override public void handleEvent(Event event) throws EventBusException { List jobs = getDeliveryJobs(event); diff --git a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookServiceImplTest.java b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookServiceImplTest.java index e945b990c808..8f7bc831c79b 100644 --- a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookServiceImplTest.java +++ b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookServiceImplTest.java @@ -34,6 +34,7 @@ import org.apache.cloudstack.mom.webhook.dao.WebhookDeliveryDao; import org.apache.cloudstack.mom.webhook.dao.WebhookFilterDao; import org.apache.cloudstack.mom.webhook.vo.WebhookDeliveryVO; +import org.apache.cloudstack.mom.webhook.vo.WebhookFilterVO; import org.apache.cloudstack.mom.webhook.vo.WebhookVO; import org.apache.cloudstack.utils.cache.LazyCache; import org.apache.commons.lang3.StringUtils; @@ -670,4 +671,89 @@ public void invalidateWebhookFiltersCacheInvalidatesSpecificCacheEntry() { Mockito.verify(cache, Mockito.times(1)).invalidate(123L); } + + @Test + public void getDirectDeliveryJobsReturnsEmptyForNoWebhooks() { + Assert.assertTrue(webhookServiceImpl.getDirectDeliveryJobs(new ArrayList<>(), 1L, "RESOURCE.ALERT", "{}").isEmpty()); + } + + @Test + public void getDirectDeliveryJobsSkipsMissingAndDisabledWebhooks() { + WebhookVO disabled = Mockito.mock(WebhookVO.class); + Mockito.when(disabled.getState()).thenReturn(Webhook.State.Disabled); + Mockito.when(webhookDao.findById(1L)).thenReturn(disabled); + Mockito.when(webhookDao.findById(2L)).thenReturn(null); + + List jobs = webhookServiceImpl.getDirectDeliveryJobs(List.of(1L, 2L), 1L, "RESOURCE.ALERT", "{}"); + + Assert.assertTrue(jobs.isEmpty()); + } + + @Test + public void getDirectDeliveryJobsBuildsAlertEventForEnabledWebhook() { + WebhookVO webhook = Mockito.mock(WebhookVO.class); + Mockito.when(webhook.getId()).thenReturn(1L); + Mockito.when(webhook.getState()).thenReturn(Webhook.State.Enabled); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhook); + Account account = Mockito.mock(Account.class); + Mockito.when(account.getUuid()).thenReturn("account-uuid"); + Mockito.when(accountManager.getAccount(5L)).thenReturn(account); + + List jobs = webhookServiceImpl.getDirectDeliveryJobs(List.of(1L), 5L, "RESOURCE.ALERT", "{\"a\":1}"); + + Assert.assertEquals(1, jobs.size()); + Event event = (Event) ReflectionTestUtils.getField(jobs.get(0), "event"); + Assert.assertEquals(EventCategory.ALERT_EVENT.getName(), event.getEventCategory()); + Assert.assertEquals("RESOURCE.ALERT", event.getEventType()); + Assert.assertEquals("{\"a\":1}", event.getDescription()); + Assert.assertEquals("account-uuid", event.getResourceAccountUuid()); + } + + @Test + public void getDirectDeliveryJobsSkipsWebhookWhenFilterExcludesEvent() { + WebhookVO webhook = Mockito.mock(WebhookVO.class); + Mockito.when(webhook.getId()).thenReturn(1L); + Mockito.when(webhook.getState()).thenReturn(Webhook.State.Enabled); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhook); + WebhookFilterVO filter = Mockito.mock(WebhookFilterVO.class); + Mockito.when(filter.getType()).thenReturn(WebhookFilter.Type.EventType); + Mockito.when(filter.getMode()).thenReturn(WebhookFilter.Mode.Exclude); + Mockito.when(filter.getMatchType()).thenReturn(WebhookFilter.MatchType.Exact); + Mockito.when(filter.getValue()).thenReturn("RESOURCE.ALERT"); + Mockito.when(webhookFilterDao.listByWebhook(1L)).thenReturn(List.of(filter)); + + List jobs = webhookServiceImpl.getDirectDeliveryJobs(List.of(1L), 5L, "RESOURCE.ALERT", "{}"); + + Assert.assertTrue(jobs.isEmpty()); + } + + @Test + public void getDirectDeliveryJobsAppliesDeliverySecuritySettings() { + WebhookVO webhook = Mockito.mock(WebhookVO.class); + Mockito.when(webhook.getId()).thenReturn(1L); + Mockito.when(webhook.getDomainId()).thenReturn(3L); + Mockito.when(webhook.getState()).thenReturn(Webhook.State.Enabled); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhook); + WebhookServiceImpl.DeliveryConfig config = new WebhookServiceImpl.DeliveryConfig(2, 7, "10.0.0.0/8", true, false, true); + Mockito.doReturn(config).when(webhookServiceImpl).getDeliveryConfig(3L); + + List jobs = webhookServiceImpl.getDirectDeliveryJobs(List.of(1L), 5L, "RESOURCE.ALERT", "{}"); + + Assert.assertEquals(1, jobs.size()); + Object job = jobs.get(0); + Assert.assertEquals(2, ReflectionTestUtils.getField(job, "deliveryTries")); + Assert.assertEquals(7, ReflectionTestUtils.getField(job, "deliveryTimeout")); + Assert.assertEquals("10.0.0.0/8", ReflectionTestUtils.getField(job, "destinationBlocklist")); + Assert.assertEquals(true, ReflectionTestUtils.getField(job, "blockLocalAddresses")); + Assert.assertEquals(true, ReflectionTestUtils.getField(job, "allowHttp")); + } + + @Test + public void loggingFailuresSwallowsAndDoesNotRethrow() { + Runnable failing = () -> { + throw new com.cloud.exception.InvalidParameterValueException("blocked IP address"); + }; + + webhookServiceImpl.loggingFailures(failing, "RESOURCE.ALERT").run(); + } } diff --git a/plugins/pom.xml b/plugins/pom.xml index 92768827f658..fb5c5c751a1b 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -101,6 +101,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..b1aff0a1ac9c --- /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 + 24.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..a9674b792e42 --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertManagerImpl.java @@ -0,0 +1,611 @@ +// 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.api.Identity; +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.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.resourcealert.dao.ResourceAlertDao; +import org.apache.cloudstack.resourcealert.dao.ResourceAlertRuleDao; +import org.apache.cloudstack.resourcealert.dao.ResourceAlertRuleWebhookDao; +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.identity.ManagementServerNode; +import org.apache.cloudstack.utils.mailing.MailAddress; +import org.apache.cloudstack.utils.mailing.SMTPMailProperties; +import org.apache.cloudstack.utils.mailing.SMTPMailSender; +import org.apache.cloudstack.webhook.WebhookHelper; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; + +import com.cloud.cluster.ManagementServerHostVO; +import com.cloud.cluster.dao.ManagementServerHostDao; +import com.cloud.domain.dao.DomainDao; +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.Storage; +import com.cloud.storage.StorageStats; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeStats; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.tags.dao.ResourceTagDao; +import com.cloud.user.Account; +import com.cloud.user.dao.AccountDao; +import com.cloud.utils.Pair; +import com.cloud.utils.component.ComponentContext; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.db.GlobalLock; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VmStats; +import com.cloud.vm.dao.UserVmDao; +import com.google.gson.JsonObject; + +public class ResourceAlertManagerImpl extends ManagerBase implements ResourceAlertManager, Configurable { + + static final String ALERT_EVENT_TYPE = "RESOURCE.ALERT"; + + static final ConfigKey EVAL_INTERVAL = new ConfigKey<>("Advanced", Integer.class, + "resourcealert.evaluation.interval", "60", + "Interval in seconds between resource alert rule evaluations", false); + + public static final ConfigKey RULES_PER_ACCOUNT_LIMIT = new ConfigKey<>("Advanced", Integer.class, + "resourcealert.per.user.limit", "20", + "Maximum number of resource alert rules an account can own; 0 = unlimited", true, ConfigKey.Scope.Account); + + static final ConfigKey HISTORY_RETENTION_DAYS = new ConfigKey<>("Advanced", Integer.class, + "resourcealert.history.retention.days", "30", + "Number of days to keep fired resource alerts; 0 keeps them forever", true); + + public static final ConfigKey DEFAULT_RESET_INTERVAL = new ConfigKey<>("Advanced", Integer.class, + "resourcealert.repeat.interval.default", "600", + "Default minimum seconds between repeat firings of a resource alert rule, used when a rule does not set one", true); + + @Inject ResourceAlertRuleDao ruleDao; + @Inject ResourceAlertDao alertDao; + @Inject ResourceAlertRuleWebhookDao ruleWebhookDao; + @Inject UserVmDao userVmDao; + @Inject HostDao hostDao; + @Inject PrimaryDataStoreDao storagePoolDao; + @Inject VolumeDao volumeDao; + @Inject StatsCollector statsCollector; + @Inject ConfigurationDao configDao; + @Inject ResourceTagDao resourceTagDao; + @Inject AccountDao accountDao; + @Inject DomainDao domainDao; + @Inject ManagementServerHostDao managementServerHostDao; + + 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(new EvaluationTask(), 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) { + if (isOrphaned(rule)) { + logger.info("Removing resource alert rule {} as its owner or resource no longer exists", rule.getUuid()); + ruleDao.remove(rule.getId()); + continue; + } + evaluateRule(rule); + } + } + + boolean isOrphaned(ResourceAlertRuleVO rule) { + if (accountDao.findById(rule.getAccountId()) == null) { + return true; + } + Long resourceId = rule.getResourceId(); + if (resourceId == null) { + return false; + } + switch (rule.getResourceType()) { + case VirtualMachine: + return userVmDao.findById(resourceId) == null; + case Volume: + return volumeDao.findById(resourceId) == null; + case Host: + return hostDao.findById(resourceId) == null; + case StoragePool: + return storagePoolDao.findById(resourceId) == null; + default: + return false; + } + } + + // Every management server collects stats for all hosts, so only one may evaluate or alerts fire once per server. + boolean isEvaluatingServer() { + ManagementServerHostVO msHost = managementServerHostDao.findOneByLongestRuntime(); + return msHost != null && msHost.getMsid() == ManagementServerNode.getManagementServerId(); + } + + void removeExpiredAlerts() { + int days = HISTORY_RETENTION_DAYS.value(); + if (days <= 0) { + return; + } + int removed = alertDao.removeOlderThan(new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(days))); + if (removed > 0) { + logger.debug("Removed {} resource alerts older than {} days", removed, days); + } + } + + class EvaluationTask extends ManagedContextRunnable { + @Override + protected void runInContext() { + GlobalLock lock = GlobalLock.getInternLock("ResourceAlertEvaluation"); + try { + if (!lock.lock(5)) { + return; + } + try { + if (isEvaluatingServer()) { + evaluateRules(); + removeExpiredAlerts(); + } + } finally { + lock.unlock(); + } + } catch (Exception e) { + logger.warn("Failed to evaluate resource alert rules", e); + } finally { + lock.releaseRef(); + } + } + } + + 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 e) { + logger.warn("Failed to evaluate resource alert rule {} for resource {}", rule.getUuid(), resourceId, e); + } + } + } + + 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: { + Pair> scope = getGenericRuleScope(rule); + return scope == null ? Collections.emptyList() : userVmDao.listIdsByAccountOrDomainsAndState( + scope.first(), scope.second(), VirtualMachine.State.Running); + } + case Volume: { + Pair> scope = getGenericRuleScope(rule); + return scope == null ? Collections.emptyList() : volumeDao.listIdsByAccountOrDomainsAndState( + scope.first(), scope.second(), Volume.State.Ready); + } + 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(); + } + } + + // Root admin rules cover the whole cloud, domain admin rules their domain tree, other rules their own account. + Pair> getGenericRuleScope(ResourceAlertRule rule) { + Account owner = accountDao.findById(rule.getAccountId()); + if (owner == null) { + return null; + } + if (Account.Type.ADMIN.equals(owner.getType())) { + return new Pair<>(null, null); + } + if (Account.Type.DOMAIN_ADMIN.equals(owner.getType()) || Account.Type.RESOURCE_DOMAIN_ADMIN.equals(owner.getType())) { + return new Pair<>(null, domainDao.getDomainAndChildrenIds(owner.getDomainId())); + } + return new Pair<>(owner.getId(), null); + } + + 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: { + if (type == ResourceAlertRule.ResourceType.Host) { + HostStats s = statsCollector.getHostStats(resourceId); + return s != null ? s.getNetworkReadKBs() : null; + } + VmStats s = statsCollector.getVmStats(resourceId, false); + return s != null ? s.getNetworkReadKBs() : null; + } + case NETWORK_WRITE_KBPS: { + if (type == ResourceAlertRule.ResourceType.Host) { + HostStats s = statsCollector.getHostStats(resourceId); + return s != null ? s.getNetworkWriteKBs() : null; + } + VmStats s = statsCollector.getVmStats(resourceId, false); + return s != null ? s.getNetworkWriteKBs() : null; + } + case STORAGE_USED_IOPS: { + // only reported by storage drivers that track IOPS + StorageStats pool = statsCollector.getStoragePoolStats(resourceId); + return pool != null && pool.getUsedIops() != null ? pool.getUsedIops().doubleValue() : null; + } + case VOLUME_SIZE_GB: { + VolumeStats s = getVolumeStats(resourceId); + return s != null ? s.getPhysicalSize() / (1024.0 * 1024.0 * 1024.0) : null; + } + case LOAD_AVERAGE: { + HostStats s = statsCollector.getHostStats(resourceId); + return s != null ? s.getLoadAverage() : 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; + } + + // Stats are keyed by path, except OVA volumes which are keyed by chain info. + private VolumeStats getVolumeStats(long volumeId) { + VolumeVO vol = volumeDao.findById(volumeId); + if (vol == null) return null; + String locator = Storage.ImageFormat.OVA.equals(vol.getFormat()) ? vol.getChainInfo() : vol.getPath(); + return locator != null ? statsCollector.getVolumeStats(locator) : 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); + deliverToWebhooks(rule, alert, resourceId, value); + + if (rule.isEmail()) { + sendEmail(subject, body); + } + + logger.warn("Alert fired: rule={} metric={} resource={} value={} threshold={}", + rule.getUuid(), rule.getMetric(), resourceId, value, rule.getThreshold()); + } + + protected WebhookHelper getWebhookHelper() { + try { + return ComponentContext.getDelegateComponentOfType(WebhookHelper.class); + } catch (NoSuchBeanDefinitionException e) { + return null; + } + } + + private void deliverToWebhooks(ResourceAlertRuleVO rule, ResourceAlertVO alert, Long resourceId, double value) { + List webhookIds = ruleWebhookDao.listWebhookIdsByRule(rule.getId()); + if (webhookIds.isEmpty()) { + return; + } + WebhookHelper webhookHelper = getWebhookHelper(); + if (webhookHelper == null) { + logger.warn("Unable to deliver alert for rule {} to webhooks as the webhook plugin is not enabled", rule.getUuid()); + return; + } + webhookHelper.deliverToWebhooks(webhookIds, rule.getAccountId(), ALERT_EVENT_TYPE, + buildWebhookPayload(rule, alert, resourceId, value)); + } + + String buildWebhookPayload(ResourceAlertRuleVO rule, ResourceAlertVO alert, Long resourceId, double value) { + JsonObject payload = new JsonObject(); + payload.addProperty("event", ALERT_EVENT_TYPE); + payload.addProperty("id", alert.getUuid()); + payload.addProperty("ruleid", rule.getUuid()); + payload.addProperty("rulename", rule.getName()); + payload.addProperty("resourcetype", rule.getResourceType().name()); + payload.addProperty("resourceid", getResourceUuid(rule.getResourceType(), resourceId)); + payload.addProperty("metric", rule.getMetric()); + payload.addProperty("condition", rule.getCondition().name()); + payload.addProperty("threshold", rule.getThreshold()); + payload.addProperty("value", value); + payload.addProperty("severity", rule.getSeverity().name()); + payload.addProperty("message", rule.getMessage()); + payload.addProperty("timestamp", alert.getAlertTimestamp().toInstant().toString()); + return payload.toString(); + } + + private String getResourceUuid(ResourceAlertRule.ResourceType type, Long resourceId) { + if (resourceId == null) { + return null; + } + Identity resource; + switch (type) { + case VirtualMachine: + resource = userVmDao.findByIdIncludingRemoved(resourceId); + break; + case Volume: + resource = volumeDao.findByIdIncludingRemoved(resourceId); + break; + case Host: + resource = hostDao.findByIdIncludingRemoved(resourceId); + break; + case StoragePool: + resource = storagePoolDao.findByIdIncludingRemoved(resourceId); + break; + default: + resource = null; + } + return resource != null ? resource.getUuid() : null; + } + + 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(ALERT_EVENT_TYPE, dcId, null, subject, body); + } catch (Exception e) { + logger.warn("Failed to publish resource alert on the event bus", e); + } + } + + @Override + public String getConfigComponentName() { + return ResourceAlertManagerImpl.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{EVAL_INTERVAL, RULES_PER_ACCOUNT_LIMIT, DEFAULT_RESET_INTERVAL, HISTORY_RETENTION_DAYS}; + } +} 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..639697f9a8b9 --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertMetric.java @@ -0,0 +1,51 @@ +// 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, ResourceAlertRule.ResourceType.Host), + NETWORK_WRITE_KBPS(ResourceAlertRule.ResourceType.VirtualMachine, ResourceAlertRule.ResourceType.Host), + LOAD_AVERAGE(ResourceAlertRule.ResourceType.Host), + VOLUME_SIZE_GB(ResourceAlertRule.ResourceType.Volume), + STORAGE_USED_IOPS(ResourceAlertRule.ResourceType.StoragePool); + + 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); + } + + public boolean isPercentage() { + return this == CPU_UTILIZATION || this == MEMORY_UTILIZATION || this == STORAGE_UTILIZATION; + } +} 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..e1d4d3596006 --- /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.user.CreateResourceAlertRuleCmd; +import org.apache.cloudstack.resourcealert.api.command.user.DeleteResourceAlertRuleCmd; +import org.apache.cloudstack.resourcealert.api.command.user.ListResourceAlertRulesCmd; +import org.apache.cloudstack.resourcealert.api.command.user.ListResourceAlertsCmd; +import org.apache.cloudstack.resourcealert.api.command.user.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..5dde93949b2e --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/ResourceAlertServiceImpl.java @@ -0,0 +1,534 @@ +// 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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.resourcealert.api.command.user.CreateResourceAlertRuleCmd; +import org.apache.cloudstack.resourcealert.api.command.user.DeleteResourceAlertRuleCmd; +import org.apache.cloudstack.resourcealert.api.command.user.ListResourceAlertRulesCmd; +import org.apache.cloudstack.resourcealert.api.command.user.ListResourceAlertsCmd; +import org.apache.cloudstack.resourcealert.api.command.user.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.dao.ResourceAlertRuleWebhookDao; +import org.apache.cloudstack.resourcealert.vo.ResourceAlertRuleJoinVO; +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.webhook.WebhookHelper; +import org.apache.commons.lang3.EnumUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.apache.commons.lang3.StringUtils; + +import com.cloud.event.ActionEvent; +import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.projects.Project; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; +import com.cloud.utils.component.ComponentContext; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.dao.UserVmDao; + +import org.apache.cloudstack.context.CallContext; + +public class ResourceAlertServiceImpl extends ManagerBase implements ResourceAlertService { + + @Inject + AccountManager accountManager; + @Inject + ResourceAlertRuleDao ruleDao; + @Inject + ResourceAlertRuleJoinDao ruleJoinDao; + @Inject + ResourceAlertDao alertDao; + @Inject + ResourceAlertRuleWebhookDao ruleWebhookDao; + @Inject + UserVmDao userVmDao; + @Inject + VolumeDao volumeDao; + @Inject + HostDao hostDao; + @Inject + PrimaryDataStoreDao storagePoolDao; + + @Override + @ActionEvent(eventType = EventTypes.EVENT_RESOURCE_ALERT_RULE_CREATE, eventDescription = "creating resource alert rule") + 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); + + validateThreshold(metric, cmd.getThreshold()); + validateResetInterval(cmd.getResetInterval()); + int resetInterval = cmd.getResetInterval() != null ? cmd.getResetInterval() : ResourceAlertManagerImpl.DEFAULT_RESET_INTERVAL.value(); + boolean email = cmd.getEmail() != null && cmd.getEmail(); + + Account caller = CallContext.current().getCallingAccount(); + checkInfrastructureAccess(caller, resourceType); + checkEmailAccess(caller, email); + Account owner = accountManager.finalizeOwner(caller, cmd.getAccountName(), cmd.getDomainId(), null); + + int limit = ResourceAlertManagerImpl.RULES_PER_ACCOUNT_LIMIT.valueIn(owner.getId()); + 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(); + + InternalIdentity resource = findResourceOrFail(resourceType, cmd.getResourceId()); + if (resource instanceof ControlledEntity) { + accountManager.checkAccess(owner, null, false, (ControlledEntity) resource); + } + Long resourceId = resource != null ? resource.getId() : null; + + ResourceAlertRuleVO rule = new ResourceAlertRuleVO( + cmd.getName(), resourceType, resourceId, + owner.getId(), domainId, + metric.name(), condition, cmd.getThreshold(), severity, + cmd.getMessage(), email, resetInterval); + + List webhookIds = resolveWebhookIds(owner, cmd.getWebhookIds()); + ruleDao.persist(rule); + CallContext.current().setEventResourceId(rule.getId()); + CallContext.current().setEventDetails("Rule: " + rule.getName()); + if (!webhookIds.isEmpty()) { + ruleWebhookDao.replaceWebhooksForRule(rule.getId(), webhookIds); + } + return toRuleResponse(ruleJoinDao.findById(rule.getId())); + } + + @Override + public ListResponse listResourceAlertRules(ListResourceAlertRulesCmd cmd) { + Long resourceId = resolveResourceIdFilter(cmd.getResourceType(), cmd.getResourceId()); + ResourceAlertRule.ResourceType resourceType = StringUtils.isNotBlank(cmd.getResourceType()) ? + parseResourceType(cmd.getResourceType()) : null; + + Account caller = CallContext.current().getCallingAccount(); + List permittedAccounts = new ArrayList<>(); + Ternary domainIdRecursiveListProject = + new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null); + accountManager.buildACLSearchParameters(caller, cmd.getId(), cmd.getAccountName(), null, + permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); + SearchBuilder sb = createAclSearchBuilder(domainIdRecursiveListProject, permittedAccounts); + sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); + sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ); + sb.and("keyword", sb.entity().getName(), SearchCriteria.Op.LIKE); + sb.and("resourceType", sb.entity().getResourceType(), SearchCriteria.Op.EQ); + sb.and("resourceId", sb.entity().getResourceId(), SearchCriteria.Op.EQ); + SearchCriteria sc = createAclSearchCriteria(sb, domainIdRecursiveListProject, permittedAccounts); + if (cmd.getId() != null) sc.setParameters("id", cmd.getId()); + if (StringUtils.isNotBlank(cmd.getRuleName())) sc.setParameters("name", cmd.getRuleName()); + if (StringUtils.isNotBlank(cmd.getKeyword())) sc.setParameters("keyword", "%" + cmd.getKeyword() + "%"); + if (resourceType != null) sc.setParameters("resourceType", resourceType); + if (resourceId != null) sc.setParameters("resourceId", resourceId); + + Filter filter = new Filter(ResourceAlertRuleJoinVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal()); + Pair, Integer> rules = ruleJoinDao.searchAndCount(sc, filter); + + List responses = rules.first().stream() + .map(this::toRuleResponse) + .collect(Collectors.toList()); + + ListResponse response = new ListResponse<>(); + response.setResponses(responses, rules.second()); + return response; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_RESOURCE_ALERT_RULE_UPDATE, eventDescription = "updating resource alert rule") + public ResourceAlertRuleResponse updateResourceAlertRule(UpdateResourceAlertRuleCmd cmd) { + ResourceAlertRuleVO rule = findRuleForCaller(cmd.getId()); + checkEmailAccess(CallContext.current().getCallingAccount(), Boolean.TRUE.equals(cmd.getEmail())); + + if (StringUtils.isNotBlank(cmd.getName())) rule.setName(cmd.getName()); + if (StringUtils.isNotBlank(cmd.getCondition())) rule.setCondition(parseCondition(cmd.getCondition())); + if (cmd.getThreshold() != null) { + validateThreshold(ResourceAlertMetric.valueOf(rule.getMetric()), cmd.getThreshold()); + 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) { + validateResetInterval(cmd.getResetInterval()); + rule.setResetInterval(cmd.getResetInterval()); + } + rule.setUpdated(new Date()); + + if (cmd.isCleanupWebhooks()) { + ruleWebhookDao.replaceWebhooksForRule(rule.getId(), new ArrayList<>()); + } else if (cmd.getWebhookIds() != null) { + Account owner = accountManager.getAccount(rule.getAccountId()); + ruleWebhookDao.replaceWebhooksForRule(rule.getId(), resolveWebhookIds(owner, cmd.getWebhookIds())); + } + ruleDao.update(rule.getId(), rule); + return toRuleResponse(ruleJoinDao.findById(rule.getId())); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_RESOURCE_ALERT_RULE_DELETE, eventDescription = "deleting resource alert rule") + public boolean deleteResourceAlertRule(DeleteResourceAlertRuleCmd cmd) { + findRuleForCaller(cmd.getId()); + return ruleDao.remove(cmd.getId()); + } + + @Override + public ListResponse listResourceAlerts(ListResourceAlertsCmd cmd) { + Long resourceId = resolveResourceIdFilter(cmd.getResourceType(), cmd.getResourceId()); + List alertRuleIds = null; + if (cmd.getAlertRuleId() != null) { + ResourceAlertRuleVO rule = ruleDao.findByUuid(cmd.getAlertRuleId()); + if (rule == null) { + throw new InvalidParameterValueException("Alert rule not found: " + cmd.getAlertRuleId()); + } + accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, rule); + alertRuleIds = new ArrayList<>(List.of(rule.getId())); + } + List visibleRuleIds = listVisibleRuleIds(cmd); + if (visibleRuleIds != null) { + if (alertRuleIds == null) { + alertRuleIds = visibleRuleIds; + } else { + alertRuleIds.retainAll(visibleRuleIds); + } + } + if (StringUtils.isNotBlank(cmd.getResourceType())) { + List typeRuleIds = ruleDao.listIdsByResourceType(parseResourceType(cmd.getResourceType())); + if (alertRuleIds == null) { + alertRuleIds = new ArrayList<>(typeRuleIds); + } else { + alertRuleIds.retainAll(typeRuleIds); + } + } + if (alertRuleIds != null && alertRuleIds.isEmpty()) { + ListResponse empty = new ListResponse<>(); + empty.setResponses(new ArrayList<>(), 0); + return empty; + } + Pair, Integer> alerts = alertDao.searchAndCountByFilters( + alertRuleIds, resourceId, cmd.getSeverity(), cmd.getStartDate(), cmd.getEndDate(), + cmd.getStartIndex(), cmd.getPageSizeVal()); + + Map rules = new HashMap<>(); + List responses = alerts.first().stream() + .map(alert -> toAlertResponse(alert, rules.computeIfAbsent(alert.getAlertRuleId(), ruleDao::findByIdIncludingRemoved))) + .collect(Collectors.toList()); + + ListResponse response = new ListResponse<>(); + response.setResponses(responses, alerts.second()); + 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); + Pair resource = describeResource(vo.getResourceType(), vo.getResourceId()); + r.setResourceId(resource != null ? resource.first() : null); + r.setResourceName(resource != null ? resource.second() : 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.setWebhookIds(getWebhookUuids(vo.getId())); + r.setAccountName(vo.getAccountName()); + r.setDomainId(vo.getDomainUuid()); + r.setDomainName(vo.getDomainName()); + r.setCreated(vo.getCreated()); + return r; + } + + private ResourceAlertResponse toAlertResponse(ResourceAlertVO vo, ResourceAlertRuleVO rule) { + ResourceAlertResponse r = new ResourceAlertResponse(); + r.setObjectName("resourcealert"); + r.setId(vo.getUuid()); + r.setAlertRuleId(rule != null ? rule.getUuid() : null); + Pair resource = rule != null ? describeResource(rule.getResourceType(), vo.getResourceId()) : null; + r.setResourceId(resource != null ? resource.first() : null); + r.setResourceName(resource != null ? resource.second() : null); + r.setResourceType(rule != null ? rule.getResourceType().name() : null); + r.setAlertRuleName(rule != null ? rule.getName() : 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 InternalIdentity findResource(ResourceAlertRule.ResourceType type, String uuid) { + switch (type) { + case VirtualMachine: + return userVmDao.findByUuid(uuid); + case Volume: + return volumeDao.findByUuid(uuid); + case Host: + return hostDao.findByUuid(uuid); + case StoragePool: + return storagePoolDao.findByUuid(uuid); + default: + return null; + } + } + + private InternalIdentity findResourceOrFail(ResourceAlertRule.ResourceType type, String uuid) { + if (StringUtils.isBlank(uuid)) { + return null; + } + InternalIdentity resource = findResource(type, uuid); + if (resource == null) { + throw new InvalidParameterValueException("Unable to find " + type.name() + " with ID " + uuid); + } + return resource; + } + + private Long resolveResourceIdFilter(String resourceType, String uuid) { + if (StringUtils.isBlank(uuid)) { + return null; + } + if (StringUtils.isBlank(resourceType)) { + throw new InvalidParameterValueException("resourcetype is required when resourceid is specified"); + } + return findResourceOrFail(parseResourceType(resourceType), uuid).getId(); + } + + // Returns the resource's uuid and display name, or null when the type or id is not set. + private Pair describeResource(ResourceAlertRule.ResourceType type, Long id) { + if (type == null || id == null) { + return null; + } + switch (type) { + case VirtualMachine: { + UserVmVO vm = userVmDao.findByIdIncludingRemoved(id); + return vm == null ? null : new Pair<>(vm.getUuid(), + StringUtils.isNotBlank(vm.getDisplayName()) ? vm.getDisplayName() : vm.getHostName()); + } + case Volume: { + VolumeVO volume = volumeDao.findByIdIncludingRemoved(id); + return volume == null ? null : new Pair<>(volume.getUuid(), volume.getName()); + } + case Host: { + HostVO host = hostDao.findByIdIncludingRemoved(id); + return host == null ? null : new Pair<>(host.getUuid(), host.getName()); + } + case StoragePool: { + StoragePoolVO pool = storagePoolDao.findByIdIncludingRemoved(id); + return pool == null ? null : new Pair<>(pool.getUuid(), pool.getName()); + } + default: + return null; + } + } + + protected WebhookHelper getWebhookHelper() { + try { + return ComponentContext.getDelegateComponentOfType(WebhookHelper.class); + } catch (NoSuchBeanDefinitionException e) { + return null; + } + } + + private List resolveWebhookIds(Account owner, List webhookUuids) { + List ids = new ArrayList<>(); + if (webhookUuids == null || webhookUuids.isEmpty()) { + return ids; + } + WebhookHelper webhookHelper = getWebhookHelper(); + if (webhookHelper == null) { + throw new InvalidParameterValueException("Webhooks are not available, the webhook plugin is not enabled"); + } + for (String uuid : webhookUuids) { + ControlledEntity webhook = webhookHelper.findWebhookByUuid(uuid); + if (!(webhook instanceof InternalIdentity)) { + throw new InvalidParameterValueException("Unable to find webhook with ID " + uuid); + } + accountManager.checkAccess(owner, null, false, webhook); + long id = ((InternalIdentity) webhook).getId(); + if (!ids.contains(id)) { + ids.add(id); + } + } + return ids; + } + + private List getWebhookUuids(long ruleId) { + List ids = ruleWebhookDao.listWebhookIdsByRule(ruleId); + WebhookHelper webhookHelper = ids.isEmpty() ? null : getWebhookHelper(); + if (webhookHelper == null) { + return new ArrayList<>(); + } + return ids.stream().map(webhookHelper::getWebhookUuid).filter(Objects::nonNull).collect(Collectors.toList()); + } + + private void validateThreshold(ResourceAlertMetric metric, Double threshold) { + if (threshold == null || threshold < 0) { + throw new InvalidParameterValueException("threshold must be zero or more"); + } + if (metric.isPercentage() && threshold > 100) { + throw new InvalidParameterValueException("threshold for " + metric.name() + " is a percentage and must be 100 or less"); + } + } + + private void validateResetInterval(Integer resetInterval) { + if (resetInterval != null && resetInterval < 0) { + throw new InvalidParameterValueException("resetinterval must be zero or more"); + } + } + + private void checkInfrastructureAccess(Account caller, ResourceAlertRule.ResourceType resourceType) { + boolean infra = resourceType == ResourceAlertRule.ResourceType.Host + || resourceType == ResourceAlertRule.ResourceType.StoragePool; + if (infra && !accountManager.isRootAdmin(caller.getId())) { + throw new PermissionDeniedException("Only root admins can create alert rules for " + resourceType.name()); + } + } + + private void checkEmailAccess(Account caller, boolean email) { + if (email && !accountManager.isRootAdmin(caller.getId())) { + throw new PermissionDeniedException("Only root admins can enable email for alert rules"); + } + } + + private ResourceAlertRuleVO findRuleForCaller(long id) { + ResourceAlertRuleVO rule = ruleDao.findById(id); + if (rule == null || rule.getRemoved() != null) { + throw new InvalidParameterValueException("Alert rule not found"); + } + accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, rule); + CallContext.current().setEventResourceId(rule.getId()); + CallContext.current().setEventDetails("Rule: " + rule.getName()); + return rule; + } + + private SearchBuilder createAclSearchBuilder( + Ternary domainIdRecursiveListProject, List permittedAccounts) { + SearchBuilder sb = ruleJoinDao.createSearchBuilder(); + accountManager.buildACLSearchBuilder(sb, domainIdRecursiveListProject.first(), domainIdRecursiveListProject.second(), + permittedAccounts, domainIdRecursiveListProject.third()); + return sb; + } + + private SearchCriteria createAclSearchCriteria(SearchBuilder sb, + Ternary domainIdRecursiveListProject, List permittedAccounts) { + SearchCriteria sc = sb.create(); + accountManager.buildACLSearchCriteria(sc, domainIdRecursiveListProject.first(), domainIdRecursiveListProject.second(), + permittedAccounts, domainIdRecursiveListProject.third()); + return sc; + } + + // Returns null when the caller can see alerts of every rule. + private List listVisibleRuleIds(ListResourceAlertsCmd cmd) { + Account caller = CallContext.current().getCallingAccount(); + List permittedAccounts = new ArrayList<>(); + Ternary domainIdRecursiveListProject = + new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null); + accountManager.buildACLSearchParameters(caller, null, cmd.getAccountName(), null, + permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); + if (permittedAccounts.isEmpty() && domainIdRecursiveListProject.first() == null) { + return null; + } + SearchBuilder sb = createAclSearchBuilder(domainIdRecursiveListProject, permittedAccounts); + SearchCriteria sc = createAclSearchCriteria(sb, domainIdRecursiveListProject, permittedAccounts); + return ruleJoinDao.searchIncludingRemoved(sc, null, null, false).stream() + .map(ResourceAlertRuleJoinVO::getId) + .collect(Collectors.toList()); + } + + 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/user/CreateResourceAlertRuleCmd.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/user/CreateResourceAlertRuleCmd.java new file mode 100644 index 000000000000..91cb39c7cd33 --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/user/CreateResourceAlertRuleCmd.java @@ -0,0 +1,134 @@ +// 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.user; + +import java.util.List; + +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, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "24.0.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 = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, required = true, + description = "type of resource to monitor: VirtualMachine, Volume, Host, StoragePool") + private String resourceType; + + @Parameter(name = ApiConstants.RESOURCE_ID, type = CommandType.STRING, + description = "UUID of the specific resource to monitor; omit for a generic rule covering all resources of this type") + private String resourceId; + + @Parameter(name = ApiConstants.METRIC, type = CommandType.STRING, required = true, + description = "metric to monitor (e.g. CPU_UTILIZATION, MEMORY_UTILIZATION)") + private String metric; + + @Parameter(name = ApiConstants.CONDITION, type = CommandType.STRING, required = true, + description = "comparison operator: GT, GTE, LT, LTE, EQ") + private String condition; + + @Parameter(name = ApiConstants.THRESHOLD, type = CommandType.DOUBLE, required = true, + description = "threshold value that triggers the alert") + private Double threshold; + + @Parameter(name = ApiConstants.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 = ApiConstants.EMAIL, type = CommandType.BOOLEAN, + description = "true to send email notification when this rule fires (admin SMTP must be configured)") + private Boolean email; + + @Parameter(name = ApiConstants.RESET_INTERVAL, type = CommandType.INTEGER, + description = "minimum seconds between repeat firings of this rule; defaults to resourcealert.repeat.interval.default") + private Integer resetInterval; + + @Parameter(name = ApiConstants.WEBHOOK_IDS, type = CommandType.LIST, collectionType = CommandType.STRING, + description = "UUIDs of webhooks to deliver alerts of this rule to; the rule owner must have access to them") + private List webhookIds; + + @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 String 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 List getWebhookIds() { return webhookIds; } + 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/user/DeleteResourceAlertRuleCmd.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/user/DeleteResourceAlertRuleCmd.java new file mode 100644 index 000000000000..e638b2d96dc0 --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/user/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.user; + +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, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "24.0.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/user/ListResourceAlertRulesCmd.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/user/ListResourceAlertRulesCmd.java new file mode 100644 index 000000000000..fcabe4537fef --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/user/ListResourceAlertRulesCmd.java @@ -0,0 +1,72 @@ +// 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.user; + +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.BaseListAccountResourcesCmd; +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, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "24.0.0") +public class ListResourceAlertRulesCmd extends BaseListAccountResourcesCmd { + + @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 = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, + description = "filter by resource type: VirtualMachine, Volume, Host, StoragePool") + private String resourceType; + + @Parameter(name = ApiConstants.RESOURCE_ID, type = CommandType.STRING, + description = "filter by UUID of a specific resource; requires resourcetype") + private String resourceId; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, + description = "filter by rule name") + private String name; + + public Long getId() { return id; } + public String getResourceType() { return resourceType; } + public String getResourceId() { return resourceId; } + public String getRuleName() { return name; } + + @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/user/ListResourceAlertsCmd.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/user/ListResourceAlertsCmd.java new file mode 100644 index 000000000000..83e90c88cad5 --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/user/ListResourceAlertsCmd.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.user; + +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.BaseListAccountResourcesCmd; +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, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "24.0.0") +public class ListResourceAlertsCmd extends BaseListAccountResourcesCmd { + + @Inject + ResourceAlertService resourceAlertService; + + @Parameter(name = ApiConstants.ALERT_RULE_ID, type = CommandType.STRING, + description = "UUID of the alert rule to filter by") + private String alertRuleId; + + @Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, + description = "filter by resource type: VirtualMachine, Volume, Host, StoragePool") + private String resourceType; + + @Parameter(name = ApiConstants.RESOURCE_ID, type = CommandType.STRING, + description = "filter by UUID of the resource that triggered the alert; requires resourcetype") + private String resourceId; + + @Parameter(name = ApiConstants.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 String getResourceType() { return resourceType; } + public String 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/user/UpdateResourceAlertRuleCmd.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/user/UpdateResourceAlertRuleCmd.java new file mode 100644 index 000000000000..3953f7a08eb8 --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/command/user/UpdateResourceAlertRuleCmd.java @@ -0,0 +1,120 @@ +// 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.user; + +import java.util.List; + +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, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "24.0.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 = ApiConstants.CONDITION, type = CommandType.STRING, + description = "new comparison operator: GT, GTE, LT, LTE, EQ") + private String condition; + + @Parameter(name = ApiConstants.THRESHOLD, type = CommandType.DOUBLE, + description = "new threshold value") + private Double threshold; + + @Parameter(name = ApiConstants.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 = ApiConstants.EMAIL, type = CommandType.BOOLEAN, + description = "enable or disable email notification") + private Boolean email; + + @Parameter(name = ApiConstants.RESET_INTERVAL, type = CommandType.INTEGER, + description = "new minimum seconds between repeat firings") + private Integer resetInterval; + + @Parameter(name = ApiConstants.WEBHOOK_IDS, type = CommandType.LIST, collectionType = CommandType.STRING, + description = "UUIDs of webhooks to deliver alerts of this rule to; replaces the current list") + private List webhookIds; + + @Parameter(name = ApiConstants.CLEANUP_WEBHOOKS, type = CommandType.BOOLEAN, + description = "true to stop delivering alerts of this rule to any webhook") + private Boolean cleanupWebhooks; + + 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; } + public List getWebhookIds() { return webhookIds; } + public boolean isCleanupWebhooks() { return Boolean.TRUE.equals(cleanupWebhooks); } + + @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..9db91ac9b641 --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/response/ResourceAlertResponse.java @@ -0,0 +1,88 @@ +// 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(ApiConstants.ALERT_RULE_ID) + @Param(description = "the ID of the rule that triggered this alert") + private String alertRuleId; + + @SerializedName(ApiConstants.RESOURCE_ID) + @Param(description = "the ID of the resource that triggered this alert") + private String resourceId; + + @SerializedName(ApiConstants.RESOURCE_NAME) + @Param(description = "name of the resource that triggered the alert") + private String resourceName; + + @SerializedName(ApiConstants.RESOURCE_TYPE) + @Param(description = "type of the resource that triggered the alert") + private String resourceType; + + @SerializedName("alertrulename") + @Param(description = "name of the alert rule") + private String alertRuleName; + + @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(ApiConstants.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 setResourceName(String resourceName) { this.resourceName = resourceName; } + public void setResourceType(String resourceType) { this.resourceType = resourceType; } + public void setAlertRuleName(String alertRuleName) { this.alertRuleName = alertRuleName; } + 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..98ed1a2ffbc5 --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/api/response/ResourceAlertRuleResponse.java @@ -0,0 +1,119 @@ +// 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 java.util.List; + +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(ApiConstants.RESOURCE_TYPE) + @Param(description = "the type of resource this rule monitors") + private String resourceType; + + @SerializedName(ApiConstants.RESOURCE_ID) + @Param(description = "the specific resource ID; absent for generic rules") + private String resourceId; + + @SerializedName(ApiConstants.RESOURCE_NAME) + @Param(description = "name of the resource the rule watches") + private String resourceName; + + @SerializedName(ApiConstants.METRIC) + @Param(description = "the metric being monitored") + private String metric; + + @SerializedName(ApiConstants.CONDITION) + @Param(description = "the comparison operator (GT, GTE, LT, LTE, EQ)") + private String condition; + + @SerializedName(ApiConstants.THRESHOLD) + @Param(description = "the threshold value that triggers this rule") + private double threshold; + + @SerializedName(ApiConstants.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(ApiConstants.EMAIL) + @Param(description = "whether email notification is enabled for this rule") + private boolean email; + + @SerializedName(ApiConstants.RESET_INTERVAL) + @Param(description = "minimum seconds between repeat firings of this rule") + private int resetInterval; + + @SerializedName(ApiConstants.WEBHOOK_IDS) + @Param(description = "UUIDs of webhooks the rule delivers alerts to") + private List webhookIds; + + @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 setResourceName(String resourceName) { this.resourceName = resourceName; } + 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 setWebhookIds(List webhookIds) { this.webhookIds = webhookIds; } + 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..c6e18eef0819 --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertDao.java @@ -0,0 +1,39 @@ +// 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.Pair; +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); + + Pair, Integer> searchAndCountByFilters(List alertRuleIds, Long resourceId, String severity, + Date startDate, Date endDate, Long startIndex, Long pageSize); + + int removeOlderThan(Date date); +} 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..0ecb2185f008 --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertDaoImpl.java @@ -0,0 +1,106 @@ +// 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.Pair; +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; + private final SearchBuilder olderThanSearch; + + public ResourceAlertDaoImpl() { + alertRuleIdSearch = createSearchBuilder(); + alertRuleIdSearch.and("alertRuleId", alertRuleIdSearch.entity().getAlertRuleId(), SearchCriteria.Op.EQ); + alertRuleIdSearch.done(); + + olderThanSearch = createSearchBuilder(); + olderThanSearch.and("alertTimestamp", olderThanSearch.entity().getAlertTimestamp(), SearchCriteria.Op.LT); + olderThanSearch.done(); + } + + @Override + public int removeOlderThan(Date date) { + SearchCriteria sc = olderThanSearch.create(); + sc.setParameters("alertTimestamp", date); + return expunge(sc); + } + + @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 Pair, Integer> searchAndCountByFilters(List alertRuleIds, Long resourceId, String severity, + Date startDate, Date endDate, Long startIndex, Long pageSize) { + SearchBuilder sb = createSearchBuilder(); + if (alertRuleIds != null) { + sb.and("alertRuleIds", sb.entity().getAlertRuleId(), SearchCriteria.Op.IN); + } + 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 (alertRuleIds != null) sc.setParameters("alertRuleIds", alertRuleIds.toArray()); + 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); + Filter filter = new Filter(ResourceAlertVO.class, "alertTimestamp", false, startIndex, pageSize); + return searchAndCount(sc, filter); + } +} 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..e9e4135306e5 --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleDao.java @@ -0,0 +1,42 @@ +// 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); + + List listIdsByResourceType(ResourceAlertRule.ResourceType resourceType); + + 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..3b67449f0ed1 --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleDaoImpl.java @@ -0,0 +1,126 @@ +// 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 java.util.stream.Collectors; + +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; + private final SearchBuilder resourceTypeSearch; + + 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(); + + resourceTypeSearch = createSearchBuilder(); + resourceTypeSearch.and("resourceType", resourceTypeSearch.entity().getResourceType(), SearchCriteria.Op.EQ); + resourceTypeSearch.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; + } + + @Override + public List listIdsByResourceType(ResourceAlertRule.ResourceType resourceType) { + SearchCriteria sc = resourceTypeSearch.create(); + sc.setParameters("resourceType", resourceType); + return listIncludingRemovedBy(sc).stream().map(ResourceAlertRuleVO::getId).collect(Collectors.toList()); + } +} 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..acdeda34e06b --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleJoinDao.java @@ -0,0 +1,27 @@ +// 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 org.apache.cloudstack.resourcealert.vo.ResourceAlertRuleJoinVO; + +import com.cloud.utils.db.GenericDao; + +public interface ResourceAlertRuleJoinDao extends GenericDao { + + ResourceAlertRuleJoinVO findByUuid(String uuid); +} 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..40895284ae9f --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleJoinDaoImpl.java @@ -0,0 +1,36 @@ +// 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 org.apache.cloudstack.resourcealert.vo.ResourceAlertRuleJoinVO; + +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); + } +} diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleWebhookDao.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleWebhookDao.java new file mode 100644 index 000000000000..72388bd28c60 --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleWebhookDao.java @@ -0,0 +1,31 @@ +// 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.ResourceAlertRuleWebhookVO; + +import com.cloud.utils.db.GenericDao; + +public interface ResourceAlertRuleWebhookDao extends GenericDao { + + List listWebhookIdsByRule(long ruleId); + + void replaceWebhooksForRule(long ruleId, List webhookIds); +} diff --git a/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleWebhookDaoImpl.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleWebhookDaoImpl.java new file mode 100644 index 000000000000..4ba0888a47a8 --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/dao/ResourceAlertRuleWebhookDaoImpl.java @@ -0,0 +1,64 @@ +// 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 java.util.stream.Collectors; + +import org.apache.cloudstack.resourcealert.vo.ResourceAlertRuleWebhookVO; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallbackNoReturn; +import com.cloud.utils.db.TransactionStatus; + +public class ResourceAlertRuleWebhookDaoImpl extends GenericDaoBase + implements ResourceAlertRuleWebhookDao { + + private final SearchBuilder ruleSearch; + + public ResourceAlertRuleWebhookDaoImpl() { + ruleSearch = createSearchBuilder(); + ruleSearch.and("ruleId", ruleSearch.entity().getRuleId(), SearchCriteria.Op.EQ); + ruleSearch.done(); + } + + @Override + public List listWebhookIdsByRule(long ruleId) { + SearchCriteria sc = ruleSearch.create(); + sc.setParameters("ruleId", ruleId); + return listBy(sc).stream().map(ResourceAlertRuleWebhookVO::getWebhookId).collect(Collectors.toList()); + } + + @Override + public void replaceWebhooksForRule(long ruleId, List webhookIds) { + Transaction.execute(new TransactionCallbackNoReturn() { + @Override + public void doInTransactionWithoutResult(TransactionStatus status) { + SearchCriteria sc = ruleSearch.create(); + sc.setParameters("ruleId", ruleId); + expunge(sc); + for (Long webhookId : webhookIds) { + persist(new ResourceAlertRuleWebhookVO(ruleId, webhookId)); + } + } + }); + } +} 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..dbf7551db5a9 --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/vo/ResourceAlertRuleJoinVO.java @@ -0,0 +1,147 @@ +// 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.acl.ControlledEntity; +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 implements ControlledEntity { + + @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; } + + @Override + public Class getEntityType() { + return ResourceAlertRule.class; + } +} 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/ResourceAlertRuleWebhookVO.java b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/vo/ResourceAlertRuleWebhookVO.java new file mode 100644 index 000000000000..4f37f4f8e4a1 --- /dev/null +++ b/plugins/resource-alerts/src/main/java/org/apache/cloudstack/resourcealert/vo/ResourceAlertRuleWebhookVO.java @@ -0,0 +1,55 @@ +// 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 javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.apache.cloudstack.api.InternalIdentity; + +@Entity +@Table(name = "resource_alert_rules_webhook") +public class ResourceAlertRuleWebhookVO implements InternalIdentity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "resource_alert_rule_id") + private long ruleId; + + @Column(name = "webhook_id") + private long webhookId; + + public ResourceAlertRuleWebhookVO() {} + + public ResourceAlertRuleWebhookVO(long ruleId, long webhookId) { + this.ruleId = ruleId; + this.webhookId = webhookId; + } + + @Override + public long getId() { return id; } + public long getRuleId() { return ruleId; } + public long getWebhookId() { return webhookId; } +} 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..237ba7038edb --- /dev/null +++ b/plugins/resource-alerts/src/main/resources/META-INF/cloudstack/resource-alerts/spring-resource-alerts-context.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + 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..f9141465901a --- /dev/null +++ b/plugins/resource-alerts/src/test/java/org/apache/cloudstack/resourcealert/ResourceAlertManagerImplTest.java @@ -0,0 +1,883 @@ +// 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.assertFalse; +import static org.junit.Assert.assertNull; +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.doReturn; +import static org.mockito.Mockito.lenient; +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.dao.ResourceAlertRuleWebhookDao; +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.identity.ManagementServerNode; +import org.apache.cloudstack.utils.mailing.SMTPMailProperties; +import org.apache.cloudstack.utils.mailing.SMTPMailSender; +import org.apache.cloudstack.webhook.WebhookHelper; +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.cluster.ManagementServerHostVO; +import com.cloud.cluster.dao.ManagementServerHostDao; +import com.cloud.domain.dao.DomainDao; +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.Storage; +import com.cloud.storage.StorageStats; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeStats; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.tags.dao.ResourceTagDao; +import com.cloud.user.Account; +import com.cloud.user.AccountVO; +import com.cloud.user.dao.AccountDao; +import com.cloud.utils.Pair; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VmStats; +import com.cloud.vm.dao.UserVmDao; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +@RunWith(MockitoJUnitRunner.class) +public class ResourceAlertManagerImplTest { + + @Spy @InjectMocks + ResourceAlertManagerImpl manager; + + @Mock ResourceAlertRuleDao ruleDao; + @Mock ResourceAlertDao alertDao; + @Mock ResourceAlertRuleWebhookDao ruleWebhookDao; + @Mock WebhookHelper webhookHelper; + @Mock UserVmDao userVmDao; + @Mock HostDao hostDao; + @Mock PrimaryDataStoreDao storagePoolDao; + @Mock VolumeDao volumeDao; + @Mock StatsCollector statsCollector; + @Mock ConfigurationDao configDao; + @Mock ResourceTagDao resourceTagDao; + @Mock AccountDao accountDao; + @Mock DomainDao domainDao; + @Mock ManagementServerHostDao managementServerHostDao; + @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()); + // owners and resources exist unless a test says otherwise + AccountVO defaultOwner = mock(AccountVO.class); + lenient().when(defaultOwner.getId()).thenReturn(1L); + lenient().when(defaultOwner.getType()).thenReturn(Account.Type.NORMAL); + lenient().when(accountDao.findById(anyLong())).thenReturn(defaultOwner); + lenient().when(userVmDao.findById(anyLong())).thenReturn(mock(UserVmVO.class)); + lenient().when(volumeDao.findById(anyLong())).thenReturn(mock(VolumeVO.class)); + lenient().when(hostDao.findById(anyLong())).thenReturn(mock(HostVO.class)); + lenient().when(storagePoolDao.findById(anyLong())).thenReturn(mock(StoragePoolVO.class)); + } + + 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)); + + stubOwner(Account.Type.NORMAL); + when(userVmDao.listIdsByAccountOrDomainsAndState(1L, null, VirtualMachine.State.Running)) + .thenReturn(Arrays.asList(101L, 102L)); + + 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 testGenericVmRuleOnlyListsRunningVms() { + ResourceAlertRuleVO rule = vmCpuRule(null); + when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule)); + stubOwner(Account.Type.NORMAL); + + manager.evaluateRules(); + + verify(userVmDao).listIdsByAccountOrDomainsAndState(1L, null, VirtualMachine.State.Running); + 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)); + + stubOwner(Account.Type.NORMAL); + when(userVmDao.listIdsByAccountOrDomainsAndState(1L, null, VirtualMachine.State.Running)) + .thenReturn(Collections.singletonList(VM_ID)); + + 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)); + + stubOwner(Account.Type.NORMAL); + when(userVmDao.listIdsByAccountOrDomainsAndState(1L, null, VirtualMachine.State.Running)) + .thenReturn(Collections.singletonList(VM_ID)); + + 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)); + + stubOwner(Account.Type.NORMAL); + when(userVmDao.listIdsByAccountOrDomainsAndState(1L, null, VirtualMachine.State.Running)) + .thenReturn(Collections.singletonList(VM_ID)); + + 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(null); + when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule)); + when(userVmDao.listIdsByAccountOrDomainsAndState(1L, null, VirtualMachine.State.Running)) + .thenReturn(Collections.singletonList(VM_ID)); + + 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()); + } + + private void stubFiringVmCpuRule(ResourceAlertRuleVO rule) { + 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); + } + + @Test + public void testFiredAlertIsDeliveredToMappedWebhooks() { + ResourceAlertRuleVO rule = vmCpuRule(VM_ID); + stubFiringVmCpuRule(rule); + when(ruleWebhookDao.listWebhookIdsByRule(rule.getId())).thenReturn(Arrays.asList(11L, 12L)); + doReturn(webhookHelper).when(manager).getWebhookHelper(); + UserVmVO vm = mock(UserVmVO.class); + when(vm.getUuid()).thenReturn("vm-uuid"); + when(userVmDao.findByIdIncludingRemoved(VM_ID)).thenReturn(vm); + + manager.evaluateRules(); + + ArgumentCaptor payloadCaptor = ArgumentCaptor.forClass(String.class); + verify(webhookHelper).deliverToWebhooks(eq(Arrays.asList(11L, 12L)), eq(1L), + eq(ResourceAlertManagerImpl.ALERT_EVENT_TYPE), payloadCaptor.capture()); + JsonObject payload = JsonParser.parseString(payloadCaptor.getValue()).getAsJsonObject(); + assertEquals(rule.getUuid(), payload.get("ruleid").getAsString()); + assertEquals("VirtualMachine", payload.get("resourcetype").getAsString()); + assertEquals("vm-uuid", payload.get("resourceid").getAsString()); + assertEquals("CPU_UTILIZATION", payload.get("metric").getAsString()); + assertEquals(85.0, payload.get("value").getAsDouble(), 0.001); + assertEquals("HIGH", payload.get("severity").getAsString()); + } + + @Test + public void testFiredAlertWithoutMappedWebhooksSkipsDelivery() { + stubFiringVmCpuRule(vmCpuRule(VM_ID)); + + manager.evaluateRules(); + + verify(alertDao).persist(any()); + verify(manager, never()).getWebhookHelper(); + } + + private ResourceAlertRuleVO hostRule(String metric, double threshold) { + return new ResourceAlertRuleVO("host", ResourceAlertRule.ResourceType.Host, + HOST_ID, 1L, 1L, metric, AlertCondition.GT, threshold, + AlertSeverity.HIGH, null, false, 600); + } + + @Test + public void testHostLoadAverageRuleFires() { + when(ruleDao.listActive()).thenReturn(Collections.singletonList(hostRule("LOAD_AVERAGE", 4.0))); + HostStats stats = mock(HostStats.class); + when(stats.getLoadAverage()).thenReturn(6.5); + when(statsCollector.getHostStats(HOST_ID)).thenReturn(stats); + + manager.evaluateRules(); + + verify(alertDao).persist(alertCaptor.capture()); + assertEquals(6.5, alertCaptor.getValue().getMetricValue(), 0.001); + } + + @Test + public void testHostNetworkReadRuleUsesHostStats() { + when(ruleDao.listActive()).thenReturn(Collections.singletonList(hostRule("NETWORK_READ_KBPS", 1000.0))); + HostStats stats = mock(HostStats.class); + when(stats.getNetworkReadKBs()).thenReturn(2500.0); + when(statsCollector.getHostStats(HOST_ID)).thenReturn(stats); + + manager.evaluateRules(); + + verify(alertDao).persist(alertCaptor.capture()); + assertEquals(2500.0, alertCaptor.getValue().getMetricValue(), 0.001); + verify(statsCollector, never()).getVmStats(anyLong(), any(Boolean.class)); + } + + @Test + public void testHostNetworkWriteRuleDoesNotFireBelowThreshold() { + when(ruleDao.listActive()).thenReturn(Collections.singletonList(hostRule("NETWORK_WRITE_KBPS", 1000.0))); + HostStats stats = mock(HostStats.class); + when(stats.getNetworkWriteKBs()).thenReturn(10.0); + when(statsCollector.getHostStats(HOST_ID)).thenReturn(stats); + + manager.evaluateRules(); + + verify(alertDao, never()).persist(any()); + } + + private static final long VOLUME_ID = 401L; + + private ResourceAlertRuleVO volumeSizeRule(double thresholdGb) { + return new ResourceAlertRuleVO("vol", ResourceAlertRule.ResourceType.Volume, + VOLUME_ID, 1L, 1L, "VOLUME_SIZE_GB", AlertCondition.GT, thresholdGb, + AlertSeverity.MEDIUM, null, false, 600); + } + + @Test + public void testVolumeSizeRuleUsesPhysicalSizeByPath() { + when(ruleDao.listActive()).thenReturn(Collections.singletonList(volumeSizeRule(10.0))); + VolumeVO vol = mock(VolumeVO.class); + when(vol.getFormat()).thenReturn(Storage.ImageFormat.QCOW2); + when(vol.getPath()).thenReturn("vol-path"); + when(volumeDao.findById(VOLUME_ID)).thenReturn(vol); + VolumeStats stats = mock(VolumeStats.class); + when(stats.getPhysicalSize()).thenReturn(20L * 1024 * 1024 * 1024); + when(statsCollector.getVolumeStats("vol-path")).thenReturn(stats); + + manager.evaluateRules(); + + verify(alertDao).persist(alertCaptor.capture()); + assertEquals(20.0, alertCaptor.getValue().getMetricValue(), 0.001); + } + + @Test + public void testVolumeSizeRuleUsesChainInfoForOva() { + when(ruleDao.listActive()).thenReturn(Collections.singletonList(volumeSizeRule(10.0))); + VolumeVO vol = mock(VolumeVO.class); + when(vol.getFormat()).thenReturn(Storage.ImageFormat.OVA); + when(vol.getChainInfo()).thenReturn("chain-info"); + when(volumeDao.findById(VOLUME_ID)).thenReturn(vol); + + manager.evaluateRules(); + + verify(statsCollector).getVolumeStats("chain-info"); + verify(alertDao, never()).persist(any()); + } + + private ResourceAlertRuleVO poolIopsRule(double threshold) { + return new ResourceAlertRuleVO("pool-iops", ResourceAlertRule.ResourceType.StoragePool, + POOL_ID, 1L, 1L, "STORAGE_USED_IOPS", AlertCondition.GT, threshold, + AlertSeverity.HIGH, null, false, 600); + } + + @Test + public void testStoragePoolIopsRuleFires() { + when(ruleDao.listActive()).thenReturn(Collections.singletonList(poolIopsRule(1000.0))); + StorageStats stats = mock(StorageStats.class); + when(stats.getUsedIops()).thenReturn(5000L); + when(statsCollector.getStoragePoolStats(POOL_ID)).thenReturn(stats); + + manager.evaluateRules(); + + verify(alertDao).persist(alertCaptor.capture()); + assertEquals(5000.0, alertCaptor.getValue().getMetricValue(), 0.001); + } + + @Test + public void testStoragePoolIopsRuleSkippedWhenDriverDoesNotReportIops() { + when(ruleDao.listActive()).thenReturn(Collections.singletonList(poolIopsRule(1000.0))); + StorageStats stats = mock(StorageStats.class); + when(stats.getUsedIops()).thenReturn(null); + when(statsCollector.getStoragePoolStats(POOL_ID)).thenReturn(stats); + + manager.evaluateRules(); + + verify(alertDao, never()).persist(any()); + } + + private AccountVO stubOwner(Account.Type type) { + AccountVO owner = mock(AccountVO.class); + when(owner.getType()).thenReturn(type); + lenient().when(owner.getId()).thenReturn(1L); + lenient().when(owner.getDomainId()).thenReturn(5L); + when(accountDao.findById(1L)).thenReturn(owner); + return owner; + } + + @Test + public void testGenericRuleScopeForRootAdminIsWholeCloud() { + stubOwner(Account.Type.ADMIN); + + Pair> scope = manager.getGenericRuleScope(vmCpuRule(null)); + + assertNull(scope.first()); + assertNull(scope.second()); + } + + @Test + public void testGenericRuleScopeForDomainAdminIsDomainTree() { + stubOwner(Account.Type.DOMAIN_ADMIN); + when(domainDao.getDomainAndChildrenIds(5L)).thenReturn(Arrays.asList(5L, 6L)); + + Pair> scope = manager.getGenericRuleScope(vmCpuRule(null)); + + assertNull(scope.first()); + assertEquals(Arrays.asList(5L, 6L), scope.second()); + } + + @Test + public void testGenericRuleScopeForUserIsOwnAccount() { + stubOwner(Account.Type.NORMAL); + + Pair> scope = manager.getGenericRuleScope(vmCpuRule(null)); + + assertEquals(Long.valueOf(1L), scope.first()); + assertNull(scope.second()); + } + + @Test + public void testGenericVolumeRuleForRootAdminListsReadyVolumesCloudWide() { + ResourceAlertRuleVO rule = new ResourceAlertRuleVO("vol", ResourceAlertRule.ResourceType.Volume, + null, 1L, 1L, "VOLUME_SIZE_GB", AlertCondition.GT, 10.0, AlertSeverity.LOW, null, false, 600); + when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule)); + stubOwner(Account.Type.ADMIN); + + manager.evaluateRules(); + + verify(volumeDao).listIdsByAccountOrDomainsAndState(null, null, Volume.State.Ready); + } + + @Test + public void testRuleRemovedWhenOwnerMissing() { + ResourceAlertRuleVO rule = vmCpuRule(null); + when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule)); + when(accountDao.findById(1L)).thenReturn(null); + + manager.evaluateRules(); + + verify(ruleDao).remove(rule.getId()); + verify(userVmDao, never()).listIdsByAccountOrDomainsAndState(any(), any(), any()); + } + + @Test + public void testSpecificRuleRemovedWhenResourceExpunged() { + ResourceAlertRuleVO rule = vmCpuRule(VM_ID); + when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule)); + when(userVmDao.findById(VM_ID)).thenReturn(null); + + manager.evaluateRules(); + + verify(ruleDao).remove(rule.getId()); + verify(statsCollector, never()).getVmStats(anyLong(), any(Boolean.class)); + } + + @Test + public void testSpecificHostRuleKeptWhileHostExists() { + ResourceAlertRuleVO rule = hostRule("CPU_UTILIZATION", 90.0); + when(ruleDao.listActive()).thenReturn(Collections.singletonList(rule)); + + manager.evaluateRules(); + + verify(ruleDao, never()).remove(anyLong()); + } + + @Test + public void testEvaluatesOnLongestRunningManagementServer() { + ManagementServerHostVO msHost = mock(ManagementServerHostVO.class); + when(msHost.getMsid()).thenReturn(ManagementServerNode.getManagementServerId()); + when(managementServerHostDao.findOneByLongestRuntime()).thenReturn(msHost); + + assertTrue(manager.isEvaluatingServer()); + } + + @Test + public void testDoesNotEvaluateOnOtherManagementServers() { + ManagementServerHostVO msHost = mock(ManagementServerHostVO.class); + when(msHost.getMsid()).thenReturn(ManagementServerNode.getManagementServerId() + 1); + when(managementServerHostDao.findOneByLongestRuntime()).thenReturn(msHost); + + assertFalse(manager.isEvaluatingServer()); + } + + @Test + public void testDoesNotEvaluateWhenNoManagementServerFound() { + assertFalse(manager.isEvaluatingServer()); + } + + @Test + public void testRemoveExpiredAlertsUsesRetentionDays() { + long before = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30); + + manager.removeExpiredAlerts(); + + ArgumentCaptor cutoff = ArgumentCaptor.forClass(Date.class); + verify(alertDao).removeOlderThan(cutoff.capture()); + long diff = Math.abs(cutoff.getValue().getTime() - before); + assertTrue("cutoff should be about 30 days ago", diff < 60_000L); + } +} 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..cc004908c2c3 --- /dev/null +++ b/plugins/resource-alerts/src/test/java/org/apache/cloudstack/resourcealert/ResourceAlertMetricTest.java @@ -0,0 +1,106 @@ +// 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 testNetworkMetricsApplyToVmAndHost() { + for (ResourceAlertMetric m : new ResourceAlertMetric[]{ + ResourceAlertMetric.NETWORK_READ_KBPS, ResourceAlertMetric.NETWORK_WRITE_KBPS}) { + assertTrue(m.name(), m.appliesTo(ResourceAlertRule.ResourceType.VirtualMachine)); + assertTrue(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)); + } + + @Test + public void testLoadAverageAppliesToHostOnly() { + assertTrue(ResourceAlertMetric.LOAD_AVERAGE.appliesTo(ResourceAlertRule.ResourceType.Host)); + assertFalse(ResourceAlertMetric.LOAD_AVERAGE.appliesTo(ResourceAlertRule.ResourceType.VirtualMachine)); + assertFalse(ResourceAlertMetric.LOAD_AVERAGE.appliesTo(ResourceAlertRule.ResourceType.Volume)); + assertFalse(ResourceAlertMetric.LOAD_AVERAGE.appliesTo(ResourceAlertRule.ResourceType.StoragePool)); + } + + @Test + public void testVolumeSizeAppliesToVolumeOnly() { + assertTrue(ResourceAlertMetric.VOLUME_SIZE_GB.appliesTo(ResourceAlertRule.ResourceType.Volume)); + assertFalse(ResourceAlertMetric.VOLUME_SIZE_GB.appliesTo(ResourceAlertRule.ResourceType.VirtualMachine)); + assertFalse(ResourceAlertMetric.VOLUME_SIZE_GB.appliesTo(ResourceAlertRule.ResourceType.Host)); + assertFalse(ResourceAlertMetric.VOLUME_SIZE_GB.appliesTo(ResourceAlertRule.ResourceType.StoragePool)); + } + + @Test + public void testStorageUsedIopsAppliesToStoragePoolOnly() { + assertTrue(ResourceAlertMetric.STORAGE_USED_IOPS.appliesTo(ResourceAlertRule.ResourceType.StoragePool)); + assertFalse(ResourceAlertMetric.STORAGE_USED_IOPS.appliesTo(ResourceAlertRule.ResourceType.Volume)); + assertFalse(ResourceAlertMetric.STORAGE_USED_IOPS.appliesTo(ResourceAlertRule.ResourceType.Host)); + assertFalse(ResourceAlertMetric.STORAGE_USED_IOPS.appliesTo(ResourceAlertRule.ResourceType.VirtualMachine)); + } + + @Test + public void testPercentageMetrics() { + assertTrue(ResourceAlertMetric.CPU_UTILIZATION.isPercentage()); + assertTrue(ResourceAlertMetric.MEMORY_UTILIZATION.isPercentage()); + assertTrue(ResourceAlertMetric.STORAGE_UTILIZATION.isPercentage()); + assertFalse(ResourceAlertMetric.DISK_READ_IOPS.isPercentage()); + assertFalse(ResourceAlertMetric.VOLUME_SIZE_GB.isPercentage()); + } +} 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..c684f14e9780 --- /dev/null +++ b/plugins/resource-alerts/src/test/java/org/apache/cloudstack/resourcealert/ResourceAlertServiceImplTest.java @@ -0,0 +1,491 @@ +// 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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.resourcealert.api.command.user.CreateResourceAlertRuleCmd; +import org.apache.cloudstack.resourcealert.api.command.user.DeleteResourceAlertRuleCmd; +import org.apache.cloudstack.resourcealert.api.command.user.ListResourceAlertsCmd; +import org.apache.cloudstack.resourcealert.api.command.user.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.dao.ResourceAlertRuleWebhookDao; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.resourcealert.api.response.ResourceAlertResponse; +import org.apache.cloudstack.resourcealert.api.response.ResourceAlertRuleResponse; +import org.apache.cloudstack.resourcealert.vo.ResourceAlertRuleJoinVO; +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.webhook.WebhookHelper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.host.dao.HostDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.utils.Pair; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.dao.UserVmDao; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class ResourceAlertServiceImplTest { + + @Spy + @InjectMocks + ResourceAlertServiceImpl service; + + @Mock AccountManager accountManager; + @Mock ResourceAlertRuleDao ruleDao; + @Mock ResourceAlertRuleJoinDao ruleJoinDao; + @Mock ResourceAlertDao alertDao; + @Mock ResourceAlertRuleWebhookDao ruleWebhookDao; + @Mock WebhookHelper webhookHelper; + @Mock UserVmDao userVmDao; + @Mock VolumeDao volumeDao; + @Mock HostDao hostDao; + @Mock PrimaryDataStoreDao storagePoolDao; + + private MockedStatic callContextMocked; + private Account caller; + private Account owner; + + @Before + public void setUp() { + caller = mock(Account.class); + when(caller.getId()).thenReturn(2L); + CallContext callContext = mock(CallContext.class); + when(callContext.getCallingAccount()).thenReturn(caller); + callContextMocked = Mockito.mockStatic(CallContext.class); + callContextMocked.when(CallContext::current).thenReturn(callContext); + + owner = mock(Account.class); + when(owner.getId()).thenReturn(42L); + when(accountManager.finalizeOwner(eq(caller), any(), any(), any())).thenReturn(owner); + } + + @After + public void tearDown() { + callContextMocked.close(); + } + + private CreateResourceAlertRuleCmd validVmCreateCmd() { + CreateResourceAlertRuleCmd cmd = mock(CreateResourceAlertRuleCmd.class); + when(cmd.getName()).thenReturn("cpu-high"); + when(cmd.getResourceType()).thenReturn("VirtualMachine"); + when(cmd.getCondition()).thenReturn("GT"); + when(cmd.getSeverity()).thenReturn("HIGH"); + when(cmd.getMetric()).thenReturn("CPU_UTILIZATION"); + when(cmd.getThreshold()).thenReturn(80.0); + when(cmd.getResetInterval()).thenReturn(null); + when(cmd.getEmail()).thenReturn(null); + return cmd; + } + + private ResourceAlertRuleVO persistedRuleCapture() { + ArgumentCaptor captor = ArgumentCaptor.forClass(ResourceAlertRuleVO.class); + verify(ruleDao).persist(captor.capture()); + return captor.getValue(); + } + + @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 = validVmCreateCmd(); + when(cmd.getMetric()).thenReturn("STORAGE_UTILIZATION"); + + service.createResourceAlertRule(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCreateFailsWhenAccountAtRuleLimit() { + CreateResourceAlertRuleCmd cmd = validVmCreateCmd(); + // default limit is 20 + when(ruleDao.countActiveByAccountId(42L)).thenReturn(20); + + service.createResourceAlertRule(cmd); + } + + @Test + public void testCreateUsesDefaultResetIntervalWhenNotSet() { + service.createResourceAlertRule(validVmCreateCmd()); + + assertEquals(600, persistedRuleCapture().getResetInterval()); + } + + @Test + public void testCreateAssignsRuleToFinalizedOwner() { + service.createResourceAlertRule(validVmCreateCmd()); + + assertEquals(42L, persistedRuleCapture().getAccountId()); + } + + @Test + public void testCreateResolvesResourceUuidAndChecksOwnerAccess() { + CreateResourceAlertRuleCmd cmd = validVmCreateCmd(); + when(cmd.getResourceId()).thenReturn("vm-uuid"); + UserVmVO vm = mock(UserVmVO.class); + when(vm.getId()).thenReturn(7L); + when(userVmDao.findByUuid("vm-uuid")).thenReturn(vm); + + service.createResourceAlertRule(cmd); + + verify(accountManager).checkAccess(owner, null, false, (ControlledEntity) vm); + assertEquals(Long.valueOf(7L), persistedRuleCapture().getResourceId()); + } + + @Test(expected = PermissionDeniedException.class) + public void testCreateFailsWhenOwnerCannotAccessResource() { + CreateResourceAlertRuleCmd cmd = validVmCreateCmd(); + when(cmd.getResourceId()).thenReturn("vm-uuid"); + UserVmVO vm = mock(UserVmVO.class); + when(userVmDao.findByUuid("vm-uuid")).thenReturn(vm); + doThrow(new PermissionDeniedException("denied")) + .when(accountManager).checkAccess(owner, null, false, (ControlledEntity) vm); + + service.createResourceAlertRule(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCreateFailsOnUnknownResourceUuid() { + CreateResourceAlertRuleCmd cmd = validVmCreateCmd(); + when(cmd.getResourceId()).thenReturn("no-such-vm"); + + service.createResourceAlertRule(cmd); + } + + @Test(expected = PermissionDeniedException.class) + public void testCreateHostRuleFailsForNonRootAdmin() { + CreateResourceAlertRuleCmd cmd = validVmCreateCmd(); + when(cmd.getResourceType()).thenReturn("Host"); + when(accountManager.isRootAdmin(2L)).thenReturn(false); + + service.createResourceAlertRule(cmd); + } + + @Test + public void testCreateHostRuleAllowedForRootAdmin() { + CreateResourceAlertRuleCmd cmd = validVmCreateCmd(); + when(cmd.getResourceType()).thenReturn("Host"); + when(accountManager.isRootAdmin(2L)).thenReturn(true); + + service.createResourceAlertRule(cmd); + + verify(ruleDao).persist(any(ResourceAlertRuleVO.class)); + } + + @Test(expected = PermissionDeniedException.class) + public void testCreateWithEmailFailsForNonRootAdmin() { + CreateResourceAlertRuleCmd cmd = validVmCreateCmd(); + when(cmd.getEmail()).thenReturn(true); + when(accountManager.isRootAdmin(2L)).thenReturn(false); + + service.createResourceAlertRule(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateFailsWhenRuleNotFound() { + UpdateResourceAlertRuleCmd cmd = mock(UpdateResourceAlertRuleCmd.class); + when(cmd.getId()).thenReturn(999L); + + 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 = PermissionDeniedException.class) + public void testUpdateFailsWhenCallerCannotAccessRule() { + UpdateResourceAlertRuleCmd cmd = mock(UpdateResourceAlertRuleCmd.class); + when(cmd.getId()).thenReturn(1L); + ResourceAlertRuleVO rule = mock(ResourceAlertRuleVO.class); + when(ruleDao.findById(1L)).thenReturn(rule); + doThrow(new PermissionDeniedException("denied")).when(accountManager).checkAccess(caller, null, true, rule); + + service.updateResourceAlertRule(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testDeleteFailsWhenRuleNotFound() { + DeleteResourceAlertRuleCmd cmd = mock(DeleteResourceAlertRuleCmd.class); + when(cmd.getId()).thenReturn(999L); + + service.deleteResourceAlertRule(cmd); + } + + @Test + public void testDeleteDoesNotRemoveWhenCallerCannotAccessRule() { + DeleteResourceAlertRuleCmd cmd = mock(DeleteResourceAlertRuleCmd.class); + when(cmd.getId()).thenReturn(1L); + ResourceAlertRuleVO rule = mock(ResourceAlertRuleVO.class); + when(ruleDao.findById(1L)).thenReturn(rule); + doThrow(new PermissionDeniedException("denied")).when(accountManager).checkAccess(caller, null, true, rule); + + try { + service.deleteResourceAlertRule(cmd); + } catch (PermissionDeniedException expected) { + } + verify(ruleDao, never()).remove(1L); + } + + @Test(expected = InvalidParameterValueException.class) + public void testListAlertsFailsWithUnknownRuleUuid() { + ListResourceAlertsCmd cmd = mock(ListResourceAlertsCmd.class); + when(cmd.getAlertRuleId()).thenReturn("no-such-uuid"); + + service.listResourceAlerts(cmd); + } + + @Test(expected = PermissionDeniedException.class) + public void testListAlertsFailsWhenCallerCannotAccessRule() { + ListResourceAlertsCmd cmd = mock(ListResourceAlertsCmd.class); + when(cmd.getAlertRuleId()).thenReturn("rule-uuid"); + ResourceAlertRuleVO rule = mock(ResourceAlertRuleVO.class); + when(ruleDao.findByUuid("rule-uuid")).thenReturn(rule); + doThrow(new PermissionDeniedException("denied")).when(accountManager).checkAccess(caller, null, true, rule); + + service.listResourceAlerts(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testListAlertsFailsWhenResourceIdWithoutType() { + ListResourceAlertsCmd cmd = mock(ListResourceAlertsCmd.class); + when(cmd.getResourceId()).thenReturn("vm-uuid"); + + service.listResourceAlerts(cmd); + } + + private ControlledEntity mockWebhook(String uuid, long id) { + ControlledEntity webhook = mock(ControlledEntity.class, Mockito.withSettings().extraInterfaces(InternalIdentity.class)); + when(((InternalIdentity) webhook).getId()).thenReturn(id); + when(webhookHelper.findWebhookByUuid(uuid)).thenReturn(webhook); + return webhook; + } + + @Test + public void testCreateMapsWebhooksAfterCheckingOwnerAccess() { + doReturn(webhookHelper).when(service).getWebhookHelper(); + CreateResourceAlertRuleCmd cmd = validVmCreateCmd(); + when(cmd.getWebhookIds()).thenReturn(List.of("wh-1", "wh-1")); + ControlledEntity webhook = mockWebhook("wh-1", 11L); + + service.createResourceAlertRule(cmd); + + verify(accountManager, Mockito.times(2)).checkAccess(owner, null, false, webhook); + verify(ruleWebhookDao).replaceWebhooksForRule(Mockito.anyLong(), eq(List.of(11L))); + } + + @Test(expected = PermissionDeniedException.class) + public void testCreateFailsWhenOwnerCannotAccessWebhook() { + doReturn(webhookHelper).when(service).getWebhookHelper(); + CreateResourceAlertRuleCmd cmd = validVmCreateCmd(); + when(cmd.getWebhookIds()).thenReturn(List.of("wh-1")); + ControlledEntity webhook = mockWebhook("wh-1", 11L); + doThrow(new PermissionDeniedException("denied")).when(accountManager).checkAccess(owner, null, false, webhook); + + service.createResourceAlertRule(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCreateFailsOnUnknownWebhook() { + doReturn(webhookHelper).when(service).getWebhookHelper(); + CreateResourceAlertRuleCmd cmd = validVmCreateCmd(); + when(cmd.getWebhookIds()).thenReturn(List.of("no-such-webhook")); + + service.createResourceAlertRule(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCreateWithWebhooksFailsWhenWebhookPluginMissing() { + doReturn(null).when(service).getWebhookHelper(); + CreateResourceAlertRuleCmd cmd = validVmCreateCmd(); + when(cmd.getWebhookIds()).thenReturn(List.of("wh-1")); + + service.createResourceAlertRule(cmd); + } + + @Test + public void testCreateWithoutWebhooksDoesNotTouchMapping() { + service.createResourceAlertRule(validVmCreateCmd()); + + verify(ruleWebhookDao, never()).replaceWebhooksForRule(Mockito.anyLong(), any()); + } + + @Test + public void testUpdateCleanupWebhooksClearsMapping() { + UpdateResourceAlertRuleCmd cmd = mock(UpdateResourceAlertRuleCmd.class); + when(cmd.getId()).thenReturn(1L); + when(cmd.isCleanupWebhooks()).thenReturn(true); + when(cmd.getThreshold()).thenReturn(null); + when(cmd.getResetInterval()).thenReturn(null); + ResourceAlertRuleVO rule = mock(ResourceAlertRuleVO.class); + when(rule.getId()).thenReturn(1L); + when(ruleDao.findById(1L)).thenReturn(rule); + + service.updateResourceAlertRule(cmd); + + verify(ruleWebhookDao).replaceWebhooksForRule(1L, new ArrayList<>()); + } + + @Test + public void testListAlertsPassesPagingAndReturnsTotalCount() { + ListResourceAlertsCmd cmd = mock(ListResourceAlertsCmd.class); + when(cmd.getDomainId()).thenReturn(null); + when(cmd.getStartIndex()).thenReturn(20L); + when(cmd.getPageSizeVal()).thenReturn(10L); + ResourceAlertVO alert = mock(ResourceAlertVO.class); + when(alertDao.searchAndCountByFilters(null, null, null, null, null, 20L, 10L)) + .thenReturn(new Pair<>(List.of(alert), 57)); + + ListResponse response = service.listResourceAlerts(cmd); + + assertEquals(Integer.valueOf(57), response.getCount()); + assertEquals(1, response.getResponses().size()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCreateFailsOnPercentageThresholdAbove100() { + CreateResourceAlertRuleCmd cmd = validVmCreateCmd(); + when(cmd.getThreshold()).thenReturn(150.0); + + service.createResourceAlertRule(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCreateFailsOnNegativeThreshold() { + CreateResourceAlertRuleCmd cmd = validVmCreateCmd(); + when(cmd.getMetric()).thenReturn("NETWORK_READ_KBPS"); + when(cmd.getThreshold()).thenReturn(-1.0); + + service.createResourceAlertRule(cmd); + } + + @Test + public void testCreateAllowsNonPercentageThresholdAbove100() { + CreateResourceAlertRuleCmd cmd = validVmCreateCmd(); + when(cmd.getMetric()).thenReturn("NETWORK_READ_KBPS"); + when(cmd.getThreshold()).thenReturn(5000.0); + + service.createResourceAlertRule(cmd); + + verify(ruleDao).persist(any(ResourceAlertRuleVO.class)); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCreateFailsOnNegativeResetInterval() { + CreateResourceAlertRuleCmd cmd = validVmCreateCmd(); + when(cmd.getResetInterval()).thenReturn(-5); + + service.createResourceAlertRule(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateFailsOnPercentageThresholdAbove100() { + UpdateResourceAlertRuleCmd cmd = mock(UpdateResourceAlertRuleCmd.class); + when(cmd.getId()).thenReturn(1L); + when(cmd.getThreshold()).thenReturn(101.0); + ResourceAlertRuleVO rule = mock(ResourceAlertRuleVO.class); + when(rule.getMetric()).thenReturn("CPU_UTILIZATION"); + when(ruleDao.findById(1L)).thenReturn(rule); + + service.updateResourceAlertRule(cmd); + } + + @Test + public void testRuleResponseIncludesResourceName() { + CreateResourceAlertRuleCmd cmd = validVmCreateCmd(); + when(cmd.getResourceId()).thenReturn("vm-uuid"); + UserVmVO vm = mock(UserVmVO.class); + when(vm.getId()).thenReturn(7L); + when(vm.getUuid()).thenReturn("vm-uuid"); + when(vm.getDisplayName()).thenReturn("web-01"); + when(userVmDao.findByUuid("vm-uuid")).thenReturn(vm); + when(userVmDao.findByIdIncludingRemoved(7L)).thenReturn(vm); + ResourceAlertRuleJoinVO joined = mock(ResourceAlertRuleJoinVO.class); + when(joined.getResourceType()).thenReturn(ResourceAlertRule.ResourceType.VirtualMachine); + when(joined.getResourceId()).thenReturn(7L); + when(ruleJoinDao.findById(Mockito.anyLong())).thenReturn(joined); + + ResourceAlertRuleResponse response = service.createResourceAlertRule(cmd); + + assertEquals("vm-uuid", org.springframework.test.util.ReflectionTestUtils.getField(response, "resourceId")); + assertEquals("web-01", org.springframework.test.util.ReflectionTestUtils.getField(response, "resourceName")); + } +} diff --git a/server/src/main/java/org/apache/cloudstack/webhook/WebhookHelper.java b/server/src/main/java/org/apache/cloudstack/webhook/WebhookHelper.java index 4f2305004a97..a328c5fe54c4 100644 --- a/server/src/main/java/org/apache/cloudstack/webhook/WebhookHelper.java +++ b/server/src/main/java/org/apache/cloudstack/webhook/WebhookHelper.java @@ -25,4 +25,10 @@ public interface WebhookHelper { void deleteWebhooksForAccount(long accountId); List listWebhooksByAccount(long accountId); + + ControlledEntity findWebhookByUuid(String uuid); + + String getWebhookUuid(long webhookId); + + void deliverToWebhooks(List webhookIds, long accountId, String eventType, String payload); } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 99bf2cf7aef9..0496dccd8cc5 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -1372,6 +1372,7 @@ "label.info": "Info", "label.info.upper": "INFO", "label.infrastructure": "Infrastructure", +"label.monitoring": "Monitoring", "label.ingest.instance": "Ingest Instance", "label.ingress": "Ingress", "label.ingress.rule": "Ingress Rule", @@ -2316,6 +2317,25 @@ "label.resourcegroup": "Resource group", "label.linstor.apitoken": "Controller API token", "label.linstor.ssl.insecure": "Allow self-signed certificate", +"label.resource.alert.rules": "Resource Alerts", +"label.resource.alerts": "Alerts", +"label.create.resource.alert.rule": "New Resource Alert", +"label.firedalerts": "Alert History", +"label.resourcealerts": "Alerts", +"label.metric": "Metric", +"label.condition": "Condition", +"label.severity": "Severity", +"label.message": "Message", +"label.resetinterval": "Cooldown (seconds)", +"label.resource.alert.all.resources": "All resources", +"label.resource.alert.owner.self": "Your own account", +"label.alertrulename": "Alert rule", +"label.webhookids": "Webhooks", +"label.cleanupwebhooks": "Remove all webhooks", +"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?", "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 9a7d874fef3e..a928fc492a6a 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -1222,7 +1222,7 @@ export default { '/computeoffering', '/systemoffering', '/diskoffering', '/backupoffering', '/networkoffering', '/vpcoffering', '/tungstenfabric', '/oauthsetting', '/guestos', '/guestoshypervisormapping', '/webhook', 'webhookdeliveries', 'webhookfilters', '/quotatariff', '/sharedfs', '/ipv4subnets', '/managementserver', '/gpucard', '/gpudevices', '/vgpuprofile', '/extension', '/snapshotpolicy', '/backupschedule', - '/kmskey', '/hsmprofile', '/dnsserver', '/dnszone'].join('|')) + '/kmskey', '/hsmprofile', '/dnsserver', '/dnszone', '/resourcealerts'].join('|')) .test(this.$route.path) }, enableGroupAction () { @@ -1231,7 +1231,8 @@ export default { 'project', 'account', 'systemvm', 'router', 'computeoffering', 'systemoffering', 'diskoffering', 'backupoffering', 'networkoffering', 'vpcoffering', 'ilbvm', 'kubernetes', 'comment', 'buckets', 'webhook', 'webhookdeliveries', 'sharedfs', 'ipv4subnets', 'asnumbers', 'guestos', 'gpucard', 'gpudevices', 'vgpuprofile', - 'quotatariff'].includes(this.$route.name) + 'quotatariff', 'resourcealerts' + ].includes(this.$route.name) }, getDateAtTimeZone (date, timezone) { return date ? moment(date).tz(timezone).format('YYYY-MM-DD HH:mm:ss') : null diff --git a/ui/src/components/view/ResourceAlertFiredTab.vue b/ui/src/components/view/ResourceAlertFiredTab.vue new file mode 100644 index 000000000000..646a0270c666 --- /dev/null +++ b/ui/src/components/view/ResourceAlertFiredTab.vue @@ -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. + + + + diff --git a/ui/src/components/view/ResourceAlertsTab.vue b/ui/src/components/view/ResourceAlertsTab.vue new file mode 100644 index 000000000000..a6665f0798d8 --- /dev/null +++ b/ui/src/components/view/ResourceAlertsTab.vue @@ -0,0 +1,101 @@ +// 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/router.js b/ui/src/config/router.js index b9c60bcd0c21..30b468764b69 100644 --- a/ui/src/config/router.js +++ b/ui/src/config/router.js @@ -43,6 +43,7 @@ import config from '@/config/section/config' import extension from '@/config/section/extension' import customaction from '@/config/section/extension/customaction' import tools from '@/config/section/tools' +import monitoring from '@/config/section/monitoring' import quota from '@/config/section/plugin/quota' import cloudian from '@/config/section/plugin/cloudian' @@ -229,6 +230,7 @@ export function asyncRouterMap () { generateRouterMap(account), generateRouterMap(domain), generateRouterMap(infra), + generateRouterMap(monitoring), generateRouterMap(zone), generateRouterMap(offering), generateRouterMap(config), diff --git a/ui/src/config/section/infra/hosts.js b/ui/src/config/section/infra/hosts.js index 046f120dd37a..ce2e356dba3c 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..d32c09548c39 --- /dev/null +++ b/ui/src/config/section/infra/resourceAlertRules.js @@ -0,0 +1,79 @@ +// 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: 'resourcealerts', + title: 'label.resource.alert.rules', + icon: 'BellOutlined', + permission: ['listResourceAlertRules'], + columns: () => { + const cols = ['name', 'resourcetype', 'resourcename', 'metric', 'condition', 'threshold', 'severity'] + if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) cols.push('account') + return cols + }, + details: ['name', 'id', 'resourcetype', 'resourcename', '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: (record, store) => { + const args = ['name', 'condition', 'threshold', 'severity', 'message', 'resetinterval', 'webhookids', 'cleanupwebhooks'] + if (store.userInfo.roletype === 'Admin') args.push('email') + return args + }, + 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/monitoring.js b/ui/src/config/section/monitoring.js new file mode 100644 index 000000000000..1bade98aa104 --- /dev/null +++ b/ui/src/config/section/monitoring.js @@ -0,0 +1,28 @@ +// 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 resourceAlerts from '@/config/section/infra/resourceAlertRules' + +export default { + name: 'monitoring', + title: 'label.monitoring', + icon: 'BarChartOutlined', + permission: ['listResourceAlertRules'], + children: [ + resourceAlerts + ] +} diff --git a/ui/src/config/section/storage.js b/ui/src/config/section/storage.js index 75bdfd4d5fa6..d0ea953569e7 100644 --- a/ui/src/config/section/storage.js +++ b/ui/src/config/section/storage.js @@ -81,6 +81,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 d125995e3e1c..c4c53dc91993 100644 --- a/ui/src/views/compute/InstanceTab.vue +++ b/ui/src/views/compute/InstanceTab.vue @@ -107,6 +107,9 @@ + + + @@ -163,6 +166,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' @@ -180,6 +184,7 @@ export default { ResourceSchedules, 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..00cbe04f8a64 --- /dev/null +++ b/ui/src/views/resourcealert/CreateResourceAlertRule.vue @@ -0,0 +1,341 @@ +// 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. + + + + + +