From 7bc15c39eaab2b2ed9c20bb512a1773cbc447be6 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 16 Jan 2024 14:27:59 +0530 Subject: [PATCH 01/75] api,server,ui: weebhoks feature Signed-off-by: Abhishek Kumar --- .../apache/cloudstack/api/ApiConstants.java | 13 +- .../api/command/admin/user/UpdateUserCmd.java | 2 +- .../api/response/BucketResponse.java | 2 +- client/pom.xml | 5 + .../META-INF/db/schema-41900to42000.sql | 21 ++ plugins/event-bus/webhook/pom.xml | 42 ++++ .../mom/webhook/WebhookApiService.java | 37 +++ .../mom/webhook/WebhookApiServiceImpl.java | 214 ++++++++++++++++++ .../mom/webhook/WebhookDispatchThread.java | 104 +++++++++ .../mom/webhook/WebhookEventBus.java | 87 +++++++ .../cloudstack/mom/webhook/WebhookRule.java | 46 ++++ .../mom/webhook/WebhookService.java | 44 ++++ .../mom/webhook/WebhookServiceImpl.java | 105 +++++++++ .../command/user/CreateWebhookRuleCmd.java | 168 ++++++++++++++ .../command/user/DeleteWebhookRuleCmd.java | 85 +++++++ .../api/command/user/ListWebhookRulesCmd.java | 84 +++++++ .../command/user/UpdateWebhookRuleCmd.java | 84 +++++++ .../api/response/WebhookRuleResponse.java | 148 ++++++++++++ .../mom/webhook/dao/WebhookRuleDao.java | 25 ++ .../mom/webhook/dao/WebhookRuleDaoImpl.java | 25 ++ .../mom/webhook/vo/WebhookRuleVO.java | 213 +++++++++++++++++ .../cloudstack/webhook/module.properties | 18 ++ .../webhook/spring-webhook-context.xml | 38 ++++ plugins/pom.xml | 1 + tools/apidoc/gen_toc.py | 4 +- 25 files changed, 1607 insertions(+), 8 deletions(-) create mode 100644 plugins/event-bus/webhook/pom.xml create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiService.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatchThread.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookEventBus.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookRule.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookServiceImpl.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/CreateWebhookRuleCmd.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookRuleCmd.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookRulesCmd.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookRuleCmd.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookRuleResponse.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookRuleDao.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookRuleDaoImpl.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookRuleVO.java create mode 100644 plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/module.properties create mode 100644 plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/spring-webhook-context.xml 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 db0c5ce494c1..7dca4b2c5b49 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -278,6 +278,7 @@ public class ApiConstants { public static final String JOB_STATUS = "jobstatus"; public static final String KEEPALIVE_ENABLED = "keepaliveenabled"; public static final String KERNEL_VERSION = "kernelversion"; + public static final String KEY = "key"; public static final String LABEL = "label"; public static final String LASTNAME = "lastname"; public static final String LAST_BOOT = "lastboottime"; @@ -349,6 +350,7 @@ public class ApiConstants { public static final String SSHKEY_ENABLED = "sshkeyenabled"; public static final String PATH = "path"; public static final String PAYLOAD = "payload"; + public static final String PAYLOAD_URL = "payloadurl"; public static final String POD_ID = "podid"; public static final String POD_NAME = "podname"; public static final String POD_IDS = "podids"; @@ -392,11 +394,9 @@ public class ApiConstants { public static final String QUERY_FILTER = "queryfilter"; public static final String SCHEDULE = "schedule"; public static final String SCOPE = "scope"; - public static final String SECRET_KEY = "usersecretkey"; - public static final String SECONDARY_IP = "secondaryip"; - public static final String SINCE = "since"; - public static final String KEY = "key"; public static final String SEARCH_BASE = "searchbase"; + public static final String SECONDARY_IP = "secondaryip"; + public static final String SECRET_KEY = "secretkey"; public static final String SECURITY_GROUP_IDS = "securitygroupids"; public static final String SECURITY_GROUP_NAMES = "securitygroupnames"; public static final String SECURITY_GROUP_NAME = "securitygroupname"; @@ -414,14 +414,15 @@ public class ApiConstants { public static final String SHOW_UNIQUE = "showunique"; public static final String SIGNATURE = "signature"; public static final String SIGNATURE_VERSION = "signatureversion"; + public static final String SINCE = "since"; public static final String SIZE = "size"; public static final String SNAPSHOT = "snapshot"; public static final String SNAPSHOT_ID = "snapshotid"; public static final String SNAPSHOT_POLICY_ID = "snapshotpolicyid"; public static final String SNAPSHOT_TYPE = "snapshottype"; public static final String SNAPSHOT_QUIESCEVM = "quiescevm"; - public static final String SUPPORTS_STORAGE_SNAPSHOT = "supportsstoragesnapshot"; public static final String SOURCE_ZONE_ID = "sourcezoneid"; + public static final String SSL_VERIFICATION = "sslverification"; public static final String START_DATE = "startdate"; public static final String START_ID = "startid"; public static final String START_IP = "startip"; @@ -440,6 +441,7 @@ public class ApiConstants { public static final String SYSTEM_VM_TYPE = "systemvmtype"; public static final String TAGS = "tags"; public static final String STORAGE_TAGS = "storagetags"; + public static final String SUPPORTS_STORAGE_SNAPSHOT = "supportsstoragesnapshot"; public static final String TARGET_IQN = "targetiqn"; public static final String TEMPLATE_FILTER = "templatefilter"; public static final String TEMPLATE_ID = "templateid"; @@ -471,6 +473,7 @@ public class ApiConstants { public static final String USERNAME = "username"; public static final String USER_CONFIGURABLE = "userconfigurable"; public static final String USER_SECURITY_GROUP_LIST = "usersecuritygrouplist"; + public static final String USER_SECRET_KEY = "usersecretkey"; public static final String USE_VIRTUAL_NETWORK = "usevirtualnetwork"; public static final String UPDATE_IN_SEQUENCE = "updateinsequence"; public static final String VALUE = "value"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java index cb9f6e189f03..082b1af0726f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java @@ -68,7 +68,7 @@ public class UpdateUserCmd extends BaseCmd { @Parameter(name = ApiConstants.CURRENT_PASSWORD, type = CommandType.STRING, description = "Current password that was being used by the user. You must inform the current password when updating the password.", acceptedOnAdminPort = false) private String currentPassword; - @Parameter(name = ApiConstants.SECRET_KEY, type = CommandType.STRING, description = "The secret key for the user. Must be specified with userApiKey") + @Parameter(name = ApiConstants.USER_SECRET_KEY, type = CommandType.STRING, description = "The secret key for the user. Must be specified with userApiKey") private String secretKey; @Parameter(name = ApiConstants.TIMEZONE, diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BucketResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BucketResponse.java index b75f36043244..a1cce6e43d7a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/BucketResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/BucketResponse.java @@ -98,7 +98,7 @@ public class BucketResponse extends BaseResponseWithTagInformation implements Co @Param(description = "Bucket Access Key") private String accessKey; - @SerializedName(ApiConstants.SECRET_KEY) + @SerializedName(ApiConstants.USER_SECRET_KEY) @Param(description = "Bucket Secret Key") private String secretKey; diff --git a/client/pom.xml b/client/pom.xml index 8b8747bdf930..d379a3b68824 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -442,6 +442,11 @@ cloud-mom-inmemory ${project.version} + + org.apache.cloudstack + cloud-mom-webhook + ${project.version} + org.apache.cloudstack cloud-mom-kafka diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql b/engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql index 1c368a2fbee2..d47b794bdd77 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql @@ -18,3 +18,24 @@ --; -- Schema upgrade from 4.19.0.0 to 4.20.0.0 --; + +-- Webhooks feature +DROP TABLE IF EXISTS `cloud`.`webhook`; +CREATE TABLE `cloud`.`webhook` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the webhook', + `uuid` varchar(255) COMMENT 'uuid of the webhook', + `name` varchar(255) NOT NULL COMMENT 'name of the webhook', + `description` varchar(4096) COMMENT 'description for the webhook', + `state` varchar(255) NOT NULL COMMENT 'state of the webhook - Enabled or Disabled', + `domain_id` bigint unsigned NOT NULL COMMENT 'ID of the owner domain of the webhook', + `account_id` bigint unsigned NOT NULL COMMENT 'ID of the owner account of the webhook', + `payload_url` varchar(255) COMMENT 'payload URL for the webhook', + `secret_key` varchar(255) COMMENT 'secret key for the webhook', + `ssl_verification` boolean COMMENT 'for https payload url', + `scope` char(32) NOT NULL COMMENT 'scope for the webhook - Local,Domain,Global', + `created` datetime COMMENT 'date the webhook was created', + `removed` datetime COMMENT 'date removed if not null', + PRIMARY KEY(`id`), + INDEX `i_webhook__account_id`(`account_id`), + CONSTRAINT `fk_webhook__account_id` FOREIGN KEY (`account_id`) REFERENCES `account`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/plugins/event-bus/webhook/pom.xml b/plugins/event-bus/webhook/pom.xml new file mode 100644 index 000000000000..72fa785740b9 --- /dev/null +++ b/plugins/event-bus/webhook/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + cloud-mom-webhook + Apache CloudStack Plugin - Webhook Event Bus + + org.apache.cloudstack + cloudstack-plugins + 4.19.0.0-SNAPSHOT + ../../pom.xml + + + + org.apache.cloudstack + cloud-framework-events + ${project.version} + + + org.apache.cloudstack + cloud-engine-api + ${project.version} + + + diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiService.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiService.java new file mode 100644 index 000000000000..8025992daf66 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiService.java @@ -0,0 +1,37 @@ +// 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.mom.webhook; + +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.mom.webhook.api.command.user.CreateWebhookRuleCmd; +import org.apache.cloudstack.mom.webhook.api.command.user.DeleteWebhookRuleCmd; +import org.apache.cloudstack.mom.webhook.api.command.user.ListWebhookRulesCmd; +import org.apache.cloudstack.mom.webhook.api.command.user.UpdateWebhookRuleCmd; +import org.apache.cloudstack.mom.webhook.api.response.WebhookRuleResponse; + +import com.cloud.utils.component.PluggableService; +import com.cloud.utils.exception.CloudRuntimeException; + +public interface WebhookApiService extends PluggableService { + + ListResponse listWebhookRules(ListWebhookRulesCmd cmd); + WebhookRuleResponse createWebhookRule(CreateWebhookRuleCmd cmd) throws CloudRuntimeException; + boolean deleteWebhookRule(DeleteWebhookRuleCmd cmd) throws CloudRuntimeException; + WebhookRuleResponse updateWebhookRule(UpdateWebhookRuleCmd cmd) throws CloudRuntimeException; + WebhookRuleResponse createWebhookRuleResponse(long webhookRuleId); +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java new file mode 100644 index 000000000000..4af93873a1ab --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java @@ -0,0 +1,214 @@ +// 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.mom.webhook; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.mom.webhook.api.command.user.CreateWebhookRuleCmd; +import org.apache.cloudstack.mom.webhook.api.command.user.DeleteWebhookRuleCmd; +import org.apache.cloudstack.mom.webhook.api.command.user.ListWebhookRulesCmd; +import org.apache.cloudstack.mom.webhook.api.command.user.UpdateWebhookRuleCmd; +import org.apache.cloudstack.mom.webhook.api.response.WebhookRuleResponse; +import org.apache.cloudstack.mom.webhook.dao.WebhookRuleDao; +import org.apache.cloudstack.mom.webhook.vo.WebhookRuleVO; +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; + +import com.cloud.api.ApiDBUtils; +import com.cloud.domain.Domain; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.projects.Project; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.utils.Ternary; +import com.cloud.utils.UriUtils; +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.utils.exception.CloudRuntimeException; +import com.cloud.utils.rest.HttpConstants; + +public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiService { + public static final Logger LOGGER = Logger.getLogger(WebhookApiServiceImpl.class.getName()); + + @Inject + AccountManager accountManager; + @Inject + WebhookRuleDao webhookRuleDao; + + protected WebhookRuleResponse createWebhookRuleResponse(WebhookRuleVO webhookRuleVO) { + WebhookRuleResponse response = new WebhookRuleResponse(); + response.setObjectName("webhook"); + response.setId(webhookRuleVO.getUuid()); + response.setName(webhookRuleVO.getName()); + response.setDescription(webhookRuleVO.getDescription()); + Account account = ApiDBUtils.findAccountById(webhookRuleVO.getAccountId()); + if (account.getType() == Account.Type.PROJECT) { + Project project = ApiDBUtils.findProjectByProjectAccountId(account.getId()); + response.setProjectId(project.getUuid()); + response.setProjectName(project.getName()); + } else { + response.setAccountName(account.getAccountName()); + } + Domain domain = ApiDBUtils.findDomainById(webhookRuleVO.getDomainId()); + response.setDomainId(domain.getUuid()); + response.setDomainName(domain.getName()); + response.setState(webhookRuleVO.getState().toString()); + response.setPayloadUrl(webhookRuleVO.getPayloadUrl()); + response.setSecretKey(webhookRuleVO.getSecretKey()); + response.setSslVerification(webhookRuleVO.isSslVerification()); + response.setScope(webhookRuleVO.getScope().toString()); + response.setCreated(webhookRuleVO.getCreated()); + return response; + } + + /** + * @param cmd + * @return Account + */ + protected Account getOwner(final CreateWebhookRuleCmd cmd) { + final Account caller = CallContext.current().getCallingAccount(); + return accountManager.finalizeOwner(caller, cmd.getAccountName(), cmd.getDomainId(), cmd.getProjectId()); + } + + @Override + public ListResponse listWebhookRules(ListWebhookRulesCmd cmd) { + final CallContext ctx = CallContext.current(); + final Account caller = ctx.getCallingAccount(); + final Long clusterId = cmd.getId(); + final String state = cmd.getState(); + final String name = cmd.getName(); + final String keyword = cmd.getKeyword(); + List responsesList = new ArrayList<>(); + List permittedAccounts = new ArrayList<>(); + Ternary domainIdRecursiveListProject = new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null); + accountManager.buildACLSearchParameters(caller, clusterId, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); + Long domainId = domainIdRecursiveListProject.first(); + Boolean isRecursive = domainIdRecursiveListProject.second(); + Project.ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); + + + Filter searchFilter = new Filter(WebhookRuleVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal()); + SearchBuilder sb = webhookRuleDao.createSearchBuilder(); + accountManager.buildACLSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + 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("state", sb.entity().getState(), SearchCriteria.Op.IN); + SearchCriteria sc = sb.create(); + accountManager.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + if (state != null) { + sc.setParameters("state", state); + } + if(keyword != null){ + sc.setParameters("keyword", "%" + keyword + "%"); + } + if (clusterId != null) { + sc.setParameters("id", clusterId); + } + if (name != null) { + sc.setParameters("name", name); + } + List rules = webhookRuleDao.search(sc, searchFilter); + for (WebhookRuleVO rule : rules) { + WebhookRuleResponse response = createWebhookRuleResponse(rule); + responsesList.add(response); + } + ListResponse response = new ListResponse<>(); + response.setResponses(responsesList); + return response; + } + + @Override + public WebhookRuleResponse createWebhookRule(CreateWebhookRuleCmd cmd) throws CloudRuntimeException { + final Account owner = getOwner(cmd); + final String name = cmd.getName(); + final String description = cmd.getDescription(); + final String payloadUrl = cmd.getPayloadUrl(); + final String secretKey = cmd.getSecretKey(); + final boolean sslVerification = cmd.isSslVerification(); + final String scopeStr = cmd.getScope(); + final boolean isAdmin = accountManager.isAdmin(owner.getId()); + final String stateStr = cmd.getState(); + WebhookRule.Scope scope = isAdmin ? WebhookRule.Scope.Global : WebhookRule.Scope.Local; + if (StringUtils.isNotEmpty(scopeStr)) { + try { + scope = WebhookRule.Scope.valueOf(scopeStr); + } catch (IllegalArgumentException iae) { + throw new InvalidParameterValueException("Invalid scope specified"); + } + } + if ((WebhookRule.Scope.Global.equals(scope) && !Account.Type.ADMIN.equals(owner.getType())) || + (WebhookRule.Scope.Domain.equals(scope) && + !List.of(Account.Type.ADMIN, Account.Type.DOMAIN_ADMIN).contains(owner.getType()))) { + throw new InvalidParameterValueException(String.format("Scope %s can not be specified for owner %s", scope, owner.getName())); + } + WebhookRule.State state = WebhookRule.State.Enabled; + if (StringUtils.isNotEmpty(stateStr)) { + try { + state = WebhookRule.State.valueOf(stateStr); + } catch (IllegalArgumentException iae) { + throw new InvalidParameterValueException("Invalid state specified"); + } + } + UriUtils.validateUrl(payloadUrl); + URI uri = URI.create(payloadUrl); + if (sslVerification && !HttpConstants.HTTPS.equalsIgnoreCase(uri.getScheme())) { + throw new InvalidParameterValueException(String.format("SSL verification can be specified only for HTTPS URLs, %s", payloadUrl)); + } + WebhookRuleVO rule = new WebhookRuleVO(name, description, state, owner.getDomainId(), + owner.getId(), payloadUrl, secretKey, sslVerification, scope); + rule = webhookRuleDao.persist(rule); + return createWebhookRuleResponse(rule); + } + + @Override + public boolean deleteWebhookRule(DeleteWebhookRuleCmd cmd) throws CloudRuntimeException { + final long id = cmd.getId(); + // check access + return webhookRuleDao.remove(id); + } + + @Override + public WebhookRuleResponse updateWebhookRule(UpdateWebhookRuleCmd cmd) throws CloudRuntimeException { + return null; + } + + @Override + public WebhookRuleResponse createWebhookRuleResponse(long webhookRuleId) { + WebhookRuleVO webhookRuleVO = webhookRuleDao.findById(webhookRuleId); + return createWebhookRuleResponse(webhookRuleVO); + } + + @Override + public List> getCommands() { + List> cmdList = new ArrayList<>(); + cmdList.add(CreateWebhookRuleCmd.class); + cmdList.add(ListWebhookRulesCmd.class); + cmdList.add(UpdateWebhookRuleCmd.class); + cmdList.add(DeleteWebhookRuleCmd.class); + return cmdList; + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatchThread.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatchThread.java new file mode 100644 index 000000000000..58257825845f --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatchThread.java @@ -0,0 +1,104 @@ +// 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.mom.webhook; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +import org.apache.cloudstack.framework.events.Event; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.log4j.Logger; + +import com.google.gson.Gson; + +public class WebhookDispatchThread implements Runnable { + private static final Logger LOGGER = Logger.getLogger(WebhookDispatchThread.class); + + private final CloseableHttpClient httpClient; + private WebhookRule rule; + private Event event; + private int dispatchRetries = 3; + private int deliveryTimeout = 10; + + public WebhookDispatchThread(CloseableHttpClient httpClient, WebhookRule rule, Event event) { + this.httpClient = httpClient; + this.rule = rule; + this.event = event; + } + + public void setDispatchRetries(int dispatchRetries) { + this.dispatchRetries = dispatchRetries; + } + + public void setDeliveryTimeout(int deliveryTimeout) { + this.deliveryTimeout = deliveryTimeout; + } + + @Override + public void run() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format("Dispatching event: %s for webhook: %s", event.getEventType(), rule.getName())); + } + int attempt = 0; + while (attempt < dispatchRetries) { + attempt++; + if (dispatch(attempt)) { + break; + } + } + } + + private boolean dispatch(int attempt) { + try { + final URI uri = new URI(rule.getPayloadUrl()); + HttpPost request = new HttpPost(); + RequestConfig.Builder requestConfig = RequestConfig.custom(); + requestConfig.setConnectTimeout(deliveryTimeout * 1000); + requestConfig.setConnectionRequestTimeout(deliveryTimeout * 1000); + requestConfig.setSocketTimeout(deliveryTimeout * 1000); + request.setConfig(requestConfig.build()); + request.setURI(uri); + if (LOGGER.isTraceEnabled()) { + LOGGER.debug(String.format("Dispatching event: %s for webhook: %s on URL: %s with timeout: %d, attempt #%d", event.getEventType(), rule.getName(), rule.getPayloadUrl(), deliveryTimeout, attempt)); + } + if (event != null) { + Gson gson = new Gson(); + String js = gson.toJson(event); + StringEntity input = new StringEntity(js, ContentType.APPLICATION_JSON); + request.setEntity(input); + } + final CloseableHttpResponse response = httpClient.execute(request); + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + if (LOGGER.isTraceEnabled()) { + LOGGER.debug(String.format("Successfully dispatched event: %s for webhook: %s", event.getEventType(), rule.getName())); + } + return true; + } + } catch (URISyntaxException | IOException e) { + LOGGER.warn(String.format("Failed to dispatch webhook: %s having URL: %s", rule.getName(), rule.getPayloadUrl())); + } + return false; + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookEventBus.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookEventBus.java new file mode 100644 index 000000000000..0c0b6e1c5226 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookEventBus.java @@ -0,0 +1,87 @@ +/* + * 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.mom.webhook; + +import java.util.Map; +import java.util.UUID; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.framework.events.Event; +import org.apache.cloudstack.framework.events.EventBus; +import org.apache.cloudstack.framework.events.EventBusException; +import org.apache.cloudstack.framework.events.EventSubscriber; +import org.apache.cloudstack.framework.events.EventTopic; +import org.apache.log4j.Logger; + +import com.cloud.utils.component.ManagerBase; +import com.google.gson.Gson; + +public class WebhookEventBus extends ManagerBase implements EventBus { + + private static final Logger LOGGER = Logger.getLogger(WebhookEventBus.class); + private static Gson gson; + + @Inject + WebhookService webhookService; + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + _name = name; + return true; + } + + @Override + public void setName(String name) { + _name = name; + } + + @Override + public UUID subscribe(EventTopic topic, EventSubscriber subscriber) throws EventBusException { + /* NOOP */ + return UUID.randomUUID(); + } + + @Override + public void unsubscribe(UUID subscriberId, EventSubscriber subscriber) throws EventBusException { + /* NOOP */ + } + + @Override + public void publish(Event event) throws EventBusException { + webhookService.handleEvent(event); + } + + @Override + public String getName() { + return _name; + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookRule.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookRule.java new file mode 100644 index 000000000000..d19fc24dfef2 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookRule.java @@ -0,0 +1,46 @@ +// 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.mom.webhook; + +import java.util.Date; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface WebhookRule extends ControlledEntity, Identity, InternalIdentity { + enum State { + Enabled, Disabled; + }; + + enum Scope { + Local, Domain, Global; + }; + + long getId(); + String getName(); + String getDescription(); + State getState(); + long getDomainId(); + long getAccountId(); + String getPayloadUrl(); + String getSecretKey(); + boolean isSslVerification(); + Scope getScope(); + Date getCreated(); +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java new file mode 100644 index 000000000000..ea382c02923d --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java @@ -0,0 +1,44 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.mom.webhook; + +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.framework.events.Event; + +import com.cloud.utils.component.PluggableService; + +public interface WebhookService extends PluggableService, Configurable { + + ConfigKey WebhookDeliveryTimeout = new ConfigKey<>("Advanced", Integer.class, + "webhook.delivery.timeout", "10", + "Wait timeout (in seconds) for a webhook delivery dispatch", + true, ConfigKey.Scope.Domain); + + ConfigKey WebhookDispatchRetries = new ConfigKey<>("Advanced", Integer.class, + "webhook.delivery.retries", "1", + "Number of tries to be made for a webhook dispatch", + true, ConfigKey.Scope.Domain); + + ConfigKey WebhookDispatcherThreadPoolSize = new ConfigKey<>("Advanced", Integer.class, + "webhook.dispatch.thread.pool.size", "5", + "Size of thread pool for webhook dispatchers", + false, ConfigKey.Scope.Global); + + void handleEvent(Event event); +} 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 new file mode 100644 index 000000000000..3b6a1ee364f7 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookServiceImpl.java @@ -0,0 +1,105 @@ +// 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.mom.webhook; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.events.Event; +import org.apache.cloudstack.mom.webhook.dao.WebhookRuleDao; +import org.apache.cloudstack.mom.webhook.vo.WebhookRuleVO; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; + +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.concurrency.NamedThreadFactory; + +public class WebhookServiceImpl extends ManagerBase implements WebhookService { + public static final String WEBHOOK_JOB_POOL_THREAD_PREFIX = "Webhook-Job-Executor"; + private ExecutorService webhookJobExecutor; + private CloseableHttpClient closeableHttpClient; + + @Inject + WebhookRuleDao webhookRuleDao; + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + try { + webhookJobExecutor = Executors.newFixedThreadPool(WebhookDispatcherThreadPoolSize.value(), new NamedThreadFactory(WEBHOOK_JOB_POOL_THREAD_PREFIX)); + closeableHttpClient = HttpClients.createDefault(); + } catch (final Exception e) { + throw new ConfigurationException("Unable to to configure WebhookServiceImpl"); + } + return true; + } + + @Override + public boolean stop() { + webhookJobExecutor.shutdown(); + return true; + } + + @Override + public String getConfigComponentName() { + return WebhookService.class.getName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{ + WebhookDeliveryTimeout, + WebhookDispatchRetries, + WebhookDispatcherThreadPoolSize + }; + } + + @Override + public void handleEvent(Event event) { + List jobs = getDispatchJobs(event); + for(Runnable job : jobs) { + webhookJobExecutor.submit(job); + } + } + + @Override + public List> getCommands() { + return null; + } + + protected List getDispatchJobs(Event event) { + List rules = webhookRuleDao.listAll(); + // All global rules + // All domain level rules for current and parent domains + // All local rules + List jobs = new ArrayList<>(); + for (WebhookRuleVO rule : rules) { + WebhookDispatchThread job = new WebhookDispatchThread(closeableHttpClient, rule, event); + job.setDispatchRetries(WebhookDispatchRetries.valueIn(rule.getDomainId())); + job.setDeliveryTimeout(WebhookDeliveryTimeout.valueIn(rule.getDomainId())); + jobs.add(job); + } + return jobs; + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/CreateWebhookRuleCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/CreateWebhookRuleCmd.java new file mode 100644 index 000000000000..2881ffc65753 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/CreateWebhookRuleCmd.java @@ -0,0 +1,168 @@ +// 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.mom.webhook.api.command.user; + + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; +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.ResponseObject; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ProjectResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.mom.webhook.WebhookApiService; +import org.apache.cloudstack.mom.webhook.WebhookRule; +import org.apache.cloudstack.mom.webhook.api.response.WebhookRuleResponse; + +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = "createWebhook", + description = "Creates a Webhook rule", + responseObject = WebhookRuleResponse.class, + responseView = ResponseObject.ResponseView.Restricted, + entityType = {WebhookRule.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "4.20.0") +public class CreateWebhookRuleCmd extends BaseCmd { + + @Inject + WebhookApiService webhookApiService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NAME, type = BaseCmd.CommandType.STRING, required = true, description = "Name for the Webhook rule") + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, type = BaseCmd.CommandType.STRING, description = "Description for the Webhook rule") + private String description; + + @Parameter(name = ApiConstants.STATE, type = BaseCmd.CommandType.STRING, description = "State of the Webhook rule") + private String state; + + @ACL(accessType = SecurityChecker.AccessType.UseEntry) + @Parameter(name = ApiConstants.ACCOUNT, type = BaseCmd.CommandType.STRING, description = "An optional account for the" + + " Webhook rule. Must be used with domainId.") + private String accountName; + + @ACL(accessType = SecurityChecker.AccessType.UseEntry) + @Parameter(name = ApiConstants.DOMAIN_ID, type = BaseCmd.CommandType.UUID, entityType = DomainResponse.class, + description = "an optional domainId for the Webhook rule. If the account parameter is used, domainId must also be used.") + private Long domainId; + + @ACL(accessType = SecurityChecker.AccessType.UseEntry) + @Parameter(name = ApiConstants.PROJECT_ID, type = BaseCmd.CommandType.UUID, entityType = ProjectResponse.class, + description = "Project for the Webhook rule") + private Long projectId; + + @Parameter(name = ApiConstants.PAYLOAD_URL, + type = BaseCmd.CommandType.STRING, + required = true, + description = "Payload URL of the Webhook rule") + private String payloadUrl; + + @Parameter(name = ApiConstants.SECRET_KEY, type = BaseCmd.CommandType.STRING, description = "Secret key of the Webhook rule") + private String secretKey; + + @Parameter(name = ApiConstants.SSL_VERIFICATION, type = BaseCmd.CommandType.BOOLEAN, description = "If set to true then SSL verification will be done for the Webhook rule otherwise not") + private Boolean sslVerification; + + @Parameter(name = ApiConstants.SCOPE, type = BaseCmd.CommandType.STRING, description = "Scope of the Webhook rule", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin}) + private String scope; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getState() { + return state; + } + + public String getAccountName() { + return accountName; + } + + public Long getDomainId() { + return domainId; + } + + public Long getProjectId() { + return projectId; + } + + public String getPayloadUrl() { + return payloadUrl; + } + + public String getSecretKey() { + return secretKey; + } + + public boolean isSslVerification() { + return Boolean.TRUE.equals(sslVerification); + } + + public String getScope() { + return scope; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccountId(); + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ServerApiException { + try { + WebhookRuleResponse response = webhookApiService.createWebhookRule(this); + if (response == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create webhook rule"); + } + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (CloudRuntimeException ex) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage()); + } + + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookRuleCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookRuleCmd.java new file mode 100644 index 000000000000..600b35cb0ce9 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookRuleCmd.java @@ -0,0 +1,85 @@ +// 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.mom.webhook.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.mom.webhook.WebhookApiService; +import org.apache.cloudstack.mom.webhook.WebhookRule; +import org.apache.cloudstack.mom.webhook.api.response.WebhookRuleResponse; +import org.apache.log4j.Logger; + +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = "deleteWebhookRule", + description = "Delete a Webhook rule", + responseObject = SuccessResponse.class, + entityType = {WebhookRule.class}, + authorized = {RoleType.Admin}) +public class DeleteWebhookRuleCmd extends BaseCmd { + public static final Logger LOGGER = Logger.getLogger(DeleteWebhookRuleCmd.class.getName()); + + @Inject + WebhookApiService webhookApiService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, + entityType = WebhookRuleResponse.class, + description = "The ID of the Webhook rule", + required = true) + private Long id; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + public Long getId() { + return id; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccountId(); + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + @Override + public void execute() throws ServerApiException { + try { + if (!webhookApiService.deleteWebhookRule(this)) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to delete webhook rule ID: %d", getId())); + } + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } catch (CloudRuntimeException ex) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage()); + } + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookRulesCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookRulesCmd.java new file mode 100644 index 000000000000..a079ba418817 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookRulesCmd.java @@ -0,0 +1,84 @@ +// 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.mom.webhook.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.BaseListProjectAndAccountResourcesCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.mom.webhook.WebhookApiService; +import org.apache.cloudstack.mom.webhook.WebhookRule; +import org.apache.cloudstack.mom.webhook.api.response.WebhookRuleResponse; + +@APICommand(name = "listWebhooks", + description = "Lists Webhook rules", + responseObject = WebhookRuleResponse.class, + responseView = ResponseObject.ResponseView.Restricted, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + entityType = {WebhookRule.class}) +public class ListWebhookRulesCmd extends BaseListProjectAndAccountResourcesCmd { + + @Inject + WebhookApiService webhookApiService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, + entityType = WebhookRuleResponse.class, + description = "The ID of the Webhook rules") + private Long id; + + @Parameter(name = ApiConstants.STATE, type = CommandType.STRING, description = "The state of the Webhook rule") + private String state; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "The name of the Webhook rule") + private String name; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + public Long getId() { + return id; + } + + public String getState() { + return state; + } + + public String getName() { + return name; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + @Override + public void execute() throws ServerApiException { + ListResponse response = webhookApiService.listWebhookRules(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookRuleCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookRuleCmd.java new file mode 100644 index 000000000000..4b55eb7ae77e --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookRuleCmd.java @@ -0,0 +1,84 @@ +// 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.mom.webhook.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.mom.webhook.WebhookApiService; +import org.apache.cloudstack.mom.webhook.WebhookRule; +import org.apache.cloudstack.mom.webhook.api.response.WebhookRuleResponse; + +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = "updateWebhookRule", + description = "Update a Webhook rule", + responseObject = SuccessResponse.class, + entityType = {WebhookRule.class}, + authorized = {RoleType.Admin}) +public class UpdateWebhookRuleCmd extends BaseCmd { + + @Inject + WebhookApiService webhookApiService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, + entityType = WebhookRuleResponse.class, + description = "The ID of the Webhook rule", + required = true) + private Long id; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + public Long getId() { + return id; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccountId(); + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + @Override + public void execute() throws ServerApiException { + try { + WebhookRuleResponse response = webhookApiService.updateWebhookRule(this); + if (response == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update Webhook rule"); + } + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (CloudRuntimeException ex) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage()); + } + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookRuleResponse.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookRuleResponse.java new file mode 100644 index 000000000000..926b3a30a3e8 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookRuleResponse.java @@ -0,0 +1,148 @@ +// 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.mom.webhook.api.response; + +import java.util.Date; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponseWithAnnotations; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.api.response.ControlledEntityResponse; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = {WebhookRuleResponse.class}) +public class WebhookRuleResponse extends BaseResponseWithAnnotations implements ControlledEntityResponse { + @SerializedName(ApiConstants.ID) + @Param(description = "The id of the Webhook rule") + private String id; + + @SerializedName(ApiConstants.NAME) + @Param(description = "The name of the Webhook rule") + private String name; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "The description of the Webhook rule") + private String description; + + @SerializedName(ApiConstants.STATE) + @Param(description = "The state of the Webhook rule") + private String state; + + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "The ID of the domain in which the Webhook rule exists") + private String domainId; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "The name of the domain in which the Webhook rule exists") + private String domainName; + + @SerializedName(ApiConstants.ACCOUNT) + @Param(description = "The account associated with the Webhook rule") + private String accountName; + + @SerializedName(ApiConstants.PROJECT_ID) + @Param(description = "the project id of the Kubernetes cluster") + private String projectId; + + @SerializedName(ApiConstants.PROJECT) + @Param(description = "the project name of the Kubernetes cluster") + private String projectName; + + @SerializedName(ApiConstants.PAYLOAD_URL) + @Param(description = "The payload URL end point for the Webhook rule") + private String payloadUrl; + + @SerializedName(ApiConstants.SECRET_KEY) + @Param(description = "The secret key for the Webhook rule") + private String secretKey; + + @SerializedName(ApiConstants.SSL_VERIFICATION) + @Param(description = "Whether SSL verification is enabled for the Webhook rule") + private boolean sslVerification; + + @SerializedName(ApiConstants.SCOPE) + @Param(description = "The scope of the Webhook rule") + private String scope; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "The date when this Webhook rule was created") + private Date created; + + public void setId(String id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setState(String state) { + this.state = state; + } + + @Override + public void setDomainId(String domainId) { + this.domainId = domainId; + } + + @Override + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + @Override + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + @Override + public void setProjectId(String projectId) { + this.projectId = projectId; + } + + @Override + public void setProjectName(String projectName) { + this.projectName = projectName; + } + + public void setPayloadUrl(String payloadUrl) { + this.payloadUrl = payloadUrl; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public void setSslVerification(boolean sslVerification) { + this.sslVerification = sslVerification; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public void setCreated(Date created) { + this.created = created; + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookRuleDao.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookRuleDao.java new file mode 100644 index 000000000000..a4751f12cbd0 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookRuleDao.java @@ -0,0 +1,25 @@ +// 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.mom.webhook.dao; + +import org.apache.cloudstack.mom.webhook.vo.WebhookRuleVO; + +import com.cloud.utils.db.GenericDao; + +public interface WebhookRuleDao extends GenericDao { +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookRuleDaoImpl.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookRuleDaoImpl.java new file mode 100644 index 000000000000..3ebfc191e858 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookRuleDaoImpl.java @@ -0,0 +1,25 @@ +// 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.mom.webhook.dao; + +import org.apache.cloudstack.mom.webhook.vo.WebhookRuleVO; + +import com.cloud.utils.db.GenericDaoBase; + +public class WebhookRuleDaoImpl extends GenericDaoBase implements WebhookRuleDao { +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookRuleVO.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookRuleVO.java new file mode 100644 index 000000000000..f41ba76d8f73 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookRuleVO.java @@ -0,0 +1,213 @@ +// 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.mom.webhook.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 org.apache.cloudstack.mom.webhook.WebhookRule; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import com.cloud.utils.db.Encrypt; +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name = "webhook") +public class WebhookRuleVO implements WebhookRule { + + @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 = "description", length = 4096) + private String description; + + @Column(name = "state") + @Enumerated(value = EnumType.STRING) + private State state; + + @Column(name = "domain_id") + private long domainId; + + @Column(name = "account_id") + private long accountId; + + @Column(name = "payload_url") + private String payloadUrl; + + @Column(name = "secret_key") + @Encrypt + private String secretKey; + + @Column(name = "ssl_verification") + private boolean sslVerification; + + @Column(name = "scope") + @Enumerated(value = EnumType.STRING) + private Scope scope; + + @Column(name = GenericDao.CREATED_COLUMN) + private Date created; + + @Column(name = GenericDao.REMOVED_COLUMN) + private Date removed; + + @Override + public long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + } + + @Override + public long getDomainId() { + return domainId; + } + + public void setDomainId(long domainId) { + this.domainId = domainId; + } + + @Override + public long getAccountId() { + return accountId; + } + + public void setAccountId(long accountId) { + this.accountId = accountId; + } + + @Override + public String getPayloadUrl() { + return payloadUrl; + } + + public void setPayloadUrl(String payloadUrl) { + this.payloadUrl = payloadUrl; + } + + @Override + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + @Override + public Scope getScope() { + return scope; + } + + @Override + public boolean isSslVerification() { + return sslVerification; + } + + public void setSslVerification(boolean sslVerification) { + this.sslVerification = sslVerification; + } + + public void setScope(Scope scope) { + this.scope = scope; + } + + @Override + public Date getCreated() { + return created; + } + + public Date getRemoved() { + return removed; + } + + @Override + public Class getEntityType() { + return WebhookRule.class; + } + + @Override + public String toString() { + return String.format("WebhookRule [%s]", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "uuid", "name")); + } + + public WebhookRuleVO() { + this.uuid = UUID.randomUUID().toString(); + } + + public WebhookRuleVO(String name, String description, State state, long domainId, long accountId, + String payloadUrl, String secretKey, boolean sslVerification, Scope scope) { + this.uuid = UUID.randomUUID().toString(); + this.name = name; + this.description = description; + this.state = state; + this.domainId = domainId; + this.accountId = accountId; + this.payloadUrl = payloadUrl; + this.secretKey = secretKey; + this.sslVerification = sslVerification; + this.scope = scope; + } +} diff --git a/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/module.properties b/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/module.properties new file mode 100644 index 000000000000..7f7a61540381 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/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=webhook +parent=core diff --git a/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/spring-webhook-context.xml b/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/spring-webhook-context.xml new file mode 100644 index 000000000000..0248a9648a61 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/spring-webhook-context.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + diff --git a/plugins/pom.xml b/plugins/pom.xml index cbfba8f82177..c7db20801b5d 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -79,6 +79,7 @@ event-bus/inmemory event-bus/kafka event-bus/rabbitmq + event-bus/webhook ha-planners/skip-heurestics diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index b971d2449417..b631c00f6f15 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -271,7 +271,9 @@ 'deleteBucket': 'Object Store', 'listBuckets': 'Object Store', 'listVmsForImport': 'Virtual Machine', - 'importVm': 'Virtual Machine' + 'importVm': 'Virtual Machine', + 'Webhook': 'Webhook', + 'Webhooks': 'Webhook' } From 844235086d7cf0d6b815394cd4d1154670e0a59f Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 12 Feb 2024 16:18:37 +0530 Subject: [PATCH 02/75] fix Signed-off-by: Abhishek Kumar --- plugins/event-bus/webhook/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/event-bus/webhook/pom.xml b/plugins/event-bus/webhook/pom.xml index 72fa785740b9..87a39ec73d92 100644 --- a/plugins/event-bus/webhook/pom.xml +++ b/plugins/event-bus/webhook/pom.xml @@ -24,7 +24,7 @@ org.apache.cloudstack cloudstack-plugins - 4.19.0.0-SNAPSHOT + 4.20.0.0-SNAPSHOT ../../pom.xml From a69192ca90996f5057ff7cbdc62f9c9664705dbb Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 13 Feb 2024 12:13:13 +0530 Subject: [PATCH 03/75] fix Signed-off-by: Abhishek Kumar --- .../org/apache/cloudstack/mom/webhook/WebhookServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3b6a1ee364f7..92a3dcc8f107 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 @@ -85,7 +85,7 @@ public void handleEvent(Event event) { @Override public List> getCommands() { - return null; + return new ArrayList<>(); } protected List getDispatchJobs(Event event) { From af388a0f1edcfb525abed350b2a81b7e1e12dd69 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 13 Feb 2024 14:10:40 +0530 Subject: [PATCH 04/75] changes Signed-off-by: Abhishek Kumar --- .../mom/webhook/WebhookApiServiceImpl.java | 77 ++++++++++++++++++- .../mom/webhook/WebhookServiceImpl.java | 12 ++- .../api/command/user/ListWebhookRulesCmd.java | 2 +- .../command/user/UpdateWebhookRuleCmd.java | 51 ++++++++++++ .../api/response/WebhookRuleResponse.java | 3 +- 5 files changed, 139 insertions(+), 6 deletions(-) diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java index 4af93873a1ab..d9510f4c6ebf 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java @@ -23,6 +23,7 @@ import javax.inject.Inject; +import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.mom.webhook.api.command.user.CreateWebhookRuleCmd; @@ -186,14 +187,86 @@ public WebhookRuleResponse createWebhookRule(CreateWebhookRuleCmd cmd) throws Cl @Override public boolean deleteWebhookRule(DeleteWebhookRuleCmd cmd) throws CloudRuntimeException { + final Account caller = CallContext.current().getCallingAccount(); final long id = cmd.getId(); - // check access + WebhookRule rule = webhookRuleDao.findById(id); + if (rule == null) { + throw new InvalidParameterValueException("Unable to find the webhook rule with the specified ID"); + } + accountManager.checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, rule); return webhookRuleDao.remove(id); } @Override public WebhookRuleResponse updateWebhookRule(UpdateWebhookRuleCmd cmd) throws CloudRuntimeException { - return null; + final Account caller = CallContext.current().getCallingAccount(); + final long id = cmd.getId(); + final String name = cmd.getName(); + final String description = cmd.getDescription(); + final String payloadUrl = cmd.getPayloadUrl(); + final String secretKey = cmd.getSecretKey(); + final Boolean sslVerification = cmd.isSslVerification(); + final String scopeStr = cmd.getScope(); + final String stateStr = cmd.getState(); + WebhookRuleVO rule = webhookRuleDao.findById(id); + if (rule == null) { + throw new InvalidParameterValueException("Unable to find the webhook rule with the specified ID"); + } + accountManager.checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, rule); + boolean updateNeeded = false; + if (StringUtils.isNotBlank(name)) { + rule.setName(name); + updateNeeded = true; + } + if (description != null) { + rule.setDescription(description); + updateNeeded = true; + } + if (StringUtils.isNotEmpty(stateStr)) { + try { + WebhookRule.State state = WebhookRule.State.valueOf(stateStr); + rule.setState(state); + updateNeeded = true; + } catch (IllegalArgumentException iae) { + throw new InvalidParameterValueException("Invalid state specified"); + } + } + if (StringUtils.isNotEmpty(scopeStr)) { + try { + WebhookRule.Scope scope = WebhookRule.Scope.valueOf(scopeStr); + Account owner = accountManager.getAccount(rule.getAccountId()); + if ((WebhookRule.Scope.Global.equals(scope) && !Account.Type.ADMIN.equals(owner.getType())) || + (WebhookRule.Scope.Domain.equals(scope) && + !List.of(Account.Type.ADMIN, Account.Type.DOMAIN_ADMIN).contains(owner.getType()))) { + throw new InvalidParameterValueException(String.format("Scope %s can not be specified for owner %s", scope, owner.getName())); + } + rule.setScope(scope); + updateNeeded = true; + } catch (IllegalArgumentException iae) { + throw new InvalidParameterValueException("Invalid scope specified"); + } + } + URI uri = URI.create(rule.getPayloadUrl()); + if (StringUtils.isNotEmpty(payloadUrl)) { + UriUtils.validateUrl(payloadUrl); + uri = URI.create(payloadUrl); + rule.setPayloadUrl(payloadUrl); + updateNeeded = true; + } + if (sslVerification != null) { + if (Boolean.TRUE.equals(sslVerification) && !HttpConstants.HTTPS.equalsIgnoreCase(uri.getScheme())) { + throw new InvalidParameterValueException(String.format("SSL verification can be specified only for HTTPS URLs, %s", payloadUrl)); + } + updateNeeded = true; + } + if (secretKey != null) { + rule.setSecretKey(secretKey); + updateNeeded = true; + } + if (updateNeeded && !webhookRuleDao.update(id, rule)) { + return null; + } + return createWebhookRuleResponse(rule); } @Override 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 92a3dcc8f107..1be65cc65b37 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 @@ -18,6 +18,7 @@ package org.apache.cloudstack.mom.webhook; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; @@ -33,6 +34,7 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; +import com.cloud.utils.Pair; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.concurrency.NamedThreadFactory; @@ -93,11 +95,17 @@ protected List getDispatchJobs(Event event) { // All global rules // All domain level rules for current and parent domains // All local rules + Map> domainConfigs = new HashMap<>(); List jobs = new ArrayList<>(); for (WebhookRuleVO rule : rules) { + if (!domainConfigs.containsKey(rule.getDomainId())) { + domainConfigs.put(rule.getDomainId(), new Pair<>(WebhookDispatchRetries.valueIn(rule.getDomainId()), + WebhookDeliveryTimeout.valueIn(rule.getDomainId()))); + } + Pair configs = domainConfigs.get(rule.getDomainId()); WebhookDispatchThread job = new WebhookDispatchThread(closeableHttpClient, rule, event); - job.setDispatchRetries(WebhookDispatchRetries.valueIn(rule.getDomainId())); - job.setDeliveryTimeout(WebhookDeliveryTimeout.valueIn(rule.getDomainId())); + job.setDispatchRetries(configs.first()); + job.setDeliveryTimeout(configs.second()); jobs.add(job); } return jobs; diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookRulesCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookRulesCmd.java index a079ba418817..b62d61bf0750 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookRulesCmd.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookRulesCmd.java @@ -32,7 +32,7 @@ import org.apache.cloudstack.mom.webhook.WebhookRule; import org.apache.cloudstack.mom.webhook.api.response.WebhookRuleResponse; -@APICommand(name = "listWebhooks", +@APICommand(name = "listWebhookRules", description = "Lists Webhook rules", responseObject = WebhookRuleResponse.class, responseView = ResponseObject.ResponseView.Restricted, diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookRuleCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookRuleCmd.java index 4b55eb7ae77e..1b21633f4b32 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookRuleCmd.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookRuleCmd.java @@ -52,6 +52,29 @@ public class UpdateWebhookRuleCmd extends BaseCmd { description = "The ID of the Webhook rule", required = true) private Long id; + @Parameter(name = ApiConstants.NAME, type = BaseCmd.CommandType.STRING, description = "Name for the Webhook rule") + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, type = BaseCmd.CommandType.STRING, description = "Description for the Webhook rule") + private String description; + + @Parameter(name = ApiConstants.STATE, type = BaseCmd.CommandType.STRING, description = "State of the Webhook rule") + private String state; + + @Parameter(name = ApiConstants.PAYLOAD_URL, + type = BaseCmd.CommandType.STRING, + description = "Payload URL of the Webhook rule") + private String payloadUrl; + + @Parameter(name = ApiConstants.SECRET_KEY, type = BaseCmd.CommandType.STRING, description = "Secret key of the Webhook rule") + private String secretKey; + + @Parameter(name = ApiConstants.SSL_VERIFICATION, type = BaseCmd.CommandType.BOOLEAN, description = "If set to true then SSL verification will be done for the Webhook rule otherwise not") + private Boolean sslVerification; + + @Parameter(name = ApiConstants.SCOPE, type = BaseCmd.CommandType.STRING, description = "Scope of the Webhook rule", + authorized = {RoleType.Admin, RoleType.DomainAdmin}) + private String scope; ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// @@ -60,6 +83,34 @@ public Long getId() { return id; } + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getState() { + return state; + } + + public String getPayloadUrl() { + return payloadUrl; + } + + public String getSecretKey() { + return secretKey; + } + + public Boolean isSslVerification() { + return sslVerification; + } + + public String getScope() { + return scope; + } + @Override public long getEntityOwnerId() { return CallContext.current().getCallingAccountId(); diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookRuleResponse.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookRuleResponse.java index 926b3a30a3e8..20fde7109014 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookRuleResponse.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookRuleResponse.java @@ -23,11 +23,12 @@ import org.apache.cloudstack.api.BaseResponseWithAnnotations; import org.apache.cloudstack.api.EntityReference; import org.apache.cloudstack.api.response.ControlledEntityResponse; +import org.apache.cloudstack.mom.webhook.WebhookRule; import com.cloud.serializer.Param; import com.google.gson.annotations.SerializedName; -@EntityReference(value = {WebhookRuleResponse.class}) +@EntityReference(value = {WebhookRule.class}) public class WebhookRuleResponse extends BaseResponseWithAnnotations implements ControlledEntityResponse { @SerializedName(ApiConstants.ID) @Param(description = "The id of the Webhook rule") From d17ded0ae282e8e1f0db6077a9ceb62107416c89 Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Thu, 21 Sep 2023 10:22:46 +0200 Subject: [PATCH 05/75] registry of message busses --- ...-lifecycle-compute-context-inheritable.xml | 5 ++ .../spring-core-registry-core-context.xml | 9 ++- .../cloud/network/NetworkStateListener.java | 69 +++++++++---------- .../cloudstack/framework/events/Event.java | 12 ++-- .../framework/events/EventDistributor.java | 33 +++++++++ .../events/EventDistributorImpl.java | 64 +++++++++++++++++ .../spring-framework-event-core-context.xml | 34 +++++++++ .../mom/inmemory/InMemoryEventBus.java | 11 +++ .../cloudstack/mom/kafka/KafkaEventBus.java | 14 +++- .../mom/rabbitmq/RabbitMQEventBus.java | 11 ++- .../contrail/management/EventUtils.java | 22 +++--- .../main/java/com/cloud/api/ApiServer.java | 46 ++++++------- .../hypervisor/HypervisorGuruManagerImpl.java | 2 +- .../listener/SnapshotStateListener.java | 29 ++++---- .../storage/listener/VolumeStateListener.java | 45 +++++------- .../com/cloud/vm/UserVmStateListener.java | 38 ++++------ .../spring-server-core-managers-context.xml | 4 ++ .../utils/component/ComponentContext.java | 9 ++- 18 files changed, 308 insertions(+), 149 deletions(-) create mode 100644 framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributor.java create mode 100644 framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributorImpl.java create mode 100644 framework/events/src/main/resources/META-INF.cloudstack.core/spring-framework-event-core-context.xml diff --git a/core/src/main/resources/META-INF/cloudstack/compute/spring-core-lifecycle-compute-context-inheritable.xml b/core/src/main/resources/META-INF/cloudstack/compute/spring-core-lifecycle-compute-context-inheritable.xml index fb0e8780ecc7..ef6adab9dd99 100644 --- a/core/src/main/resources/META-INF/cloudstack/compute/spring-core-lifecycle-compute-context-inheritable.xml +++ b/core/src/main/resources/META-INF/cloudstack/compute/spring-core-lifecycle-compute-context-inheritable.xml @@ -39,6 +39,11 @@ + + + + + diff --git a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml index a36d12431551..bfe722fad557 100644 --- a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml +++ b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml @@ -287,11 +287,16 @@ + + + + + class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry"> - + diff --git a/engine/components-api/src/main/java/com/cloud/network/NetworkStateListener.java b/engine/components-api/src/main/java/com/cloud/network/NetworkStateListener.java index 1e1251d8cdcf..1686935c3ac7 100644 --- a/engine/components-api/src/main/java/com/cloud/network/NetworkStateListener.java +++ b/engine/components-api/src/main/java/com/cloud/network/NetworkStateListener.java @@ -24,16 +24,13 @@ import javax.inject.Inject; +import com.cloud.utils.component.ComponentContext; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.framework.events.EventBus; -import org.apache.cloudstack.framework.events.EventBusException; -import org.apache.log4j.Logger; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.apache.cloudstack.framework.events.EventDistributor; import com.cloud.event.EventCategory; import com.cloud.network.Network.Event; import com.cloud.network.Network.State; -import com.cloud.utils.component.ComponentContext; import com.cloud.utils.fsm.StateListener; import com.cloud.utils.fsm.StateMachine2; @@ -42,14 +39,16 @@ public class NetworkStateListener implements StateListener t } private void pubishOnEventBus(String event, String status, Network vo, State oldState, State newState) { - - String configKey = "publish.resource.state.events"; - String value = _configDao.getValue(configKey); - boolean configValue = Boolean.parseBoolean(value); - if(!configValue) - return; - try { - s_eventBus = ComponentContext.getComponent(EventBus.class); - } catch (NoSuchBeanDefinitionException nbe) { - return; // no provider is configured to provide events bus, so just return - } - - String resourceName = getEntityFromClassName(Network.class.getName()); - org.apache.cloudstack.framework.events.Event eventMsg = - new org.apache.cloudstack.framework.events.Event("management-server", EventCategory.RESOURCE_STATE_CHANGE_EVENT.getName(), event, resourceName, vo.getUuid()); - Map eventDescription = new HashMap(); - eventDescription.put("resource", resourceName); - eventDescription.put("id", vo.getUuid()); - eventDescription.put("old-state", oldState.name()); - eventDescription.put("new-state", newState.name()); - - String eventDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").format(new Date()); - eventDescription.put("eventDateTime", eventDate); - - eventMsg.setDescription(eventDescription); - try { - s_eventBus.publish(eventMsg); - } catch (EventBusException e) { - s_logger.warn("Failed to publish state change event on the event bus."); - } + String configKey = "publish.resource.state.events"; + String value = _configDao.getValue(configKey); + boolean configValue = Boolean.parseBoolean(value); + if(!configValue) + return; + if (eventDistributor == null) { + setEventDistributor(ComponentContext.getComponent(EventDistributor.class)); + } + + String resourceName = getEntityFromClassName(Network.class.getName()); + org.apache.cloudstack.framework.events.Event eventMsg = + new org.apache.cloudstack.framework.events.Event("management-server", EventCategory.RESOURCE_STATE_CHANGE_EVENT.getName(), event, resourceName, vo.getUuid()); + Map eventDescription = new HashMap<>(); + eventDescription.put("resource", resourceName); + eventDescription.put("id", vo.getUuid()); + eventDescription.put("old-state", oldState.name()); + eventDescription.put("new-state", newState.name()); + + String eventDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").format(new Date()); + eventDescription.put("eventDateTime", eventDate); + + eventMsg.setDescription(eventDescription); + + eventDistributor.publish(eventMsg); } private String getEntityFromClassName(String entityClassName) { diff --git a/framework/events/src/main/java/org/apache/cloudstack/framework/events/Event.java b/framework/events/src/main/java/org/apache/cloudstack/framework/events/Event.java index 4a3eaf9e68c0..bd1ea2aa5f95 100644 --- a/framework/events/src/main/java/org/apache/cloudstack/framework/events/Event.java +++ b/framework/events/src/main/java/org/apache/cloudstack/framework/events/Event.java @@ -31,11 +31,11 @@ public class Event { String description; public Event(String eventSource, String eventCategory, String eventType, String resourceType, String resourceUUID) { - this.eventCategory = eventCategory; - this.eventType = eventType; - this.eventSource = eventSource; - this.resourceType = resourceType; - this.resourceUUID = resourceUUID; + setEventCategory(eventCategory); + setEventType(eventType); + setEventSource(eventSource); + setResourceType(resourceType); + setResourceUUID(resourceUUID); } public String getEventCategory() { @@ -68,7 +68,7 @@ public String getDescription() { public void setDescription(Object message) { Gson gson = new Gson(); - this.description = gson.toJson(message).toString(); + this.description = gson.toJson(message); } public void setDescription(String description) { diff --git a/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributor.java b/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributor.java new file mode 100644 index 000000000000..4f4775317103 --- /dev/null +++ b/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributor.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.framework.events; + +import com.cloud.utils.component.Manager; + +import java.util.List; + +public interface EventDistributor extends Manager { + /** + * publish an event on to the event busses + * + * @param event event that needs to be published on the event bus + */ + List publish(Event event); +} diff --git a/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributorImpl.java b/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributorImpl.java new file mode 100644 index 000000000000..e92d36b4541f --- /dev/null +++ b/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributorImpl.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.framework.events; + +import com.cloud.utils.component.ManagerBase; +import org.apache.log4j.Logger; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; + +public class EventDistributorImpl extends ManagerBase implements EventDistributor { + private static final Logger LOGGER = Logger.getLogger(EventDistributorImpl.class); + + public void setEventBusses(List eventBusses) { + this.eventBusses = eventBusses; + } + + List eventBusses; + + @PostConstruct + public void init() { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace(String.format("testing %d event busses", eventBusses.size())); + } + publish(new Event("server", "NONE","starting", "server", "NONE")); + } + + @Override + public List publish(Event event) { + LOGGER.info(String.format("publishing %s to %d event busses", (event == null ? "" : event.getDescription()), eventBusses.size())); + List exceptions = new ArrayList<>(); + if (event == null) { + return exceptions; + } + for (EventBus bus : eventBusses) { + try { + bus.publish(event); + } catch (EventBusException e) { + LOGGER.warn(String.format("no publish for bus %s of event %s", bus.getClass().getName(), event.getDescription())); + exceptions.add(e); + } + } + return exceptions; + } + +} diff --git a/framework/events/src/main/resources/META-INF.cloudstack.core/spring-framework-event-core-context.xml b/framework/events/src/main/resources/META-INF.cloudstack.core/spring-framework-event-core-context.xml new file mode 100644 index 000000000000..45eb666cb1cf --- /dev/null +++ b/framework/events/src/main/resources/META-INF.cloudstack.core/spring-framework-event-core-context.xml @@ -0,0 +1,34 @@ + + + + + + diff --git a/plugins/event-bus/inmemory/src/main/java/org/apache/cloudstack/mom/inmemory/InMemoryEventBus.java b/plugins/event-bus/inmemory/src/main/java/org/apache/cloudstack/mom/inmemory/InMemoryEventBus.java index b7d74df980f7..5538a50988d6 100644 --- a/plugins/event-bus/inmemory/src/main/java/org/apache/cloudstack/mom/inmemory/InMemoryEventBus.java +++ b/plugins/event-bus/inmemory/src/main/java/org/apache/cloudstack/mom/inmemory/InMemoryEventBus.java @@ -62,6 +62,10 @@ public UUID subscribe(EventTopic topic, EventSubscriber subscriber) throws Event if (subscriber == null || topic == null) { throw new EventBusException("Invalid EventSubscriber/EventTopic object passed."); } + if (s_logger.isDebugEnabled()) { + s_logger.debug(String.format("subscribing \'%s\' to events of type \'%s\' from \'%s\'",subscriber.toString(), topic.getEventType(), topic.getEventSource())); + } + UUID subscriberId = UUID.randomUUID(); subscribers.put(subscriberId, new Pair(topic, subscriber)); @@ -70,6 +74,9 @@ public UUID subscribe(EventTopic topic, EventSubscriber subscriber) throws Event @Override public void unsubscribe(UUID subscriberId, EventSubscriber subscriber) throws EventBusException { + if (s_logger.isDebugEnabled()) { + s_logger.debug(String.format("unsubscribing \'%s\'",subscriberId)); + } if (subscriberId == null) { throw new EventBusException("Cannot unregister a null subscriberId."); } @@ -87,7 +94,11 @@ public void unsubscribe(UUID subscriberId, EventSubscriber subscriber) throws Ev @Override public void publish(Event event) throws EventBusException { + if (s_logger.isTraceEnabled()) { + s_logger.trace(String.format("publish \'%s\'", event.getDescription())); + } if (subscribers == null || subscribers.isEmpty()) { + s_logger.trace("no subscribers, no publish"); return; // no subscriber to publish to, so just return } diff --git a/plugins/event-bus/kafka/src/main/java/org/apache/cloudstack/mom/kafka/KafkaEventBus.java b/plugins/event-bus/kafka/src/main/java/org/apache/cloudstack/mom/kafka/KafkaEventBus.java index 17a58a5d2326..7d48a391025e 100644 --- a/plugins/event-bus/kafka/src/main/java/org/apache/cloudstack/mom/kafka/KafkaEventBus.java +++ b/plugins/event-bus/kafka/src/main/java/org/apache/cloudstack/mom/kafka/KafkaEventBus.java @@ -89,19 +89,29 @@ public void setName(String name) { @Override public UUID subscribe(EventTopic topic, EventSubscriber subscriber) throws EventBusException { + if (s_logger.isDebugEnabled()) { + s_logger.debug(String.format("subscribing \'%s\' to events of type \'%s\' from \'%s\'",subscriber.toString(), topic.getEventType(), topic.getEventSource())); + } + /* NOOP */ return UUID.randomUUID(); } @Override public void unsubscribe(UUID subscriberId, EventSubscriber subscriber) throws EventBusException { + if (s_logger.isDebugEnabled()) { + s_logger.debug(String.format("unsubscribing \'%s\'",subscriberId)); + } /* NOOP */ } @Override public void publish(Event event) throws EventBusException { - ProducerRecord record = new ProducerRecord(_topic, event.getResourceUUID(), event.getDescription()); - _producer.send(record); + if (s_logger.isTraceEnabled()) { + s_logger.trace(String.format("publish \'%s\'", event.getDescription())); + } + ProducerRecord newRecord = new ProducerRecord<>(_topic, event.getResourceUUID(), event.getDescription()); + _producer.send(newRecord); } @Override diff --git a/plugins/event-bus/rabbitmq/src/main/java/org/apache/cloudstack/mom/rabbitmq/RabbitMQEventBus.java b/plugins/event-bus/rabbitmq/src/main/java/org/apache/cloudstack/mom/rabbitmq/RabbitMQEventBus.java index f54c769908d8..5e5589aca5c6 100644 --- a/plugins/event-bus/rabbitmq/src/main/java/org/apache/cloudstack/mom/rabbitmq/RabbitMQEventBus.java +++ b/plugins/event-bus/rabbitmq/src/main/java/org/apache/cloudstack/mom/rabbitmq/RabbitMQEventBus.java @@ -187,11 +187,14 @@ public static void setRetryInterval(Integer retryInterval) { */ @Override public UUID subscribe(EventTopic topic, EventSubscriber subscriber) throws EventBusException { - if (subscriber == null || topic == null) { throw new EventBusException("Invalid EventSubscriber/EventTopic object passed."); } + if (s_logger.isDebugEnabled()) { + s_logger.debug(String.format("subscribing \'%s\' to events of type \'%s\' from \'%s\'",subscriber.toString(), topic.getEventType(), topic.getEventSource())); + } + // create a UUID, that will be used for managing subscriptions and also used as queue name // for on the queue used for the subscriber on the AMQP broker UUID queueId = UUID.randomUUID(); @@ -252,6 +255,9 @@ public void handleDelivery(String queueName, Envelope envelope, AMQP.BasicProper @Override public void unsubscribe(UUID subscriberId, EventSubscriber subscriber) throws EventBusException { + if (s_logger.isDebugEnabled()) { + s_logger.debug(String.format("unsubscribing \'%s\'",subscriberId)); + } try { String classname = subscriber.getClass().getName(); String queueName = UUID.nameUUIDFromBytes(classname.getBytes()).toString(); @@ -267,6 +273,9 @@ public void unsubscribe(UUID subscriberId, EventSubscriber subscriber) throws Ev // publish event on to the exchange created on AMQP server @Override public void publish(Event event) throws EventBusException { + if (s_logger.isTraceEnabled()) { + s_logger.trace(String.format("publish \'%s\'", event.getDescription())); + } String routingKey = createRoutingKey(event); String eventDescription = event.getDescription(); diff --git a/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/EventUtils.java b/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/EventUtils.java index 78ec01344ca7..d6a286596318 100644 --- a/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/EventUtils.java +++ b/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/EventUtils.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.List; +import org.apache.cloudstack.framework.events.EventDistributor; import org.apache.log4j.Logger; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.stereotype.Component; @@ -42,15 +43,21 @@ import com.cloud.utils.component.ComponentContext; import com.cloud.utils.component.ComponentMethodInterceptor; + @Component public class EventUtils { private static final Logger s_logger = Logger.getLogger(EventUtils.class); + private static EventDistributor eventDistributor; protected static EventBus s_eventBus = null; public EventUtils() { } + public static void setEventDistributor(EventDistributor eventDistributorImpl) { + eventDistributor = eventDistributorImpl; + } + private static void publishOnMessageBus(String eventCategory, String eventType, String details, Event.State state) { if (state != com.cloud.event.Event.State.Completed) { @@ -58,6 +65,7 @@ private static void publishOnMessageBus(String eventCategory, String eventType, } try { + setEventDistributor(ComponentContext.getComponent(EventDistributor.class)); s_eventBus = ComponentContext.getComponent(EventBus.class); } catch (NoSuchBeanDefinitionException nbe) { return; // no provider is configured to provide events bus, so just return @@ -66,18 +74,16 @@ private static void publishOnMessageBus(String eventCategory, String eventType, org.apache.cloudstack.framework.events.Event event = new org.apache.cloudstack.framework.events.Event(ManagementService.Name, eventCategory, eventType, EventTypes.getEntityForEvent(eventType), null); - Map eventDescription = new HashMap(); + Map eventDescription = new HashMap<>(); eventDescription.put("event", eventType); eventDescription.put("status", state.toString()); eventDescription.put("details", details); event.setDescription(eventDescription); - try { - s_eventBus.publish(event); - } catch (EventBusException evx) { - String errMsg = "Failed to publish contrail event."; - s_logger.warn(errMsg, evx); + List exceptions = eventDistributor.publish(event); + for (EventBusException ex : exceptions) { + String errMsg = "Failed to publish event."; + s_logger.warn(errMsg, ex); } - } public static class EventInterceptor implements ComponentMethodInterceptor, MethodInterceptor { @@ -118,7 +124,7 @@ public Object invoke(MethodInvocation invocation) throws Throwable { } protected List getActionEvents(Method m) { - List result = new ArrayList(); + List result = new ArrayList<>(); ActionEvents events = m.getAnnotation(ActionEvents.class); diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java index b602ed2edbc1..0310fa3881bf 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -95,8 +95,8 @@ import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; -import org.apache.cloudstack.framework.events.EventBus; import org.apache.cloudstack.framework.events.EventBusException; +import org.apache.cloudstack.framework.events.EventDistributor; import org.apache.cloudstack.framework.jobs.AsyncJob; import org.apache.cloudstack.framework.jobs.AsyncJobManager; import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; @@ -134,7 +134,6 @@ import org.apache.http.protocol.ResponseServer; import org.apache.log4j.Logger; import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.stereotype.Component; import com.cloud.api.dispatch.DispatchChainFactory; @@ -197,26 +196,26 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer */ private static final String CONTROL_CHARACTERS = "[\000-\011\013-\014\016-\037\177]"; + @Inject + private AccountManager accountMgr; + @Inject + private APIAuthenticationManager authManager; @Inject private ApiDispatcher dispatcher; @Inject - private DispatchChainFactory dispatchChainFactory; + private AsyncJobManager asyncMgr; @Inject - private AccountManager accountMgr; + private DispatchChainFactory dispatchChainFactory; @Inject private DomainManager domainMgr; @Inject private DomainDao domainDao; @Inject - private UUIDManager uuidMgr; - @Inject - private AsyncJobManager asyncMgr; - @Inject private EntityManager entityMgr; @Inject - private APIAuthenticationManager authManager; - @Inject private ProjectDao projectDao; + @Inject + private UUIDManager uuidMgr; private List pluggableServices; @@ -225,6 +224,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer @Inject private ApiAsyncJobDispatcher asyncDispatcher; + private EventDistributor eventDistributor = null; private static int s_workerCount = 0; private static Map>> s_apiNameCmdClassMap = new HashMap>>(); @@ -291,6 +291,10 @@ public boolean configure(final String name, final Map params) th return true; } + public void setEventDistributor(EventDistributor eventDistributor) { + this.eventDistributor = eventDistributor; + } + @MessageHandler(topic = AsyncJob.Topics.JOB_EVENT_PUBLISH) public void handleAsyncJobPublishEvent(String subject, String senderAddress, Object args) { assert (args != null); @@ -302,12 +306,8 @@ public void handleAsyncJobPublishEvent(String subject, String senderAddress, Obj if (s_logger.isTraceEnabled()) s_logger.trace("Handle asyjob publish event " + jobEvent); - - EventBus eventBus = null; - try { - eventBus = ComponentContext.getComponent(EventBus.class); - } catch (NoSuchBeanDefinitionException nbe) { - return; // no provider is configured to provide events bus, so just return + if (eventDistributor == null) { + setEventDistributor(ComponentContext.getComponent(EventDistributor.class)); } if (!job.getDispatcher().equalsIgnoreCase("ApiAsyncJobDispatcher")) { @@ -320,7 +320,7 @@ public void handleAsyncJobPublishEvent(String subject, String senderAddress, Obj // Get the event type from the cmdInfo json string String info = job.getCmdInfo(); String cmdEventType = "unknown"; - Map cmdInfoObj = new HashMap(); + Map cmdInfoObj = new HashMap<>(); if (info != null) { Type type = new TypeToken>(){}.getType(); Map cmdInfo = ApiGsonHelper.getBuilder().create().fromJson(info, type); @@ -348,7 +348,7 @@ public void handleAsyncJobPublishEvent(String subject, String senderAddress, Obj org.apache.cloudstack.framework.events.Event event = new org.apache.cloudstack.framework.events.Event("management-server", EventCategory.ASYNC_JOB_CHANGE_EVENT.getName(), jobEvent, instanceType, instanceUuid); - Map eventDescription = new HashMap(); + Map eventDescription = new HashMap<>(); eventDescription.put("command", job.getCmd()); eventDescription.put("user", userJobOwner.getUuid()); eventDescription.put("account", jobOwner.getUuid()); @@ -369,12 +369,10 @@ public void handleAsyncJobPublishEvent(String subject, String senderAddress, Obj eventDescription.put("domainname", domain.getName()); } event.setDescription(eventDescription); - - try { - eventBus.publish(event); - } catch (EventBusException evx) { - String errMsg = "Failed to publish async job event on the event bus."; - s_logger.warn(errMsg, evx); + List exceptions = eventDistributor.publish(event); + for (EventBusException ex : exceptions) { + String errMsg = "Failed to publish event."; + s_logger.warn(errMsg, ex); } } diff --git a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruManagerImpl.java b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruManagerImpl.java index a5f1f9fa5cb1..03c2f4856691 100644 --- a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruManagerImpl.java +++ b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruManagerImpl.java @@ -40,7 +40,7 @@ public class HypervisorGuruManagerImpl extends ManagerBase implements Hypervisor HostDao _hostDao; List _hvGuruList; - Map _hvGurus = new ConcurrentHashMap(); + Map _hvGurus = new ConcurrentHashMap<>(); @PostConstruct public void init() { diff --git a/server/src/main/java/com/cloud/storage/listener/SnapshotStateListener.java b/server/src/main/java/com/cloud/storage/listener/SnapshotStateListener.java index c68b05c50624..8133547eca56 100644 --- a/server/src/main/java/com/cloud/storage/listener/SnapshotStateListener.java +++ b/server/src/main/java/com/cloud/storage/listener/SnapshotStateListener.java @@ -25,13 +25,11 @@ import javax.annotation.PostConstruct; import javax.inject.Inject; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.framework.events.EventBus; -import org.apache.cloudstack.framework.events.EventBusException; -import org.apache.log4j.Logger; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.stereotype.Component; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.framework.events.EventDistributor; + import com.cloud.configuration.Config; import com.cloud.event.EventCategory; import com.cloud.server.ManagementService; @@ -46,13 +44,12 @@ @Component public class SnapshotStateListener implements StateListener { - protected static EventBus s_eventBus = null; protected static ConfigurationDao s_configDao; @Inject private ConfigurationDao configDao; - private static final Logger s_logger = Logger.getLogger(SnapshotStateListener.class); + private EventDistributor eventDistributor = null; public SnapshotStateListener() { @@ -63,6 +60,10 @@ void init() { s_configDao = configDao; } + public void setEventDistributor(EventDistributor eventDistributor) { + this.eventDistributor = eventDistributor; + } + @Override public boolean preStateTransitionEvent(State oldState, Event event, State newState, SnapshotVO vo, boolean status, Object opaque) { pubishOnEventBus(event.name(), "preStateTransitionEvent", vo, oldState, newState); @@ -83,17 +84,15 @@ private void pubishOnEventBus(String event, String status, Snapshot vo, State ol if(!configValue) { return; } - try { - s_eventBus = ComponentContext.getComponent(EventBus.class); - } catch (NoSuchBeanDefinitionException nbe) { - return; // no provider is configured to provide events bus, so just return + if (eventDistributor == null) { + setEventDistributor(ComponentContext.getComponent(EventDistributor.class)); } String resourceName = getEntityFromClassName(Snapshot.class.getName()); org.apache.cloudstack.framework.events.Event eventMsg = new org.apache.cloudstack.framework.events.Event(ManagementService.Name, EventCategory.RESOURCE_STATE_CHANGE_EVENT.getName(), event, resourceName, vo.getUuid()); - Map eventDescription = new HashMap(); + Map eventDescription = new HashMap<>(); eventDescription.put("resource", resourceName); eventDescription.put("id", vo.getUuid()); eventDescription.put("old-state", oldState.name()); @@ -103,11 +102,7 @@ private void pubishOnEventBus(String event, String status, Snapshot vo, State ol eventDescription.put("eventDateTime", eventDate); eventMsg.setDescription(eventDescription); - try { - s_eventBus.publish(eventMsg); - } catch (EventBusException e) { - s_logger.warn("Failed to publish state change event on the event bus."); - } + eventDistributor.publish(eventMsg); } private String getEntityFromClassName(String entityClassName) { diff --git a/server/src/main/java/com/cloud/storage/listener/VolumeStateListener.java b/server/src/main/java/com/cloud/storage/listener/VolumeStateListener.java index d2a4dc93b2f3..2177afe66296 100644 --- a/server/src/main/java/com/cloud/storage/listener/VolumeStateListener.java +++ b/server/src/main/java/com/cloud/storage/listener/VolumeStateListener.java @@ -22,41 +22,40 @@ import java.util.HashMap; import java.util.Map; -import com.cloud.event.EventTypes; -import com.cloud.event.UsageEventUtils; -import com.cloud.utils.fsm.StateMachine2; -import com.cloud.vm.VMInstanceVO; -import com.cloud.vm.VirtualMachine; -import com.cloud.vm.dao.VMInstanceDao; -import org.apache.log4j.Logger; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; - -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.framework.events.EventBus; -import org.apache.cloudstack.framework.events.EventBusException; - import com.cloud.configuration.Config; import com.cloud.event.EventCategory; +import com.cloud.event.EventTypes; +import com.cloud.event.UsageEventUtils; import com.cloud.server.ManagementService; import com.cloud.storage.Volume; import com.cloud.storage.Volume.Event; import com.cloud.storage.Volume.State; import com.cloud.utils.component.ComponentContext; import com.cloud.utils.fsm.StateListener; +import com.cloud.utils.fsm.StateMachine2; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.VMInstanceDao; + +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.framework.events.EventDistributor; public class VolumeStateListener implements StateListener { - protected static EventBus s_eventBus = null; protected ConfigurationDao _configDao; protected VMInstanceDao _vmInstanceDao; - private static final Logger s_logger = Logger.getLogger(VolumeStateListener.class); + private EventDistributor eventDistributor; public VolumeStateListener(ConfigurationDao configDao, VMInstanceDao vmInstanceDao) { this._configDao = configDao; this._vmInstanceDao = vmInstanceDao; } + public void setEventDistributor(EventDistributor eventDistributor) { + this.eventDistributor = eventDistributor; + } + @Override public boolean preStateTransitionEvent(State oldState, Event event, State newState, Volume vo, boolean status, Object opaque) { pubishOnEventBus(event.name(), "preStateTransitionEvent", vo, oldState, newState); @@ -92,23 +91,21 @@ public boolean postStateTransitionEvent(StateMachine2.Transition t return true; } - private void pubishOnEventBus(String event, String status, Volume vo, State oldState, State newState) { + private void pubishOnEventBus(String event, String status, Volume vo, State oldState, State newState) { String configKey = Config.PublishResourceStateEvent.key(); String value = _configDao.getValue(configKey); boolean configValue = Boolean.parseBoolean(value); if(!configValue) return; - try { - s_eventBus = ComponentContext.getComponent(EventBus.class); - } catch (NoSuchBeanDefinitionException nbe) { - return; // no provider is configured to provide events bus, so just return + if (eventDistributor == null) { + setEventDistributor(ComponentContext.getComponent(EventDistributor.class)); } String resourceName = getEntityFromClassName(Volume.class.getName()); org.apache.cloudstack.framework.events.Event eventMsg = new org.apache.cloudstack.framework.events.Event(ManagementService.Name, EventCategory.RESOURCE_STATE_CHANGE_EVENT.getName(), event, resourceName, - vo.getUuid()); + vo.getUuid()); Map eventDescription = new HashMap(); eventDescription.put("resource", resourceName); eventDescription.put("id", vo.getUuid()); @@ -119,11 +116,7 @@ private void pubishOnEventBus(String event, String status, Volume vo, State oldS eventDescription.put("eventDateTime", eventDate); eventMsg.setDescription(eventDescription); - try { - s_eventBus.publish(eventMsg); - } catch (EventBusException e) { - s_logger.warn("Failed to state change event on the event bus."); - } + eventDistributor.publish(eventMsg); } private String getEntityFromClassName(String entityClassName) { diff --git a/server/src/main/java/com/cloud/vm/UserVmStateListener.java b/server/src/main/java/com/cloud/vm/UserVmStateListener.java index e9f7e7c5c72d..8d397278fdca 100644 --- a/server/src/main/java/com/cloud/vm/UserVmStateListener.java +++ b/server/src/main/java/com/cloud/vm/UserVmStateListener.java @@ -24,15 +24,6 @@ import javax.inject.Inject; -import com.cloud.server.ManagementService; -import com.cloud.utils.fsm.StateMachine2; -import com.cloud.vm.dao.UserVmDao; -import org.apache.log4j.Logger; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; - -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.framework.events.EventBus; - import com.cloud.configuration.Config; import com.cloud.event.EventCategory; import com.cloud.event.EventTypes; @@ -41,11 +32,17 @@ import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; import com.cloud.service.dao.ServiceOfferingDao; +import com.cloud.server.ManagementService; import com.cloud.utils.component.ComponentContext; import com.cloud.utils.fsm.StateListener; +import com.cloud.utils.fsm.StateMachine2; import com.cloud.vm.VirtualMachine.Event; import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.UserVmDao; + +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.framework.events.EventDistributor; public class UserVmStateListener implements StateListener { @@ -56,9 +53,7 @@ public class UserVmStateListener implements StateListener eventDescription = new HashMap(); + Map eventDescription = new HashMap<>(); eventDescription.put("resource", resourceName); eventDescription.put("id", vo.getUuid()); eventDescription.put("old-state", oldState.name()); @@ -149,12 +146,7 @@ private void pubishOnEventBus(String event, String status, VirtualMachine vo, Vi eventDescription.put("eventDateTime", eventDate); eventMsg.setDescription(eventDescription); - try { - s_eventBus.publish(eventMsg); - } catch (org.apache.cloudstack.framework.events.EventBusException e) { - s_logger.warn("Failed to publish state change event on the event bus."); - } - + eventDistributor.publish(eventMsg); } private String getEntityFromClassName(String entityClassName) { diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index 7227264e2297..13a6c6c7fa2f 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -144,6 +144,10 @@ + + + + diff --git a/utils/src/main/java/com/cloud/utils/component/ComponentContext.java b/utils/src/main/java/com/cloud/utils/component/ComponentContext.java index 8486dbf4bd4a..a03d21db656c 100644 --- a/utils/src/main/java/com/cloud/utils/component/ComponentContext.java +++ b/utils/src/main/java/com/cloud/utils/component/ComponentContext.java @@ -100,7 +100,7 @@ public static void initComponentsLifeCycle() { s_logger.info("Running SystemIntegrityChecker " + entry.getKey()); try { entry.getValue().check(); - } catch (Throwable e) { + } catch (RuntimeException e) { s_logger.error("System integrity check failed. Refuse to startup", e); System.exit(1); } @@ -178,6 +178,13 @@ public static T getComponent(String name) { return (T)s_appContext.getBean(name); } + /** + * only ever used to get the event bus + * + * @param beanType the component type to return + * @return one of the component registered for the requested type + * @param + */ public static T getComponent(Class beanType) { assert (s_appContext != null); Map matchedTypes = getComponentsOfType(beanType); From fa874ed49d02fd23faec7acf7b29fa0b92955f17 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 13 Feb 2024 17:10:05 +0530 Subject: [PATCH 06/75] test bus Signed-off-by: Abhishek Kumar --- client/pom.xml | 5 ++ plugins/event-bus/testbus1/pom.xml | 41 +++++++++ .../mom/testbus1/Test1EventBus.java | 88 +++++++++++++++++++ plugins/pom.xml | 1 + 4 files changed, 135 insertions(+) create mode 100644 plugins/event-bus/testbus1/pom.xml create mode 100644 plugins/event-bus/testbus1/src/main/java/org/apache/cloudstack/mom/testbus1/Test1EventBus.java diff --git a/client/pom.xml b/client/pom.xml index 8b8747bdf930..259fd47a2012 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -447,6 +447,11 @@ cloud-mom-kafka ${project.version} + + org.apache.cloudstack + cloud-mom-testbus1 + ${project.version} + org.apache.cloudstack cloud-framework-agent-lb diff --git a/plugins/event-bus/testbus1/pom.xml b/plugins/event-bus/testbus1/pom.xml new file mode 100644 index 000000000000..f70fc9a05509 --- /dev/null +++ b/plugins/event-bus/testbus1/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + cloud-mom-testbus1 + Apache CloudStack Plugin - Test1 Event Bus + + org.apache.cloudstack + cloudstack-plugins + 4.20.0.0-SNAPSHOT + ../../pom.xml + + + + org.apache.cloudstack + cloud-framework-events + ${project.version} + + + com.rabbitmq + amqp-client + + + diff --git a/plugins/event-bus/testbus1/src/main/java/org/apache/cloudstack/mom/testbus1/Test1EventBus.java b/plugins/event-bus/testbus1/src/main/java/org/apache/cloudstack/mom/testbus1/Test1EventBus.java new file mode 100644 index 000000000000..b442b79816d0 --- /dev/null +++ b/plugins/event-bus/testbus1/src/main/java/org/apache/cloudstack/mom/testbus1/Test1EventBus.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.mom.testbus1; + +import java.util.Map; +import java.util.UUID; + +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.framework.events.Event; +import org.apache.cloudstack.framework.events.EventBus; +import org.apache.cloudstack.framework.events.EventBusException; +import org.apache.cloudstack.framework.events.EventSubscriber; +import org.apache.cloudstack.framework.events.EventTopic; +import org.apache.log4j.Logger; + +import com.cloud.utils.component.ManagerBase; + +public class Test1EventBus extends ManagerBase implements EventBus { + + private static final Logger s_logger = Logger.getLogger(Test1EventBus.class); + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + _name = name; + return true; + } + + @Override + public void setName(String name) { + _name = name; + } + + @Override + public UUID subscribe(EventTopic topic, EventSubscriber subscriber) throws EventBusException { + if (s_logger.isDebugEnabled()) { + s_logger.debug(String.format("subscribing \'%s\' to events of type \'%s\' from \'%s\'",subscriber.toString(), topic.getEventType(), topic.getEventSource())); + } + + /* NOOP */ + return UUID.randomUUID(); + } + + @Override + public void unsubscribe(UUID subscriberId, EventSubscriber subscriber) throws EventBusException { + if (s_logger.isDebugEnabled()) { + s_logger.debug(String.format("unsubscribing \'%s\'",subscriberId)); + } + /* NOOP */ + } + + @Override + public void publish(Event event) throws EventBusException { + s_logger.info(String.format("New event: %s", event.getDescription())); + } + + @Override + public String getName() { + return _name; + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } +} diff --git a/plugins/pom.xml b/plugins/pom.xml index cbfba8f82177..299207802e6c 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -79,6 +79,7 @@ event-bus/inmemory event-bus/kafka event-bus/rabbitmq + event-bus/testbus1 ha-planners/skip-heurestics From 75a56f75a168912502d83fcbe9b42123be56f5d1 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 13 Feb 2024 17:11:22 +0530 Subject: [PATCH 07/75] refactor Signed-off-by: Abhishek Kumar --- .../network/contrail/management/EventUtils.java | 16 ++++++---------- .../java/com/cloud/event/ActionEventUtils.java | 16 +++++++++------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/EventUtils.java b/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/EventUtils.java index d6a286596318..f8e4ce18133d 100644 --- a/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/EventUtils.java +++ b/plugins/network-elements/juniper-contrail/src/main/java/org/apache/cloudstack/network/contrail/management/EventUtils.java @@ -18,22 +18,20 @@ package org.apache.cloudstack.network.contrail.management; import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Map; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.events.EventBusException; import org.apache.cloudstack.framework.events.EventDistributor; import org.apache.log4j.Logger; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.stereotype.Component; -import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.framework.events.EventBus; -import org.apache.cloudstack.framework.events.EventBusException; -import org.aopalliance.intercept.MethodInterceptor; -import org.aopalliance.intercept.MethodInvocation; - import com.cloud.event.ActionEvent; import com.cloud.event.ActionEvents; import com.cloud.event.Event; @@ -49,7 +47,6 @@ public class EventUtils { private static final Logger s_logger = Logger.getLogger(EventUtils.class); private static EventDistributor eventDistributor; - protected static EventBus s_eventBus = null; public EventUtils() { } @@ -66,7 +63,6 @@ private static void publishOnMessageBus(String eventCategory, String eventType, try { setEventDistributor(ComponentContext.getComponent(EventDistributor.class)); - s_eventBus = ComponentContext.getComponent(EventBus.class); } catch (NoSuchBeanDefinitionException nbe) { return; // no provider is configured to provide events bus, so just return } diff --git a/server/src/main/java/com/cloud/event/ActionEventUtils.java b/server/src/main/java/com/cloud/event/ActionEventUtils.java index 36461d20e421..b0508eb4b7e2 100644 --- a/server/src/main/java/com/cloud/event/ActionEventUtils.java +++ b/server/src/main/java/com/cloud/event/ActionEventUtils.java @@ -22,6 +22,7 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import javax.annotation.PostConstruct; @@ -32,8 +33,8 @@ import org.apache.cloudstack.api.InternalIdentity; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.framework.events.EventBus; import org.apache.cloudstack.framework.events.EventBusException; +import org.apache.cloudstack.framework.events.EventDistributor; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; @@ -62,7 +63,7 @@ public class ActionEventUtils { private static AccountDao s_accountDao; private static ProjectDao s_projectDao; protected static UserDao s_userDao; - protected static EventBus s_eventBus = null; + private static EventDistributor eventDistributor; protected static EntityManager s_entityMgr; protected static ConfigurationDao s_configDao; @@ -198,8 +199,9 @@ private static void publishOnEventBus(long userId, long accountId, String eventC boolean configValue = Boolean.parseBoolean(value); if(!configValue) return; + try { - s_eventBus = ComponentContext.getComponent(EventBus.class); + eventDistributor = ComponentContext.getComponent(EventDistributor.class); } catch (NoSuchBeanDefinitionException nbe) { return; // no provider is configured to provide events bus, so just return } @@ -233,10 +235,10 @@ private static void publishOnEventBus(long userId, long accountId, String eventC event.setDescription(eventDescription); - try { - s_eventBus.publish(event); - } catch (EventBusException e) { - s_logger.warn("Failed to publish action event on the event bus."); + List exceptions = eventDistributor.publish(event); + for (EventBusException ex : exceptions) { + String errMsg = "Failed to publish event."; + s_logger.warn(errMsg, ex); } } From 0c2a5c701ea39186b1417ee99810e75e9c7f455c Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 13 Feb 2024 17:12:21 +0530 Subject: [PATCH 08/75] test Signed-off-by: Abhishek Kumar --- .../events/EventDistributorImpl.java | 21 +++++++++++++++---- .../spring-framework-event-core-context.xml | 4 ++-- .../spring-server-core-managers-context.xml | 1 + 3 files changed, 20 insertions(+), 6 deletions(-) rename framework/events/src/main/resources/{META-INF.cloudstack.core => META-INF/cloudstack/core}/spring-framework-event-core-context.xml (90%) diff --git a/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributorImpl.java b/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributorImpl.java index e92d36b4541f..7650688caa69 100644 --- a/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributorImpl.java +++ b/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributorImpl.java @@ -19,13 +19,16 @@ package org.apache.cloudstack.framework.events; -import com.cloud.utils.component.ManagerBase; -import org.apache.log4j.Logger; - -import javax.annotation.PostConstruct; import java.util.ArrayList; import java.util.List; +import javax.annotation.PostConstruct; + +import org.apache.cloudstack.framework.ca.CAProvider; +import org.apache.log4j.Logger; + +import com.cloud.utils.component.ManagerBase; + public class EventDistributorImpl extends ManagerBase implements EventDistributor { private static final Logger LOGGER = Logger.getLogger(EventDistributorImpl.class); @@ -35,6 +38,16 @@ public void setEventBusses(List eventBusses) { List eventBusses; + List caProviders; + + public List getCaProviders() { + return caProviders; + } + + public void setCaProviders(List caProviders) { + this.caProviders = caProviders; + } + @PostConstruct public void init() { if (LOGGER.isTraceEnabled()) { diff --git a/framework/events/src/main/resources/META-INF.cloudstack.core/spring-framework-event-core-context.xml b/framework/events/src/main/resources/META-INF/cloudstack/core/spring-framework-event-core-context.xml similarity index 90% rename from framework/events/src/main/resources/META-INF.cloudstack.core/spring-framework-event-core-context.xml rename to framework/events/src/main/resources/META-INF/cloudstack/core/spring-framework-event-core-context.xml index 45eb666cb1cf..8b0c12ca4f11 100644 --- a/framework/events/src/main/resources/META-INF.cloudstack.core/spring-framework-event-core-context.xml +++ b/framework/events/src/main/resources/META-INF/cloudstack/core/spring-framework-event-core-context.xml @@ -28,7 +28,7 @@ > - + + diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index 13a6c6c7fa2f..e706ac984ce4 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -146,6 +146,7 @@ + From d82863bdfe31f60abafd686b042f757d956e00de Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 13 Feb 2024 22:05:23 +0530 Subject: [PATCH 09/75] fix and refactor Signed-off-by: Abhishek Kumar --- client/pom.xml | 2 +- ...-lifecycle-compute-context-inheritable.xml | 5 --- .../spring-core-registry-core-context.xml | 10 +++--- .../cloudstack/event/module.properties | 21 +++++++++++++ ...re-lifecycle-event-context-inheritable.xml | 31 +++++++++++++++++++ .../framework/events/EventDistributor.java | 2 +- .../events/EventDistributorImpl.java | 23 ++++---------- .../event-bus/{testbus1 => testbus}/pom.xml | 2 +- .../cloudstack/mom/testbus/TestEventBus.java} | 6 ++-- .../cloudstack/testbus/module.properties | 18 +++++++++++ .../testbus/spring-event-testbus-context.xml | 11 +++---- plugins/pom.xml | 2 +- .../spring-server-core-managers-context.xml | 3 +- 13 files changed, 94 insertions(+), 42 deletions(-) create mode 100644 core/src/main/resources/META-INF/cloudstack/event/module.properties create mode 100644 core/src/main/resources/META-INF/cloudstack/event/spring-core-lifecycle-event-context-inheritable.xml rename plugins/event-bus/{testbus1 => testbus}/pom.xml (97%) rename plugins/event-bus/{testbus1/src/main/java/org/apache/cloudstack/mom/testbus1/Test1EventBus.java => testbus/src/main/java/org/apache/cloudstack/mom/testbus/TestEventBus.java} (95%) create mode 100644 plugins/event-bus/testbus/src/main/resources/META-INF/cloudstack/testbus/module.properties rename framework/events/src/main/resources/META-INF/cloudstack/core/spring-framework-event-core-context.xml => plugins/event-bus/testbus/src/main/resources/META-INF/cloudstack/testbus/spring-event-testbus-context.xml (83%) diff --git a/client/pom.xml b/client/pom.xml index 259fd47a2012..a69c951bbed8 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -449,7 +449,7 @@ org.apache.cloudstack - cloud-mom-testbus1 + cloud-mom-testbus ${project.version} diff --git a/core/src/main/resources/META-INF/cloudstack/compute/spring-core-lifecycle-compute-context-inheritable.xml b/core/src/main/resources/META-INF/cloudstack/compute/spring-core-lifecycle-compute-context-inheritable.xml index ef6adab9dd99..fb0e8780ecc7 100644 --- a/core/src/main/resources/META-INF/cloudstack/compute/spring-core-lifecycle-compute-context-inheritable.xml +++ b/core/src/main/resources/META-INF/cloudstack/compute/spring-core-lifecycle-compute-context-inheritable.xml @@ -39,11 +39,6 @@ - - - - - diff --git a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml index bfe722fad557..08902b08e8e6 100644 --- a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml +++ b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml @@ -287,11 +287,6 @@ - - - - @@ -355,4 +350,9 @@ + + + + diff --git a/core/src/main/resources/META-INF/cloudstack/event/module.properties b/core/src/main/resources/META-INF/cloudstack/event/module.properties new file mode 100644 index 000000000000..ab1f88e98448 --- /dev/null +++ b/core/src/main/resources/META-INF/cloudstack/event/module.properties @@ -0,0 +1,21 @@ +# +# 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=event +parent=core diff --git a/core/src/main/resources/META-INF/cloudstack/event/spring-core-lifecycle-event-context-inheritable.xml b/core/src/main/resources/META-INF/cloudstack/event/spring-core-lifecycle-event-context-inheritable.xml new file mode 100644 index 000000000000..63d11c65bacb --- /dev/null +++ b/core/src/main/resources/META-INF/cloudstack/event/spring-core-lifecycle-event-context-inheritable.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributor.java b/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributor.java index 4f4775317103..b001da0a632d 100644 --- a/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributor.java +++ b/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributor.java @@ -25,7 +25,7 @@ public interface EventDistributor extends Manager { /** - * publish an event on to the event busses + * publish an event on to the event buses * * @param event event that needs to be published on the event bus */ diff --git a/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributorImpl.java b/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributorImpl.java index 7650688caa69..e8e4138ca223 100644 --- a/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributorImpl.java +++ b/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributorImpl.java @@ -24,7 +24,6 @@ import javax.annotation.PostConstruct; -import org.apache.cloudstack.framework.ca.CAProvider; import org.apache.log4j.Logger; import com.cloud.utils.component.ManagerBase; @@ -32,38 +31,28 @@ public class EventDistributorImpl extends ManagerBase implements EventDistributor { private static final Logger LOGGER = Logger.getLogger(EventDistributorImpl.class); - public void setEventBusses(List eventBusses) { - this.eventBusses = eventBusses; - } - - List eventBusses; - - List caProviders; - - public List getCaProviders() { - return caProviders; - } + List eventBuses; - public void setCaProviders(List caProviders) { - this.caProviders = caProviders; + public void setEventBuses(List eventBuses) { + this.eventBuses = eventBuses; } @PostConstruct public void init() { if (LOGGER.isTraceEnabled()) { - LOGGER.trace(String.format("testing %d event busses", eventBusses.size())); + LOGGER.trace(String.format("testing %d event buses", eventBuses.size())); } publish(new Event("server", "NONE","starting", "server", "NONE")); } @Override public List publish(Event event) { - LOGGER.info(String.format("publishing %s to %d event busses", (event == null ? "" : event.getDescription()), eventBusses.size())); + LOGGER.info(String.format("publishing %s to %d event buses", (event == null ? "" : event.getDescription()), eventBuses.size())); List exceptions = new ArrayList<>(); if (event == null) { return exceptions; } - for (EventBus bus : eventBusses) { + for (EventBus bus : eventBuses) { try { bus.publish(event); } catch (EventBusException e) { diff --git a/plugins/event-bus/testbus1/pom.xml b/plugins/event-bus/testbus/pom.xml similarity index 97% rename from plugins/event-bus/testbus1/pom.xml rename to plugins/event-bus/testbus/pom.xml index f70fc9a05509..af108c45e2b0 100644 --- a/plugins/event-bus/testbus1/pom.xml +++ b/plugins/event-bus/testbus/pom.xml @@ -19,7 +19,7 @@ 4.0.0 - cloud-mom-testbus1 + cloud-mom-testbus Apache CloudStack Plugin - Test1 Event Bus org.apache.cloudstack diff --git a/plugins/event-bus/testbus1/src/main/java/org/apache/cloudstack/mom/testbus1/Test1EventBus.java b/plugins/event-bus/testbus/src/main/java/org/apache/cloudstack/mom/testbus/TestEventBus.java similarity index 95% rename from plugins/event-bus/testbus1/src/main/java/org/apache/cloudstack/mom/testbus1/Test1EventBus.java rename to plugins/event-bus/testbus/src/main/java/org/apache/cloudstack/mom/testbus/TestEventBus.java index b442b79816d0..7b3254707c44 100644 --- a/plugins/event-bus/testbus1/src/main/java/org/apache/cloudstack/mom/testbus1/Test1EventBus.java +++ b/plugins/event-bus/testbus/src/main/java/org/apache/cloudstack/mom/testbus/TestEventBus.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.cloudstack.mom.testbus1; +package org.apache.cloudstack.mom.testbus; import java.util.Map; import java.util.UUID; @@ -33,9 +33,9 @@ import com.cloud.utils.component.ManagerBase; -public class Test1EventBus extends ManagerBase implements EventBus { +public class TestEventBus extends ManagerBase implements EventBus { - private static final Logger s_logger = Logger.getLogger(Test1EventBus.class); + private static final Logger s_logger = Logger.getLogger(TestEventBus.class); @Override public boolean configure(String name, Map params) throws ConfigurationException { diff --git a/plugins/event-bus/testbus/src/main/resources/META-INF/cloudstack/testbus/module.properties b/plugins/event-bus/testbus/src/main/resources/META-INF/cloudstack/testbus/module.properties new file mode 100644 index 000000000000..1b3f474d9654 --- /dev/null +++ b/plugins/event-bus/testbus/src/main/resources/META-INF/cloudstack/testbus/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=testbus +parent=event diff --git a/framework/events/src/main/resources/META-INF/cloudstack/core/spring-framework-event-core-context.xml b/plugins/event-bus/testbus/src/main/resources/META-INF/cloudstack/testbus/spring-event-testbus-context.xml similarity index 83% rename from framework/events/src/main/resources/META-INF/cloudstack/core/spring-framework-event-core-context.xml rename to plugins/event-bus/testbus/src/main/resources/META-INF/cloudstack/testbus/spring-event-testbus-context.xml index 8b0c12ca4f11..11854a27ada4 100644 --- a/framework/events/src/main/resources/META-INF/cloudstack/core/spring-framework-event-core-context.xml +++ b/plugins/event-bus/testbus/src/main/resources/META-INF/cloudstack/testbus/spring-event-testbus-context.xml @@ -25,10 +25,9 @@ http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd" -> - - - - + > + + + + diff --git a/plugins/pom.xml b/plugins/pom.xml index 299207802e6c..eef3cf1040ed 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -79,7 +79,7 @@ event-bus/inmemory event-bus/kafka event-bus/rabbitmq - event-bus/testbus1 + event-bus/testbus ha-planners/skip-heurestics diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index e706ac984ce4..a2eaf2d84699 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -145,8 +145,7 @@ - - + From f74ffee9577bf2ec418fd8ac3d465b42b4967d6a Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 14 Feb 2024 15:58:24 +0530 Subject: [PATCH 10/75] changes for webhook dispatch history Signed-off-by: Abhishek Kumar --- .../apache/cloudstack/api/ApiConstants.java | 4 + .../META-INF/db/schema-41900to42000.sql | 20 ++- .../events/EventDistributorImpl.java | 12 +- .../mom/webhook/WebhookApiService.java | 5 + .../mom/webhook/WebhookApiServiceImpl.java | 120 ++++++++++++++++ .../mom/webhook/WebhookDispatch.java | 35 +++++ .../mom/webhook/WebhookDispatchThread.java | 102 +++++++++++-- .../mom/webhook/WebhookService.java | 10 ++ .../mom/webhook/WebhookServiceImpl.java | 77 +++++++++- .../user/DeleteWebhookDispatchHistoryCmd.java | 104 ++++++++++++++ .../command/user/DeleteWebhookRuleCmd.java | 5 +- .../user/ListWebhookDispatchHistoryCmd.java | 92 ++++++++++++ .../api/command/user/ListWebhookRulesCmd.java | 3 +- .../command/user/UpdateWebhookRuleCmd.java | 3 +- .../api/response/WebhookDispatchResponse.java | 104 ++++++++++++++ .../api/response/WebhookRuleResponse.java | 2 +- .../mom/webhook/dao/WebhookDispatchDao.java | 26 ++++ .../webhook/dao/WebhookDispatchDaoImpl.java | 40 ++++++ .../mom/webhook/vo/WebhookDispatchVO.java | 135 ++++++++++++++++++ .../cloudstack/webhook/module.properties | 2 +- ...t.xml => spring-event-webhook-context.xml} | 7 +- 21 files changed, 880 insertions(+), 28 deletions(-) create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatch.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookDispatchHistoryCmd.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookDispatchHistoryCmd.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookDispatchResponse.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookDispatchDao.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookDispatchDaoImpl.java create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookDispatchVO.java rename plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/{spring-webhook-context.xml => spring-event-webhook-context.xml} (87%) 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 7dca4b2c5b49..78afce480da9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -441,6 +441,7 @@ public class ApiConstants { public static final String SYSTEM_VM_TYPE = "systemvmtype"; public static final String TAGS = "tags"; public static final String STORAGE_TAGS = "storagetags"; + public static final String SUCCESS = "success"; public static final String SUPPORTS_STORAGE_SNAPSHOT = "supportsstoragesnapshot"; public static final String TARGET_IQN = "targetiqn"; public static final String TEMPLATE_FILTER = "templatefilter"; @@ -1094,6 +1095,9 @@ public class ApiConstants { public static final String PARAMETER_DESCRIPTION_IS_TAG_A_RULE = "Whether the informed tag is a JS interpretable rule or not."; + public static final String WEBHOOK_RULE_ID = "webhookruleid"; + public static final String WEBHOOK_RULE_NAME = "webhookrulename"; + /** * This enum specifies IO Drivers, each option controls specific policies on I/O. * Qemu guests support "threads" and "native" options Since 0.8.8 ; "io_uring" is supported Since 6.3.0 (QEMU 5.0). diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql b/engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql index d47b794bdd77..bc4acfba32ad 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql @@ -27,8 +27,8 @@ CREATE TABLE `cloud`.`webhook` ( `name` varchar(255) NOT NULL COMMENT 'name of the webhook', `description` varchar(4096) COMMENT 'description for the webhook', `state` varchar(255) NOT NULL COMMENT 'state of the webhook - Enabled or Disabled', - `domain_id` bigint unsigned NOT NULL COMMENT 'ID of the owner domain of the webhook', - `account_id` bigint unsigned NOT NULL COMMENT 'ID of the owner account of the webhook', + `domain_id` bigint unsigned NOT NULL COMMENT 'id of the owner domain of the webhook', + `account_id` bigint unsigned NOT NULL COMMENT 'id of the owner account of the webhook', `payload_url` varchar(255) COMMENT 'payload URL for the webhook', `secret_key` varchar(255) COMMENT 'secret key for the webhook', `ssl_verification` boolean COMMENT 'for https payload url', @@ -39,3 +39,19 @@ CREATE TABLE `cloud`.`webhook` ( INDEX `i_webhook__account_id`(`account_id`), CONSTRAINT `fk_webhook__account_id` FOREIGN KEY (`account_id`) REFERENCES `account`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `cloud`.`webhook_dispatch`; +CREATE TABLE `cloud`.`webhook_dispatch` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the webhook dispatch', + `uuid` varchar(255) COMMENT 'uuid of the webhook', + `webhook_id` bigint unsigned NOT NULL COMMENT 'id of the webhook rule', + `mshost_msid` bigint unsigned NOT NULL COMMENT 'msid of the management server', + `payload` TEXT COMMENT 'payload URL for the webhook', + `success` boolean COMMENT 'webhook dispatch succeeded or not', + `response` TEXT COMMENT 'response of webhook dispatch', + `start_time` datetime COMMENT 'start timestamp of the webhook dispatch', + `end_time` datetime COMMENT 'end timestamp of the webhook dispatch', + PRIMARY KEY(`id`), + INDEX `i_webhook__webhook_id`(`webhook_id`), + CONSTRAINT `fk_webhook__webhook_id` FOREIGN KEY (`webhook_id`) REFERENCES `webhook`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributorImpl.java b/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributorImpl.java index e8e4138ca223..9bf7e0b2ddad 100644 --- a/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributorImpl.java +++ b/framework/events/src/main/java/org/apache/cloudstack/framework/events/EventDistributorImpl.java @@ -24,6 +24,7 @@ import javax.annotation.PostConstruct; +import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import com.cloud.utils.component.ManagerBase; @@ -40,14 +41,17 @@ public void setEventBuses(List eventBuses) { @PostConstruct public void init() { if (LOGGER.isTraceEnabled()) { - LOGGER.trace(String.format("testing %d event buses", eventBuses.size())); + LOGGER.trace(String.format("Found %d event buses : %s", eventBuses.size(), + StringUtils.join(eventBuses.stream().map(x->x.getClass().getName()).toArray()))); } - publish(new Event("server", "NONE","starting", "server", "NONE")); } @Override public List publish(Event event) { - LOGGER.info(String.format("publishing %s to %d event buses", (event == null ? "" : event.getDescription()), eventBuses.size())); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace(String.format("Publishing event: %s to %d event buses", + (event == null ? "" : event.getDescription()), eventBuses.size())); + } List exceptions = new ArrayList<>(); if (event == null) { return exceptions; @@ -56,7 +60,7 @@ public List publish(Event event) { try { bus.publish(event); } catch (EventBusException e) { - LOGGER.warn(String.format("no publish for bus %s of event %s", bus.getClass().getName(), event.getDescription())); + LOGGER.warn(String.format("Failed to publish for bus %s of event %s", bus.getClass().getName(), event.getDescription())); exceptions.add(e); } } diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiService.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiService.java index 8025992daf66..bc44e0065f5b 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiService.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiService.java @@ -19,9 +19,12 @@ import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.mom.webhook.api.command.user.CreateWebhookRuleCmd; +import org.apache.cloudstack.mom.webhook.api.command.user.DeleteWebhookDispatchHistoryCmd; import org.apache.cloudstack.mom.webhook.api.command.user.DeleteWebhookRuleCmd; +import org.apache.cloudstack.mom.webhook.api.command.user.ListWebhookDispatchHistoryCmd; import org.apache.cloudstack.mom.webhook.api.command.user.ListWebhookRulesCmd; import org.apache.cloudstack.mom.webhook.api.command.user.UpdateWebhookRuleCmd; +import org.apache.cloudstack.mom.webhook.api.response.WebhookDispatchResponse; import org.apache.cloudstack.mom.webhook.api.response.WebhookRuleResponse; import com.cloud.utils.component.PluggableService; @@ -34,4 +37,6 @@ public interface WebhookApiService extends PluggableService { boolean deleteWebhookRule(DeleteWebhookRuleCmd cmd) throws CloudRuntimeException; WebhookRuleResponse updateWebhookRule(UpdateWebhookRuleCmd cmd) throws CloudRuntimeException; WebhookRuleResponse createWebhookRuleResponse(long webhookRuleId); + ListResponse listWebhookDispatchHistory(ListWebhookDispatchHistoryCmd cmd); + boolean deleteWebhookDispatchHistory(DeleteWebhookDispatchHistoryCmd cmd) throws CloudRuntimeException; } diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java index d9510f4c6ebf..32d904a08ae7 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java @@ -27,18 +27,26 @@ import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.mom.webhook.api.command.user.CreateWebhookRuleCmd; +import org.apache.cloudstack.mom.webhook.api.command.user.DeleteWebhookDispatchHistoryCmd; import org.apache.cloudstack.mom.webhook.api.command.user.DeleteWebhookRuleCmd; +import org.apache.cloudstack.mom.webhook.api.command.user.ListWebhookDispatchHistoryCmd; import org.apache.cloudstack.mom.webhook.api.command.user.ListWebhookRulesCmd; import org.apache.cloudstack.mom.webhook.api.command.user.UpdateWebhookRuleCmd; +import org.apache.cloudstack.mom.webhook.api.response.WebhookDispatchResponse; import org.apache.cloudstack.mom.webhook.api.response.WebhookRuleResponse; +import org.apache.cloudstack.mom.webhook.dao.WebhookDispatchDao; import org.apache.cloudstack.mom.webhook.dao.WebhookRuleDao; +import org.apache.cloudstack.mom.webhook.vo.WebhookDispatchVO; import org.apache.cloudstack.mom.webhook.vo.WebhookRuleVO; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import com.cloud.api.ApiDBUtils; +import com.cloud.cluster.ManagementServerHostVO; +import com.cloud.cluster.dao.ManagementServerHostDao; import com.cloud.domain.Domain; import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; import com.cloud.projects.Project; import com.cloud.user.Account; import com.cloud.user.AccountManager; @@ -58,6 +66,10 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ AccountManager accountManager; @Inject WebhookRuleDao webhookRuleDao; + @Inject + WebhookDispatchDao webhookDispatchDao; + @Inject + ManagementServerHostDao managementServerHostDao; protected WebhookRuleResponse createWebhookRuleResponse(WebhookRuleVO webhookRuleVO) { WebhookRuleResponse response = new WebhookRuleResponse(); @@ -85,6 +97,76 @@ protected WebhookRuleResponse createWebhookRuleResponse(WebhookRuleVO webhookRul return response; } + protected ManagementServerHostVO basicWebhookDispatchApiCheck(Account caller, final Long id, final Long webhookRuleId, + Long managementServerId) { + if (id != null) { + WebhookDispatchVO webhookDispatchVO = webhookDispatchDao.findById(id); + if (webhookDispatchVO == null) { + throw new InvalidParameterValueException("Invalid ID specified"); + } + WebhookRuleVO webhookRuleVO = webhookRuleDao.findById(webhookDispatchVO.getWebhookRuleId()); + if (webhookRuleVO != null) { + accountManager.checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhookRuleVO); + } + } + if (webhookRuleId != null) { + WebhookRuleVO webhookRuleVO = webhookRuleDao.findById(webhookRuleId); + if (webhookRuleVO == null) { + throw new InvalidParameterValueException("Invalid Webhook rule specified"); + } + accountManager.checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhookRuleVO); + } + ManagementServerHostVO managementServerHostVO = null; + if (managementServerId != null) { + if (!Account.Type.ADMIN.equals(caller.getType())) { + throw new PermissionDeniedException("Invalid parameter specified"); + } + managementServerHostVO = managementServerHostDao.findById(managementServerId); + if (managementServerHostVO == null) { + throw new InvalidParameterValueException("Invalid management server specified"); + } + } + return managementServerHostVO; + } + + protected SearchCriteria getWebhookDispatchSearchCriteria(final Long id, + final Long webhookRuleId, final Long managementServerId, final String keyword) { + SearchBuilder sb = webhookDispatchDao.createSearchBuilder(); + sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); + sb.and("webhookRuleId", sb.entity().getWebhookRuleId(), SearchCriteria.Op.EQ); + sb.and("managementServerId", sb.entity().getManagementServerId(), SearchCriteria.Op.EQ); + sb.and("keyword", sb.entity().getPayload(), SearchCriteria.Op.LIKE); + SearchCriteria sc = sb.create(); + if (id != null) { + sc.setParameters("id", id); + } + if (webhookRuleId != null) { + sc.setParameters("webhookRuleId", webhookRuleId); + } + if (managementServerId != null) { + sc.setParameters("managementServerId", managementServerId); + } + if (keyword != null) { + sc.setParameters("keyword", "%" + keyword + "%"); + } + return sc; + } + + protected WebhookDispatchResponse createWebhookDispatchResponse(WebhookDispatchVO webhookDispatchVO) { + WebhookDispatchResponse response = new WebhookDispatchResponse(); + response.setObjectName(WebhookDispatch.class.getSimpleName().toLowerCase()); + response.setId(webhookDispatchVO.getUuid()); + response.setWebhookRuleId("SOME"); + response.setWebhookRuleName("SOME"); + response.setManagementServerId("SOME"); + response.setPayload(webhookDispatchVO.getPayload()); + response.setSuccess(webhookDispatchVO.isSuccess()); + response.setResponse(webhookDispatchVO.getResponse()); + response.setStartTime(webhookDispatchVO.getStartTime()); + response.setEndTime(webhookDispatchVO.getEndTime()); + return response; + } + /** * @param cmd * @return Account @@ -275,6 +357,42 @@ public WebhookRuleResponse createWebhookRuleResponse(long webhookRuleId) { return createWebhookRuleResponse(webhookRuleVO); } + @Override + public ListResponse listWebhookDispatchHistory(ListWebhookDispatchHistoryCmd cmd) { + final CallContext ctx = CallContext.current(); + final Account caller = ctx.getCallingAccount(); + final Long id = cmd.getId(); + final Long webhookRuleId = cmd.getWebhookRuleId(); + final Long managementServerId = cmd.getManagementServerId(); + final String keyword = cmd.getKeyword(); + List responsesList = new ArrayList<>(); + ManagementServerHostVO host = basicWebhookDispatchApiCheck(caller, id, webhookRuleId, managementServerId); + + Filter searchFilter = new Filter(WebhookDispatchVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal()); + SearchCriteria sc = getWebhookDispatchSearchCriteria(id, webhookRuleId, host != null ? host.getMsid() : null, keyword); + List dispatches = webhookDispatchDao.search(sc, searchFilter); + for (WebhookDispatchVO dispatch : dispatches) { + WebhookDispatchResponse response = createWebhookDispatchResponse(dispatch); + responsesList.add(response); + } + ListResponse response = new ListResponse<>(); + response.setResponses(responsesList); + return response; + } + + @Override + public boolean deleteWebhookDispatchHistory(DeleteWebhookDispatchHistoryCmd cmd) throws CloudRuntimeException { + final CallContext ctx = CallContext.current(); + final Account caller = ctx.getCallingAccount(); + final Long id = cmd.getId(); + final Long webhookRuleId = cmd.getWebhookRuleId(); + final Long managementServerId = cmd.getManagementServerId(); + ManagementServerHostVO host = basicWebhookDispatchApiCheck(caller, id, webhookRuleId, managementServerId); + SearchCriteria sc = getWebhookDispatchSearchCriteria(id, webhookRuleId, host != null ? host.getMsid() : null, null); + int removed = webhookDispatchDao.remove(sc); + return removed > 0; + } + @Override public List> getCommands() { List> cmdList = new ArrayList<>(); @@ -282,6 +400,8 @@ public List> getCommands() { cmdList.add(ListWebhookRulesCmd.class); cmdList.add(UpdateWebhookRuleCmd.class); cmdList.add(DeleteWebhookRuleCmd.class); + cmdList.add(ListWebhookDispatchHistoryCmd.class); + cmdList.add(DeleteWebhookDispatchHistoryCmd.class); return cmdList; } } diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatch.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatch.java new file mode 100644 index 000000000000..654766fbda5f --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatch.java @@ -0,0 +1,35 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.mom.webhook; + +import java.util.Date; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface WebhookDispatch extends Identity, InternalIdentity { + + long getId(); + long getWebhookRuleId(); + long getManagementServerId(); + String getPayload(); + boolean isSuccess(); + String getResponse(); + Date getStartTime(); + Date getEndTime(); +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatchThread.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatchThread.java index 58257825845f..929671acb4d1 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatchThread.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatchThread.java @@ -18,11 +18,18 @@ package org.apache.cloudstack.mom.webhook; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import org.apache.cloudstack.framework.async.AsyncCompletionCallback; +import org.apache.cloudstack.framework.async.AsyncRpcContext; import org.apache.cloudstack.framework.events.Event; +import org.apache.cloudstack.storage.command.CommandResult; import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.io.IOUtils; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; @@ -31,6 +38,7 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.log4j.Logger; +import com.cloud.event.EventCategory; import com.google.gson.Gson; public class WebhookDispatchThread implements Runnable { @@ -39,13 +47,19 @@ public class WebhookDispatchThread implements Runnable { private final CloseableHttpClient httpClient; private WebhookRule rule; private Event event; + private String payload; + private String response; + private Date startTime; private int dispatchRetries = 3; private int deliveryTimeout = 10; - public WebhookDispatchThread(CloseableHttpClient httpClient, WebhookRule rule, Event event) { + AsyncCompletionCallback callback; + + public WebhookDispatchThread(CloseableHttpClient httpClient, WebhookRule rule, Event event,AsyncCompletionCallback callback) { this.httpClient = httpClient; this.rule = rule; this.event = event; + this.callback = callback; } public void setDispatchRetries(int dispatchRetries) { @@ -61,16 +75,34 @@ public void run() { if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("Dispatching event: %s for webhook: %s", event.getEventType(), rule.getName())); } + if (event == null) { + LOGGER.warn(String.format("Invalid event received for dispatching webhook: %s", rule.getName())); + return; + } + Gson gson = new Gson(); + payload = gson.toJson(event); int attempt = 0; + boolean success = false; while (attempt < dispatchRetries) { attempt++; if (dispatch(attempt)) { + success = true; break; } } + callback.complete(new WebhookDispatchResult(payload, success, response, startTime)); } - private boolean dispatch(int attempt) { + protected void updateResponseFromRequest(InputStream is) { + try { + this.response = IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (IOException e) { + LOGGER.error(String.format("Failed to parse response for event: %s, webhook: %s having URL: %s", event.getEventType(), rule.getName(), rule.getPayloadUrl())); + } + } + + protected boolean dispatch(int attempt) { + startTime = new Date(); try { final URI uri = new URI(rule.getPayloadUrl()); HttpPost request = new HttpPost(); @@ -81,24 +113,74 @@ private boolean dispatch(int attempt) { request.setConfig(requestConfig.build()); request.setURI(uri); if (LOGGER.isTraceEnabled()) { - LOGGER.debug(String.format("Dispatching event: %s for webhook: %s on URL: %s with timeout: %d, attempt #%d", event.getEventType(), rule.getName(), rule.getPayloadUrl(), deliveryTimeout, attempt)); + LOGGER.trace(String.format("Dispatching event: %s for webhook: %s on URL: %s with timeout: %d, attempt #%d", event.getEventType(), rule.getName(), rule.getPayloadUrl(), deliveryTimeout, attempt)); } - if (event != null) { - Gson gson = new Gson(); - String js = gson.toJson(event); - StringEntity input = new StringEntity(js, ContentType.APPLICATION_JSON); - request.setEntity(input); + StringEntity input = new StringEntity(payload, ContentType.APPLICATION_JSON); + request.setEntity(input); + + if (!"RANDOM".equals(this.event.getEventCategory())) { //ToDo: Remove this debug + if (EventCategory.ACTION_EVENT.getName().equals(this.event.getEventCategory())) { + LOGGER.info(String.format("Successfully dispatched event: %s for webhook: %s", payload, rule.getName())); + response = "Success"; + return true; + } + response = "DEBUG"; + return false; } + final CloseableHttpResponse response = httpClient.execute(request); + updateResponseFromRequest(response.getEntity().getContent()); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { if (LOGGER.isTraceEnabled()) { - LOGGER.debug(String.format("Successfully dispatched event: %s for webhook: %s", event.getEventType(), rule.getName())); + LOGGER.trace(String.format("Successfully dispatched event: %s for webhook: %s", event.getEventType(), rule.getName())); } return true; } } catch (URISyntaxException | IOException e) { - LOGGER.warn(String.format("Failed to dispatch webhook: %s having URL: %s", rule.getName(), rule.getPayloadUrl())); + LOGGER.warn(String.format("Failed to dispatch webhook: %s having URL: %s, in attempt #%d due to: %s", + rule.getName(), rule.getPayloadUrl(), attempt, e.getMessage())); + response = String.format("Failed due to : %s", e.getMessage()); } return false; } + + public static class WebhookDispatchContext extends AsyncRpcContext { + private final WebhookRule rule; + + public WebhookDispatchContext(AsyncCompletionCallback callback, WebhookRule rule) { + super(callback); + this.rule = rule; + } + + public WebhookRule getRule() { + return rule; + } + } + + public class WebhookDispatchResult extends CommandResult { + private String payload; + private Date starTime; + private Date endTime; + + public WebhookDispatchResult(String payload, boolean success, String response, Date starTime) { + super(); + this.payload = payload; + this.setResult(response); + this.setSuccess(success); + this.starTime = starTime; + this.endTime = new Date(); + } + + public String getPayload() { + return payload; + } + + public Date getStarTime() { + return starTime; + } + + public Date getEndTime() { + return endTime; + } + } } diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java index ea382c02923d..eedf5fdcada2 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java @@ -40,5 +40,15 @@ public interface WebhookService extends PluggableService, Configurable { "Size of thread pool for webhook dispatchers", false, ConfigKey.Scope.Global); + ConfigKey WebhookDispatchHistoryLimit = new ConfigKey<>("Advanced", Integer.class, + "webhook.dispatch.history.limit", "100", + "Limit for number of webhook dispatches to keep in history", + true, ConfigKey.Scope.Global); + + ConfigKey WebhookDispatchHistoryCleanupInterval = new ConfigKey<>("Advanced", Integer.class, + "webhook.dispatch.history.cleanup.interval", "3600", + "Interval (in seconds) for cleaning up webhook dispatch history", + false, ConfigKey.Scope.Global); + void handleEvent(Event event); } 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 1be65cc65b37..40581c7e33f8 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 @@ -23,33 +23,52 @@ import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.naming.ConfigurationException; +import org.apache.cloudstack.framework.async.AsyncCallbackDispatcher; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.events.Event; +import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.mom.webhook.dao.WebhookDispatchDao; import org.apache.cloudstack.mom.webhook.dao.WebhookRuleDao; +import org.apache.cloudstack.mom.webhook.vo.WebhookDispatchVO; import org.apache.cloudstack.mom.webhook.vo.WebhookRuleVO; +import org.apache.cloudstack.utils.identity.ManagementServerNode; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; +import org.apache.log4j.Logger; +import com.cloud.cluster.ManagementServerHostVO; +import com.cloud.cluster.dao.ManagementServerHostDao; import com.cloud.utils.Pair; +import com.cloud.utils.component.ComponentContext; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.concurrency.NamedThreadFactory; +import com.cloud.utils.db.GlobalLock; public class WebhookServiceImpl extends ManagerBase implements WebhookService { + public static final Logger LOGGER = Logger.getLogger(WebhookApiServiceImpl.class.getName()); public static final String WEBHOOK_JOB_POOL_THREAD_PREFIX = "Webhook-Job-Executor"; private ExecutorService webhookJobExecutor; + private ScheduledExecutorService webhookDispatchCleanupExecutor; private CloseableHttpClient closeableHttpClient; @Inject WebhookRuleDao webhookRuleDao; + @Inject + protected WebhookDispatchDao webhookDispatchDao; + @Inject + ManagementServerHostDao managementServerHostDao; @Override public boolean configure(String name, Map params) throws ConfigurationException { try { webhookJobExecutor = Executors.newFixedThreadPool(WebhookDispatcherThreadPoolSize.value(), new NamedThreadFactory(WEBHOOK_JOB_POOL_THREAD_PREFIX)); + webhookDispatchCleanupExecutor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("Webhook-Dispatch-Cleanup-Worker")); closeableHttpClient = HttpClients.createDefault(); } catch (final Exception e) { throw new ConfigurationException("Unable to to configure WebhookServiceImpl"); @@ -57,6 +76,14 @@ public boolean configure(String name, Map params) throws Configu return true; } + @Override + public boolean start() { + long webhookDispatchCleanupInterval = WebhookDispatchHistoryCleanupInterval.value(); + webhookDispatchCleanupExecutor.scheduleWithFixedDelay(new WebhookDispatchCleanupWorker(), + webhookDispatchCleanupInterval, webhookDispatchCleanupInterval, TimeUnit.SECONDS); + return true; + } + @Override public boolean stop() { webhookJobExecutor.shutdown(); @@ -73,7 +100,9 @@ public ConfigKey[] getConfigKeys() { return new ConfigKey[]{ WebhookDeliveryTimeout, WebhookDispatchRetries, - WebhookDispatcherThreadPoolSize + WebhookDispatcherThreadPoolSize, + WebhookDispatchHistoryLimit, + WebhookDispatchHistoryCleanupInterval }; } @@ -103,11 +132,55 @@ protected List getDispatchJobs(Event event) { WebhookDeliveryTimeout.valueIn(rule.getDomainId()))); } Pair configs = domainConfigs.get(rule.getDomainId()); - WebhookDispatchThread job = new WebhookDispatchThread(closeableHttpClient, rule, event); + WebhookDispatchThread.WebhookDispatchContext context = + new WebhookDispatchThread.WebhookDispatchContext<>(null, rule); + AsyncCallbackDispatcher caller = + AsyncCallbackDispatcher.create(this); + caller.setCallback(caller.getTarget().dispatchCompleteCallback(null, null)) + .setContext(context); + WebhookDispatchThread job = new WebhookDispatchThread(closeableHttpClient, rule, event, caller); + job = ComponentContext.inject(job); job.setDispatchRetries(configs.first()); job.setDeliveryTimeout(configs.second()); jobs.add(job); } return jobs; } + + protected Void dispatchCompleteCallback( + AsyncCallbackDispatcher callback, + WebhookDispatchThread.WebhookDispatchContext context) { + WebhookDispatchThread.WebhookDispatchResult result = callback.getResult(); + WebhookRule rule = context.getRule(); + WebhookDispatchVO dispatchVO = new WebhookDispatchVO(rule.getId(), ManagementServerNode.getManagementServerId(), + result.getPayload(), result.isSuccess(), result.getResult(), result.getStarTime(), + result.getEndTime()); + webhookDispatchDao.persist(dispatchVO); + return null; + } + + + public class WebhookDispatchCleanupWorker extends ManagedContextRunnable { + @Override + protected void runInContext() { + GlobalLock gcLock = GlobalLock.getInternLock("WebhookDispatchHistoryCleanup"); + try { + if (gcLock.lock(3)) { + try { + ManagementServerHostVO msHost = managementServerHostDao.findOneByLongestRuntime(); + if (msHost == null || (msHost.getMsid() != ManagementServerNode.getManagementServerId())) { + LOGGER.trace("Skipping the webhook dispatch cleanup task on this management server"); + return; + } + long limit = WebhookDispatchHistoryLimit.value(); + webhookDispatchDao.removeOlderDispatches(limit); + } finally { + gcLock.unlock(); + } + } + } finally { + gcLock.releaseRef(); + } + } + } } diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookDispatchHistoryCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookDispatchHistoryCmd.java new file mode 100644 index 000000000000..0caba0c18563 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookDispatchHistoryCmd.java @@ -0,0 +1,104 @@ +// 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.mom.webhook.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.ManagementServerResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.mom.webhook.WebhookApiService; +import org.apache.cloudstack.mom.webhook.WebhookDispatch; +import org.apache.cloudstack.mom.webhook.api.response.WebhookDispatchResponse; +import org.apache.cloudstack.mom.webhook.api.response.WebhookRuleResponse; + +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = "deleteWebhookDispatchHistory", + description = "Deletes Webhook dispatch history", + responseObject = SuccessResponse.class, + entityType = {WebhookDispatch.class}, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "4.20.0") +public class DeleteWebhookDispatchHistoryCmd extends BaseCmd { + + @Inject + WebhookApiService webhookApiService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.ID, type = BaseCmd.CommandType.UUID, + entityType = WebhookDispatchResponse.class, + description = "The ID of the Webhook dispatch") + private Long id; + + @Parameter(name = ApiConstants.WEBHOOK_RULE_ID, type = BaseCmd.CommandType.UUID, + entityType = WebhookRuleResponse.class, + description = "The ID of the Webhook rule") + private Long webhookRuleId; + + @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, type = BaseCmd.CommandType.UUID, + entityType = ManagementServerResponse.class, + description = "The ID of the management server", + authorized = {RoleType.Admin}) + private Long managementServerId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + public Long getId() { + return id; + } + + public Long getWebhookRuleId() { + return webhookRuleId; + } + + public Long getManagementServerId() { + return managementServerId; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccountId(); + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + @Override + public void execute() throws ServerApiException { + try { + if (!webhookApiService.deleteWebhookDispatchHistory(this)) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete webhook dispatch history"); + } + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } catch (CloudRuntimeException ex) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage()); + } + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookRuleCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookRuleCmd.java index 600b35cb0ce9..68d1f8674b65 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookRuleCmd.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookRuleCmd.java @@ -31,7 +31,6 @@ import org.apache.cloudstack.mom.webhook.WebhookApiService; import org.apache.cloudstack.mom.webhook.WebhookRule; import org.apache.cloudstack.mom.webhook.api.response.WebhookRuleResponse; -import org.apache.log4j.Logger; import com.cloud.utils.exception.CloudRuntimeException; @@ -39,9 +38,9 @@ description = "Delete a Webhook rule", responseObject = SuccessResponse.class, entityType = {WebhookRule.class}, - authorized = {RoleType.Admin}) + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "4.20.0") public class DeleteWebhookRuleCmd extends BaseCmd { - public static final Logger LOGGER = Logger.getLogger(DeleteWebhookRuleCmd.class.getName()); @Inject WebhookApiService webhookApiService; diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookDispatchHistoryCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookDispatchHistoryCmd.java new file mode 100644 index 000000000000..984c8f46dc3e --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookDispatchHistoryCmd.java @@ -0,0 +1,92 @@ +// 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.mom.webhook.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.BaseCmd; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ManagementServerResponse; +import org.apache.cloudstack.mom.webhook.WebhookApiService; +import org.apache.cloudstack.mom.webhook.WebhookDispatch; +import org.apache.cloudstack.mom.webhook.api.response.WebhookDispatchResponse; +import org.apache.cloudstack.mom.webhook.api.response.WebhookRuleResponse; + +@APICommand(name = "listWebhookDispatchHistory", + description = "Lists Webhook dispatch history", + responseObject = WebhookRuleResponse.class, + responseView = ResponseObject.ResponseView.Restricted, + entityType = {WebhookDispatch.class}, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "4.20.0") +public class ListWebhookDispatchHistoryCmd extends BaseListCmd { + + @Inject + WebhookApiService webhookApiService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.ID, type = BaseCmd.CommandType.UUID, + entityType = WebhookDispatchResponse.class, + description = "The ID of the Webhook dispatch") + private Long id; + + @Parameter(name = ApiConstants.WEBHOOK_RULE_ID, type = BaseCmd.CommandType.UUID, + entityType = WebhookRuleResponse.class, + description = "The ID of the Webhook rule") + private Long webhookRuleId; + + @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, type = BaseCmd.CommandType.UUID, + entityType = ManagementServerResponse.class, + description = "The ID of the management server", + authorized = {RoleType.Admin}) + private Long managementServerId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + public Long getId() { + return id; + } + + public Long getWebhookRuleId() { + return webhookRuleId; + } + + public Long getManagementServerId() { + return managementServerId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + @Override + public void execute() throws ServerApiException { + ListResponse response = webhookApiService.listWebhookDispatchHistory(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookRulesCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookRulesCmd.java index b62d61bf0750..5e2f5b3b4a28 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookRulesCmd.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookRulesCmd.java @@ -36,8 +36,9 @@ description = "Lists Webhook rules", responseObject = WebhookRuleResponse.class, responseView = ResponseObject.ResponseView.Restricted, + entityType = {WebhookRule.class}, authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, - entityType = {WebhookRule.class}) + since = "4.20.0") public class ListWebhookRulesCmd extends BaseListProjectAndAccountResourcesCmd { @Inject diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookRuleCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookRuleCmd.java index 1b21633f4b32..49678012a6e1 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookRuleCmd.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookRuleCmd.java @@ -38,7 +38,8 @@ description = "Update a Webhook rule", responseObject = SuccessResponse.class, entityType = {WebhookRule.class}, - authorized = {RoleType.Admin}) + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "4.20.0") public class UpdateWebhookRuleCmd extends BaseCmd { @Inject diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookDispatchResponse.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookDispatchResponse.java new file mode 100644 index 000000000000..fef2be5b02e7 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookDispatchResponse.java @@ -0,0 +1,104 @@ +// 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.mom.webhook.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.mom.webhook.WebhookDispatch; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = {WebhookDispatch.class}) +public class WebhookDispatchResponse extends BaseResponse { + @SerializedName(ApiConstants.ID) + @Param(description = "The ID of the Webhook dispatch") + private String id; + + @SerializedName(ApiConstants.WEBHOOK_RULE_ID) + @Param(description = "The ID of the Webhook rule") + private String webhookRuleId; + + @SerializedName(ApiConstants.WEBHOOK_RULE_NAME) + @Param(description = "The name of the Webhook rule") + private String webhookRuleName; + + @SerializedName(ApiConstants.MANAGEMENT_SERVER_ID) + @Param(description = "The ID of the management server which executed dispatch") + private String managementServerId; + + @SerializedName(ApiConstants.PAYLOAD) + @Param(description = "The payload of the webhook dispatch") + private String payload; + + @SerializedName(ApiConstants.SUCCESS) + @Param(description = "Whether Webhook dispatch succeeded or not") + private boolean success; + + @SerializedName(ApiConstants.RESPONSE) + @Param(description = "The response of the webhook dispatch") + private String response; + + @SerializedName(ApiConstants.START_DATE) + @Param(description = "The start time of the Webhook dispatch") + private Date startTime; + + @SerializedName(ApiConstants.END_DATE) + @Param(description = "The end time of the Webhook dispatch") + private Date endTime; + + public void setId(String id) { + this.id = id; + } + + public void setWebhookRuleId(String webhookRuleId) { + this.webhookRuleId = webhookRuleId; + } + + public void setWebhookRuleName(String webhookRuleName) { + this.webhookRuleName = webhookRuleName; + } + + public void setManagementServerId(String managementServerId) { + this.managementServerId = managementServerId; + } + + public void setPayload(String payload) { + this.payload = payload; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public void setResponse(String response) { + this.response = response; + } + + public void setStartTime(Date startTime) { + this.startTime = startTime; + } + + public void setEndTime(Date endTime) { + this.endTime = endTime; + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookRuleResponse.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookRuleResponse.java index 20fde7109014..5356130daefe 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookRuleResponse.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookRuleResponse.java @@ -31,7 +31,7 @@ @EntityReference(value = {WebhookRule.class}) public class WebhookRuleResponse extends BaseResponseWithAnnotations implements ControlledEntityResponse { @SerializedName(ApiConstants.ID) - @Param(description = "The id of the Webhook rule") + @Param(description = "The ID of the Webhook rule") private String id; @SerializedName(ApiConstants.NAME) diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookDispatchDao.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookDispatchDao.java new file mode 100644 index 000000000000..517e4bdd45db --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookDispatchDao.java @@ -0,0 +1,26 @@ +// 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.mom.webhook.dao; + +import org.apache.cloudstack.mom.webhook.vo.WebhookDispatchVO; + +import com.cloud.utils.db.GenericDao; + +public interface WebhookDispatchDao extends GenericDao { + void removeOlderDispatches(long limit); +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookDispatchDaoImpl.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookDispatchDaoImpl.java new file mode 100644 index 000000000000..9846c651a24e --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookDispatchDaoImpl.java @@ -0,0 +1,40 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.mom.webhook.dao; + +import java.util.List; + +import org.apache.cloudstack.mom.webhook.vo.WebhookDispatchVO; + +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 WebhookDispatchDaoImpl extends GenericDaoBase implements WebhookDispatchDao { + @Override + public void removeOlderDispatches(long limit) { + Filter searchFilter = new Filter(WebhookDispatchVO.class, "id", false, 0L, limit); + List keep = listAll(searchFilter); + SearchBuilder sb = createSearchBuilder(); + sb.and("id", sb.entity().getId(), SearchCriteria.Op.NOTIN); + SearchCriteria sc = sb.create(); + sc.setParameters("id", keep.stream().map(WebhookDispatchVO::getId).toArray()); + remove(sc); + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookDispatchVO.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookDispatchVO.java new file mode 100644 index 000000000000..05784cb78c85 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookDispatchVO.java @@ -0,0 +1,135 @@ +// 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.mom.webhook.vo; + + +import java.util.Date; +import java.util.UUID; + +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 javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.apache.cloudstack.mom.webhook.WebhookDispatch; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +@Entity +@Table(name = "webhook_dispatch") +public class WebhookDispatchVO implements WebhookDispatch { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "webhook_id") + private long webhookRuleId; + + @Column(name = "mshost_msid") + private long mangementServerId; + + @Column(name = "payload", length = 65535) + private String payload; + + @Column(name = "success") + private boolean success; + + @Column(name = "response", length = 65535) + private String response; + + @Column(name = "start_time") + @Temporal(value = TemporalType.TIMESTAMP) + private Date startTime; + + @Column(name = "end_time") + @Temporal(value = TemporalType.TIMESTAMP) + private Date endTime; + + @Override + public long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + + @Override + public long getWebhookRuleId() { + return webhookRuleId; + } + + @Override + public long getManagementServerId() { + return 0; + } + + @Override + public String getPayload() { + return payload; + } + + @Override + public boolean isSuccess() { + return success; + } + + @Override + public String getResponse() { + return response; + } + + @Override + public Date getStartTime() { + return startTime; + } + + @Override + public Date getEndTime() { + return endTime; + } + + @Override + public String toString() { + return String.format("WebhookDispatch [%s]", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "uuid", "webhookRuleId", "endTime", "success")); + } + + public WebhookDispatchVO() { + this.uuid = UUID.randomUUID().toString(); + } + + public WebhookDispatchVO(long webhookRuleId, long managementServerId, String payload, boolean success, + String response, Date startTime, Date endTime) { + this.uuid = UUID.randomUUID().toString(); + this.webhookRuleId = webhookRuleId; + this.mangementServerId = managementServerId; + this.payload = payload; + this.success = success; + this.response = response; + this.startTime = startTime; + this.endTime = endTime; + } +} diff --git a/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/module.properties b/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/module.properties index 7f7a61540381..299144ff82a2 100644 --- a/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/module.properties +++ b/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/module.properties @@ -15,4 +15,4 @@ # specific language governing permissions and limitations # under the License. name=webhook -parent=core +parent=event diff --git a/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/spring-webhook-context.xml b/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/spring-event-webhook-context.xml similarity index 87% rename from plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/spring-webhook-context.xml rename to plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/spring-event-webhook-context.xml index 0248a9648a61..36f7ffa4ea1d 100644 --- a/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/spring-webhook-context.xml +++ b/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/spring-event-webhook-context.xml @@ -28,11 +28,12 @@ > + - - - + + + From a06b8f66d89f8ee054a1be3d8e2913ce341edfe6 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 15 Feb 2024 00:40:28 +0530 Subject: [PATCH 11/75] changes, initial ui Signed-off-by: Abhishek Kumar --- .../cloudstack/framework/events/Event.java | 9 ++ .../mom/webhook/WebhookApiServiceImpl.java | 32 ++++++- .../mom/webhook/WebhookDispatchThread.java | 41 ++++++++- .../command/user/CreateWebhookRuleCmd.java | 4 +- .../api/command/user/ListWebhookRulesCmd.java | 10 +++ .../com/cloud/event/ActionEventUtils.java | 14 ++-- ui/public/locales/en.json | 7 ++ ui/src/components/view/SearchView.vue | 37 ++++++++ ui/src/config/section/tools.js | 84 +++++++++++++++++++ 9 files changed, 226 insertions(+), 12 deletions(-) diff --git a/framework/events/src/main/java/org/apache/cloudstack/framework/events/Event.java b/framework/events/src/main/java/org/apache/cloudstack/framework/events/Event.java index bd1ea2aa5f95..e1ef83c9e0ec 100644 --- a/framework/events/src/main/java/org/apache/cloudstack/framework/events/Event.java +++ b/framework/events/src/main/java/org/apache/cloudstack/framework/events/Event.java @@ -23,6 +23,7 @@ public class Event { + String eventUuid; String eventCategory; String eventType; String eventSource; @@ -38,6 +39,14 @@ public Event(String eventSource, String eventCategory, String eventType, String setResourceUUID(resourceUUID); } + public String getEventUuid() { + return eventUuid; + } + + public void setEventUuid(String eventUuid) { + this.eventUuid = eventUuid; + } + public String getEventCategory() { return eventCategory; } diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java index 32d904a08ae7..d67dbeaa93f1 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java @@ -181,9 +181,10 @@ public ListResponse listWebhookRules(ListWebhookRulesCmd cm final CallContext ctx = CallContext.current(); final Account caller = ctx.getCallingAccount(); final Long clusterId = cmd.getId(); - final String state = cmd.getState(); + final String stateStr = cmd.getState(); final String name = cmd.getName(); final String keyword = cmd.getKeyword(); + final String scopeStr = cmd.getScope(); List responsesList = new ArrayList<>(); List permittedAccounts = new ArrayList<>(); Ternary domainIdRecursiveListProject = new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null); @@ -199,11 +200,36 @@ public ListResponse listWebhookRules(ListWebhookRulesCmd cm 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("state", sb.entity().getState(), SearchCriteria.Op.IN); + sb.and("state", sb.entity().getState(), SearchCriteria.Op.EQ); + sb.and("scope", sb.entity().getState(), SearchCriteria.Op.EQ); SearchCriteria sc = sb.create(); accountManager.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + WebhookRule.Scope scope = null; + if (StringUtils.isNotEmpty(scopeStr)) { + try { + scope = WebhookRule.Scope.valueOf(scopeStr); + } catch (IllegalArgumentException iae) { + throw new InvalidParameterValueException("Invalid scope specified"); + } + } + if ((WebhookRule.Scope.Global.equals(scope) && !Account.Type.ADMIN.equals(caller.getType())) || + (WebhookRule.Scope.Domain.equals(scope) && + !List.of(Account.Type.ADMIN, Account.Type.DOMAIN_ADMIN).contains(caller.getType()))) { + throw new InvalidParameterValueException(String.format("Scope %s can not be specified", scope)); + } + WebhookRule.State state = null; + if (StringUtils.isNotEmpty(stateStr)) { + try { + state = WebhookRule.State.valueOf(stateStr); + } catch (IllegalArgumentException iae) { + throw new InvalidParameterValueException("Invalid state specified"); + } + } + if (scope != null) { + sc.setParameters("scope", scope.name()); + } if (state != null) { - sc.setParameters("state", state); + sc.setParameters("state", state.name()); } if(keyword != null){ sc.setParameters("keyword", "%" + keyword + "%"); diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatchThread.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatchThread.java index 929671acb4d1..416769ee88cc 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatchThread.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatchThread.java @@ -19,17 +19,28 @@ import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.Date; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + import org.apache.cloudstack.framework.async.AsyncCompletionCallback; import org.apache.cloudstack.framework.async.AsyncRpcContext; import org.apache.cloudstack.framework.events.Event; import org.apache.cloudstack.storage.command.CommandResult; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Hex; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.io.IOUtils; +import org.apache.http.HttpHeaders; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; @@ -44,6 +55,10 @@ public class WebhookDispatchThread implements Runnable { private static final Logger LOGGER = Logger.getLogger(WebhookDispatchThread.class); + private static final String HEADER_X_CS_EVENT_ID = "X-CS-Event-ID"; + private static final String HEADER_X_CS_EVENT = "X-CS-Event"; + private static final String HEADER_X_CS_SIGNATURE = "X-CS-Signature"; + private final CloseableHttpClient httpClient; private WebhookRule rule; private Event event; @@ -112,13 +127,19 @@ protected boolean dispatch(int attempt) { requestConfig.setSocketTimeout(deliveryTimeout * 1000); request.setConfig(requestConfig.build()); request.setURI(uri); + + request.setHeader(HEADER_X_CS_EVENT, event.getEventType()); + request.setHeader(HEADER_X_CS_EVENT, event.getEventType()); + request.setHeader(HttpHeaders.USER_AGENT, String.format("CS-Hookshot/%s", event.getEventUuid())); + + if (LOGGER.isTraceEnabled()) { LOGGER.trace(String.format("Dispatching event: %s for webhook: %s on URL: %s with timeout: %d, attempt #%d", event.getEventType(), rule.getName(), rule.getPayloadUrl(), deliveryTimeout, attempt)); } StringEntity input = new StringEntity(payload, ContentType.APPLICATION_JSON); request.setEntity(input); - if (!"RANDOM".equals(this.event.getEventCategory())) { //ToDo: Remove this debug + if (!"VirtualMachine".equals(this.event.getResourceType())) { if (EventCategory.ACTION_EVENT.getName().equals(this.event.getEventCategory())) { LOGGER.info(String.format("Successfully dispatched event: %s for webhook: %s", payload, rule.getName())); response = "Success"; @@ -144,6 +165,24 @@ protected boolean dispatch(int attempt) { return false; } + public static String getHac(String dataUno, String keyUno) + throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException, DecoderException { + + SecretKey secretKey; + Mac mac = Mac.getInstance("HMACSHA256"); + + byte[] keyBytes = Hex.decodeHex(keyUno); + + secretKey = new SecretKeySpec(keyBytes, mac.getAlgorithm()); + + mac.init(secretKey); + + byte[] text = dataUno.getBytes("UTF-8"); + + byte[] encodedText = mac.doFinal(text); + return new String(Base64.encodeBase64(encodedText)).trim(); + } + public static class WebhookDispatchContext extends AsyncRpcContext { private final WebhookRule rule; diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/CreateWebhookRuleCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/CreateWebhookRuleCmd.java index 2881ffc65753..e06540f9c697 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/CreateWebhookRuleCmd.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/CreateWebhookRuleCmd.java @@ -39,7 +39,7 @@ import com.cloud.utils.exception.CloudRuntimeException; -@APICommand(name = "createWebhook", +@APICommand(name = "createWebhookRule", description = "Creates a Webhook rule", responseObject = WebhookRuleResponse.class, responseView = ResponseObject.ResponseView.Restricted, @@ -94,7 +94,7 @@ public class CreateWebhookRuleCmd extends BaseCmd { private Boolean sslVerification; @Parameter(name = ApiConstants.SCOPE, type = BaseCmd.CommandType.STRING, description = "Scope of the Webhook rule", - authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin}) + authorized = {RoleType.Admin, RoleType.DomainAdmin}) private String scope; ///////////////////////////////////////////////////// diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookRulesCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookRulesCmd.java index 5e2f5b3b4a28..dadcdb85da9b 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookRulesCmd.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookRulesCmd.java @@ -58,6 +58,12 @@ public class ListWebhookRulesCmd extends BaseListProjectAndAccountResourcesCmd { @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "The name of the Webhook rule") private String name; + @Parameter(name = ApiConstants.SCOPE, + type = CommandType.STRING, + description = "The scope of the Webhook rule", + authorized = {RoleType.Admin, RoleType.DomainAdmin}) + private String scope; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -73,6 +79,10 @@ public String getName() { return name; } + public String getScope() { + return scope; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/server/src/main/java/com/cloud/event/ActionEventUtils.java b/server/src/main/java/com/cloud/event/ActionEventUtils.java index b0508eb4b7e2..94d79d310ef9 100644 --- a/server/src/main/java/com/cloud/event/ActionEventUtils.java +++ b/server/src/main/java/com/cloud/event/ActionEventUtils.java @@ -101,8 +101,8 @@ void init() { public static Long onActionEvent(Long userId, Long accountId, Long domainId, String type, String description, Long resourceId, String resourceType) { Ternary resourceDetails = getResourceDetails(resourceId, resourceType, type); - publishOnEventBus(userId, accountId, EventCategory.ACTION_EVENT.getName(), type, com.cloud.event.Event.State.Completed, description, resourceDetails.second(), resourceDetails.third()); Event event = persistActionEvent(userId, accountId, domainId, null, type, Event.State.Completed, true, description, resourceDetails.first(), resourceDetails.third(), null); + publishOnEventBus(event.getUuid(), userId, accountId, EventCategory.ACTION_EVENT.getName(), type, com.cloud.event.Event.State.Completed, description, resourceDetails.second(), resourceDetails.third()); return event.getId(); } @@ -111,8 +111,8 @@ public static Long onActionEvent(Long userId, Long accountId, Long domainId, Str */ public static Long onScheduledActionEvent(Long userId, Long accountId, String type, String description, Long resourceId, String resourceType, boolean eventDisplayEnabled, long startEventId) { Ternary resourceDetails = getResourceDetails(resourceId, resourceType, type); - publishOnEventBus(userId, accountId, EventCategory.ACTION_EVENT.getName(), type, com.cloud.event.Event.State.Scheduled, description, resourceDetails.second(), resourceDetails.third()); Event event = persistActionEvent(userId, accountId, null, null, type, Event.State.Scheduled, eventDisplayEnabled, description, resourceDetails.first(), resourceDetails.third(), startEventId); + publishOnEventBus(event.getUuid(), userId, accountId, EventCategory.ACTION_EVENT.getName(), type, com.cloud.event.Event.State.Scheduled, description, resourceDetails.second(), resourceDetails.third()); return event.getId(); } @@ -136,8 +136,8 @@ public static void onStartedActionEventFromContext(String eventType, String even */ public static Long onStartedActionEvent(Long userId, Long accountId, String type, String description, Long resourceId, String resourceType, boolean eventDisplayEnabled, long startEventId) { Ternary resourceDetails = getResourceDetails(resourceId, resourceType, type); - publishOnEventBus(userId, accountId, EventCategory.ACTION_EVENT.getName(), type, com.cloud.event.Event.State.Started, description, resourceDetails.second(), resourceDetails.third()); Event event = persistActionEvent(userId, accountId, null, null, type, Event.State.Started, eventDisplayEnabled, description, resourceDetails.first(), resourceDetails.third(), startEventId); + publishOnEventBus(event.getUuid(), userId, accountId, EventCategory.ACTION_EVENT.getName(), type, com.cloud.event.Event.State.Started, description, resourceDetails.second(), resourceDetails.third()); return event.getId(); } @@ -148,16 +148,16 @@ public static Long onCompletedActionEvent(Long userId, Long accountId, String le public static Long onCompletedActionEvent(Long userId, Long accountId, String level, String type, boolean eventDisplayEnabled, String description, Long resourceId, String resourceType, long startEventId) { Ternary resourceDetails = getResourceDetails(resourceId, resourceType, type); - publishOnEventBus(userId, accountId, EventCategory.ACTION_EVENT.getName(), type, com.cloud.event.Event.State.Completed, description, resourceDetails.second(), resourceDetails.third()); Event event = persistActionEvent(userId, accountId, null, level, type, Event.State.Completed, eventDisplayEnabled, description, resourceDetails.first(), resourceDetails.third(), startEventId); + publishOnEventBus(event.getUuid(), userId, accountId, EventCategory.ACTION_EVENT.getName(), type, com.cloud.event.Event.State.Completed, description, resourceDetails.second(), resourceDetails.third()); return event.getId(); } public static Long onCreatedActionEvent(Long userId, Long accountId, String level, String type, boolean eventDisplayEnabled, String description, Long resourceId, String resourceType) { Ternary resourceDetails = getResourceDetails(resourceId, resourceType, type); - publishOnEventBus(userId, accountId, EventCategory.ACTION_EVENT.getName(), type, com.cloud.event.Event.State.Created, description, resourceDetails.second(), resourceDetails.third()); Event event = persistActionEvent(userId, accountId, null, level, type, Event.State.Created, eventDisplayEnabled, description, resourceDetails.first(), resourceDetails.third(), null); + publishOnEventBus(event.getUuid(), userId, accountId, EventCategory.ACTION_EVENT.getName(), type, com.cloud.event.Event.State.Created, description, resourceDetails.second(), resourceDetails.third()); return event.getId(); } @@ -193,7 +193,8 @@ private static Event persistActionEvent(Long userId, Long accountId, Long domain return event; } - private static void publishOnEventBus(long userId, long accountId, String eventCategory, String eventType, Event.State state, String description, String resourceUuid, String resourceType) { + private static void publishOnEventBus(String eventUuid, long userId, long accountId, String eventCategory, + String eventType, Event.State state, String description, String resourceUuid, String resourceType) { String configKey = Config.PublishActionEvent.key(); String value = s_configDao.getValue(configKey); boolean configValue = Boolean.parseBoolean(value); @@ -208,6 +209,7 @@ private static void publishOnEventBus(long userId, long accountId, String eventC org.apache.cloudstack.framework.events.Event event = new org.apache.cloudstack.framework.events.Event(ManagementService.Name, eventCategory, eventType, resourceType, resourceUuid); + event.setEventUuid(eventUuid); Map eventDescription = new HashMap(); Project project = s_projectDao.findByProjectAccountId(accountId); diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 71da3c6d0aae..73a22b8b9bfe 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -574,6 +574,7 @@ "label.create.tungsten.routing.policy": "Create Tungsten-Fabric routing policy", "label.create.user": "Create User", "label.create.vpn.connection": "Create VPN connection", +"label.create.webhook": "Create Webhook", "label.created": "Created", "label.creating": "Creating", "label.creating.iprange": "Creating IP ranges", @@ -662,6 +663,7 @@ "label.delete.vpn.customer.gateway": "Delete VPN customer gateway", "label.delete.vpn.gateway": "Delete VPN gateway", "label.delete.vpn.user": "Delete VPN User", +"label.delete.webhook": "Delete Webhook", "label.deleteconfirm": "Please confirm that you would like to delete this", "label.deleting": "Deleting", "label.deleting.failed": "Deleting failed", @@ -718,6 +720,7 @@ "label.disable.storage": "Disable storage pool", "label.disable.vpc.offering": "Disable VPC offering", "label.disable.vpn": "Disable remote access VPN", +"label.disable.webhook": "Disable Webhook", "label.disabled": "Disabled", "label.disconnected": "Last disconnected", "label.disk": "Disk", @@ -824,6 +827,7 @@ "label.enable.storage": "Enable storage pool", "label.enable.vpc.offering": "Enable VPC offering", "label.enable.vpn": "Enable remote access VPN", +"label.enable.webhook": "Enable Webhook", "label.enabled": "Enabled", "label.encrypt": "Encrypt", "label.encryptroot": "Encrypt Root Disk", @@ -1520,6 +1524,7 @@ "label.patp": "Palo Alto threat profile", "label.pavr": "Virtual router", "label.payload": "Payload", +"label.payloadurl": "Payload URL", "label.pcidevice": "GPU", "label.pending.jobs": "Pending Jobs", "label.per.account": "Per Account", @@ -2169,6 +2174,7 @@ "label.update.to": "updated to", "label.update.traffic.label": "Update traffic labels", "label.update.vmware.datacenter": "Update VMWare datacenter", +"label.update.webhook": "Update Webhook", "label.updating": "Updating", "label.upgrade.router.newer.template": "Upgrade router to use newer Template", "label.upload": "Upload", @@ -2346,6 +2352,7 @@ "label.warn": "Warn", "label.warn.upper": "WARN", "label.warning": "Warning", +"label.webhooks": "Webhooks", "label.wednesday": "Wednesday", "label.weekly": "Weekly", "label.welcome": "Welcome", diff --git a/ui/src/components/view/SearchView.vue b/ui/src/components/view/SearchView.vue index 8e5e00887659..6bea2808cc70 100644 --- a/ui/src/components/view/SearchView.vue +++ b/ui/src/components/view/SearchView.vue @@ -154,6 +154,7 @@ diff --git a/ui/src/config/section/tools.js b/ui/src/config/section/tools.js index ae0747db3a03..276e6075c25e 100644 --- a/ui/src/config/section/tools.js +++ b/ui/src/config/section/tools.js @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. import store from '@/store' +import { shallowRef, defineAsyncComponent } from 'vue' export default { name: 'tools', @@ -80,6 +81,9 @@ export default { if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) { cols.push('scope') } + if (store.getters.listAllProjects) { + cols.push('project') + } return cols }, details: ['name', 'id', 'description', 'scope', 'payloadurl', 'sslverification', 'secret', 'state', 'account', 'domainid'], @@ -90,19 +94,38 @@ export default { } return filters }, - filters: () => { - const filters = ['self', 'all'] - return filters - }, + tabs: [ + { + name: 'details', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue'))) + }, + { + name: 'history', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/WebhookDispatchHistoryTab.vue'))) + } + ], actions: [ { api: 'createWebhookRule', icon: 'plus-outlined', label: 'label.create.webhook', - message: 'message.webhook.create', docHelp: 'adminguide/events.html#creating-webhooks', listView: true, - args: ['name', 'description', 'payloadurl', 'sslverification', 'secret'] + args: (record, store) => { + var fields = ['name', 'description', 'payloadurl', 'sslverification', 'secretkey', 'state'] + if (['Admin', 'DomainAdmin'].includes(store.userInfo.roletype)) { + fields.push('scope') + } + return fields + }, + mapping: { + state: { + options: ['Enabled', 'Disabled'] + }, + scope: { + options: ['Local', 'Domain', 'Global'] + } + } }, { api: 'updateWebhookRule', @@ -110,7 +133,12 @@ export default { label: 'label.update.webhook', dataView: true, popup: true, - args: ['name', 'description', 'payloadurl', 'sslverification', 'secret'] + args: ['name', 'description', 'payloadurl', 'sslverification', 'secretkey', 'state'], + mapping: { + state: { + options: ['Enabled', 'Disabled'] + } + } }, { api: 'updateWebhookRule', @@ -152,6 +180,27 @@ export default { groupMap: (selection) => { return selection.map(x => { return { id: x } }) } } ] + }, + { + name: 'webhookhistory', + title: 'label.webhook.history', + icon: 'gateway-outlined', + hidden: true, + permission: ['listWebhookDispatchHistory'], + columns: () => { + const cols = ['eventtype', 'payload', 'webhookrulename', 'success', 'response', 'startdate', 'enddate'] + if (['Admin'].includes(store.getters.userInfo.roletype)) { + cols.splice(3, 0, 'managementservername') + } + return cols + }, + details: () => { + const fields = ['id', 'eventid', 'eventtype', 'payload', 'success', 'response', 'startdate', 'enddate'] + if (['Admin'].includes(store.getters.userInfo.roletype)) { + fields.splice(1, 0, 'managementserverid', 'managementservername') + } + return fields + } } ] } diff --git a/ui/src/views/AutogenView.vue b/ui/src/views/AutogenView.vue index bf1c42d4c05f..eedff4e227a8 100644 --- a/ui/src/views/AutogenView.vue +++ b/ui/src/views/AutogenView.vue @@ -795,7 +795,7 @@ export default { this.projectView = Boolean(store.getters.project && store.getters.project.id) this.hasProjectId = ['vm', 'vmgroup', 'ssh', 'affinitygroup', 'volume', 'snapshot', 'vmsnapshot', 'guestnetwork', 'vpc', 'securitygroups', 'publicip', 'vpncustomergateway', 'template', 'iso', 'event', 'kubernetes', - 'autoscalevmgroup', 'vnfapp'].includes(this.$route.name) + 'autoscalevmgroup', 'vnfapp', 'webhook'].includes(this.$route.name) if ((this.$route && this.$route.params && this.$route.params.id) || this.$route.query.dataView) { this.dataView = true From 23621eb77906c8991d15f54012e7014b5d6e614d Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 16 Feb 2024 08:30:47 +0530 Subject: [PATCH 13/75] changes for account webhook cleanup Signed-off-by: Abhishek Kumar --- .../mom/webhook/WebhookServiceImpl.java | 8 ++++++- .../mom/webhook/dao/WebhookRuleDao.java | 1 + .../mom/webhook/dao/WebhookRuleDaoImpl.java | 9 ++++++++ .../com/cloud/user/AccountManagerImpl.java | 13 +++++++++-- .../cloudstack/webhook/WebhookHelper.java | 22 +++++++++++++++++++ 5 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 server/src/main/java/org/apache/cloudstack/webhook/WebhookHelper.java 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 9293a1716f62..552e3a20c64d 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 @@ -38,6 +38,7 @@ import org.apache.cloudstack.mom.webhook.vo.WebhookDispatchVO; import org.apache.cloudstack.mom.webhook.vo.WebhookRuleVO; import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.cloudstack.webhook.WebhookHelper; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.log4j.Logger; @@ -52,7 +53,7 @@ import com.cloud.utils.concurrency.NamedThreadFactory; import com.cloud.utils.db.GlobalLock; -public class WebhookServiceImpl extends ManagerBase implements WebhookService { +public class WebhookServiceImpl extends ManagerBase implements WebhookService, WebhookHelper { public static final Logger LOGGER = Logger.getLogger(WebhookApiServiceImpl.class.getName()); public static final String WEBHOOK_JOB_POOL_THREAD_PREFIX = "Webhook-Job-Executor"; private ExecutorService webhookJobExecutor; @@ -168,6 +169,11 @@ protected Void dispatchCompleteCallback( return null; } + @Override + public void deleteRulesForAccount(long accountId) { + webhookRuleDao.deleteByAccount(accountId); + } + public class WebhookDispatchCleanupWorker extends ManagedContextRunnable { @Override diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookRuleDao.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookRuleDao.java index 90bfa7f4fe48..fc8b4bbc9d7a 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookRuleDao.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookRuleDao.java @@ -25,4 +25,5 @@ public interface WebhookRuleDao extends GenericDao { List listByEnabledRulesForDispatch(Long accountId, List domainIds); + void deleteByAccount(long accountId); } diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookRuleDaoImpl.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookRuleDaoImpl.java index ad85f274d6e6..70e1dad94ef6 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookRuleDaoImpl.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookRuleDaoImpl.java @@ -57,4 +57,13 @@ public List listByEnabledRulesForDispatch(Long accountId, List sb = createSearchBuilder(); + sb.and("accountId", sb.entity().getAccountId(), SearchCriteria.Op.EQ); + SearchCriteria sc = sb.create(); + sc.setParameters("accountId", accountId); + remove(sc); + } } diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 6e764620033f..f35153196e41 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -77,12 +77,14 @@ import org.apache.cloudstack.resourcedetail.UserDetailVO; import org.apache.cloudstack.resourcedetail.dao.UserDetailsDao; import org.apache.cloudstack.utils.baremetal.BaremetalUtils; +import org.apache.cloudstack.webhook.WebhookHelper; import org.apache.commons.codec.binary.Base64; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import com.cloud.api.ApiDBUtils; import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd; @@ -169,6 +171,7 @@ import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; import com.cloud.utils.Ternary; +import com.cloud.utils.component.ComponentContext; import com.cloud.utils.component.Manager; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.component.PluggableService; @@ -1102,8 +1105,14 @@ public int compare(NetworkVO network1, NetworkVO network2) { userDataDao.removeByAccountId(accountId); // Delete WebhookRules - //ToDo: webhook delete - + try { + WebhookHelper webhookService = ComponentContext.getComponent(WebhookHelper.class); + webhookService.deleteRulesForAccount(accountId); + } catch (NoSuchBeanDefinitionException ignored) { + if (s_logger.isTraceEnabled()) { + s_logger.trace("No WebhookHelper bean found"); + } + } return true; } catch (Exception ex) { diff --git a/server/src/main/java/org/apache/cloudstack/webhook/WebhookHelper.java b/server/src/main/java/org/apache/cloudstack/webhook/WebhookHelper.java new file mode 100644 index 000000000000..2c1d202e56dc --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/webhook/WebhookHelper.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.webhook; + +public interface WebhookHelper { + void deleteRulesForAccount(long accountId); +} From d4bd0865cf13f37252400327b9fad0edd7d592c5 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 16 Feb 2024 08:31:40 +0530 Subject: [PATCH 14/75] fix remaining event bus usage Signed-off-by: Abhishek Kumar --- .../java/com/cloud/event/UsageEventUtils.java | 20 ++-- .../java/com/cloud/event/AlertGenerator.java | 23 ++--- .../com/cloud/vm/UserVmStateListener.java | 11 +-- .../com/cloud/event/ActionEventUtilsTest.java | 19 ++-- .../HypervisorTemplateAdapterTest.java | 97 +++++++++---------- 5 files changed, 81 insertions(+), 89 deletions(-) diff --git a/engine/components-api/src/main/java/com/cloud/event/UsageEventUtils.java b/engine/components-api/src/main/java/com/cloud/event/UsageEventUtils.java index 1a2fab150a7b..f5cea0e776f9 100644 --- a/engine/components-api/src/main/java/com/cloud/event/UsageEventUtils.java +++ b/engine/components-api/src/main/java/com/cloud/event/UsageEventUtils.java @@ -25,14 +25,13 @@ import javax.annotation.PostConstruct; import javax.inject.Inject; -import org.apache.commons.collections.MapUtils; -import org.apache.log4j.Logger; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; - import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.events.Event; import org.apache.cloudstack.framework.events.EventBus; -import org.apache.cloudstack.framework.events.EventBusException; +import org.apache.cloudstack.framework.events.EventDistributor; +import org.apache.commons.collections.MapUtils; +import org.apache.log4j.Logger; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import com.cloud.dc.DataCenterVO; import com.cloud.dc.dao.DataCenterDao; @@ -49,6 +48,7 @@ public class UsageEventUtils { private static final Logger s_logger = Logger.getLogger(UsageEventUtils.class); protected static EventBus s_eventBus = null; protected static ConfigurationDao s_configDao; + private static EventDistributor eventDistributor; @Inject UsageEventDao usageEventDao; @@ -206,9 +206,9 @@ private static void publishUsageEvent(String usageEventType, Long accountId, Lon if( !configValue) return; try { - s_eventBus = ComponentContext.getComponent(EventBus.class); + eventDistributor = ComponentContext.getComponent(EventDistributor.class); } catch (NoSuchBeanDefinitionException nbe) { - return; // no provider is configured to provide events bus, so just return + return; // no provider is configured to provide events distributor, so just return } Account account = s_accountDao.findById(accountId); @@ -237,11 +237,7 @@ private static void publishUsageEvent(String usageEventType, Long accountId, Lon event.setDescription(eventDescription); - try { - s_eventBus.publish(event); - } catch (EventBusException e) { - s_logger.warn("Failed to publish usage event on the event bus."); - } + eventDistributor.publish(event); } static final String Name = "management-server"; diff --git a/server/src/main/java/com/cloud/event/AlertGenerator.java b/server/src/main/java/com/cloud/event/AlertGenerator.java index 9e12486db477..cfe3344059f4 100644 --- a/server/src/main/java/com/cloud/event/AlertGenerator.java +++ b/server/src/main/java/com/cloud/event/AlertGenerator.java @@ -20,19 +20,19 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import javax.annotation.PostConstruct; import javax.inject.Inject; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.framework.events.EventBusException; +import org.apache.cloudstack.framework.events.EventDistributor; import org.apache.log4j.Logger; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.stereotype.Component; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.framework.events.EventBus; -import org.apache.cloudstack.framework.events.EventBusException; - import com.cloud.configuration.Config; import com.cloud.dc.DataCenterVO; import com.cloud.dc.HostPodVO; @@ -47,8 +47,8 @@ public class AlertGenerator { private static final Logger s_logger = Logger.getLogger(AlertGenerator.class); private static DataCenterDao s_dcDao; private static HostPodDao s_podDao; - protected static EventBus s_eventBus = null; protected static ConfigurationDao s_configDao; + protected static EventDistributor eventDistributor; @Inject DataCenterDao dcDao; @@ -75,9 +75,9 @@ public static void publishAlertOnEventBus(String alertType, long dataCenterId, L if(!configValue) return; try { - s_eventBus = ComponentContext.getComponent(EventBus.class); + eventDistributor = ComponentContext.getComponent(EventDistributor.class); } catch (NoSuchBeanDefinitionException nbe) { - return; // no provider is configured to provide events bus, so just return + return; // no provider is configured to provide events distributor, so just return } org.apache.cloudstack.framework.events.Event event = @@ -106,10 +106,11 @@ public static void publishAlertOnEventBus(String alertType, long dataCenterId, L event.setDescription(eventDescription); - try { - s_eventBus.publish(event); - } catch (EventBusException e) { - s_logger.warn("Failed to publish alert on the event bus."); + + List exceptions = eventDistributor.publish(event); + for (EventBusException ex : exceptions) { + String errMsg = "Failed to publish event."; + s_logger.warn(errMsg, ex); } } } diff --git a/server/src/main/java/com/cloud/vm/UserVmStateListener.java b/server/src/main/java/com/cloud/vm/UserVmStateListener.java index 8d397278fdca..51619c680c3f 100644 --- a/server/src/main/java/com/cloud/vm/UserVmStateListener.java +++ b/server/src/main/java/com/cloud/vm/UserVmStateListener.java @@ -43,6 +43,7 @@ import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.events.EventDistributor; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; public class UserVmStateListener implements StateListener { @@ -66,10 +67,6 @@ public UserVmStateListener(UsageEventDao usageEventDao, NetworkDao networkDao, N this._configDao = configDao; } - public void setEventDistributor(EventDistributor eventDistributor) { - this.eventDistributor = eventDistributor; - } - @Override public boolean preStateTransitionEvent(State oldState, Event event, State newState, VirtualMachine vo, boolean status, Object opaque) { pubishOnEventBus(event.name(), "preStateTransitionEvent", vo, oldState, newState); @@ -127,8 +124,10 @@ private void pubishOnEventBus(String event, String status, VirtualMachine vo, Vi boolean configValue = Boolean.parseBoolean(value); if(!configValue) return; - if (eventDistributor == null) { - setEventDistributor(ComponentContext.getComponent(EventDistributor.class)); + try { + eventDistributor = ComponentContext.getComponent(EventDistributor.class); + } catch (NoSuchBeanDefinitionException nbe) { + return; // no provider is configured to provide events distributor, so just return } String resourceName = getEntityFromClassName(VirtualMachine.class.getName()); diff --git a/server/src/test/java/com/cloud/event/ActionEventUtilsTest.java b/server/src/test/java/com/cloud/event/ActionEventUtilsTest.java index aed28702df5c..5812f9871e4c 100644 --- a/server/src/test/java/com/cloud/event/ActionEventUtilsTest.java +++ b/server/src/test/java/com/cloud/event/ActionEventUtilsTest.java @@ -29,7 +29,7 @@ import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.events.Event; -import org.apache.cloudstack.framework.events.EventBus; +import org.apache.cloudstack.framework.events.EventDistributor; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -97,7 +97,7 @@ public class ActionEventUtilsTest { protected ConfigurationDao configDao; @Mock - protected EventBus eventBus; + protected EventDistributor eventDistributor; private AccountVO account; private UserVO user; @@ -149,7 +149,7 @@ public void setupCommonMocks() throws Exception { //Some basic mocks. Mockito.when(configDao.getValue(Config.PublishActionEvent.key())).thenReturn("true"); componentContextMocked = Mockito.mockStatic(ComponentContext.class); - componentContextMocked.when(() -> ComponentContext.getComponent(EventBus.class)).thenReturn(eventBus); + componentContextMocked.when(() -> ComponentContext.getComponent(EventDistributor.class)).thenReturn(eventDistributor); //Needed for persist to actually set an ID that can be returned from the ActionEventUtils //methods. @@ -166,14 +166,11 @@ public EventVO answer(InvocationOnMock invocation) throws Throwable { }); //Needed to record events published on the bus. - Mockito.doAnswer(new Answer() { - @Override public Void answer(InvocationOnMock invocation) throws Throwable { - Event event = (Event)invocation.getArguments()[0]; - publishedEvents.add(event); - return null; - } - - }).when(eventBus).publish(Mockito.any(Event.class)); + Mockito.doAnswer((Answer) invocation -> { + Event event = (Event)invocation.getArguments()[0]; + publishedEvents.add(event); + return null; + }).when(eventDistributor).publish(Mockito.any(Event.class)); account = new AccountVO("testaccount", 1L, "networkdomain", Account.Type.NORMAL, "uuid"); account.setId(ACCOUNT_ID); diff --git a/server/src/test/java/com/cloud/template/HypervisorTemplateAdapterTest.java b/server/src/test/java/com/cloud/template/HypervisorTemplateAdapterTest.java index 8657c07b5ef5..ccb62fa1b5a7 100644 --- a/server/src/test/java/com/cloud/template/HypervisorTemplateAdapterTest.java +++ b/server/src/test/java/com/cloud/template/HypervisorTemplateAdapterTest.java @@ -18,26 +18,26 @@ package com.cloud.template; -import com.cloud.dc.DataCenterVO; -import com.cloud.dc.dao.DataCenterDao; -import com.cloud.event.EventTypes; -import com.cloud.event.UsageEventUtils; -import com.cloud.event.UsageEventVO; -import com.cloud.event.dao.UsageEventDao; -import com.cloud.exception.InvalidParameterValueException; -import com.cloud.org.Grouping; -import com.cloud.server.StatsCollector; -import com.cloud.storage.Storage.ImageFormat; -import com.cloud.storage.TemplateProfile; -import com.cloud.storage.VMTemplateStorageResourceAssoc.Status; -import com.cloud.storage.VMTemplateVO; -import com.cloud.storage.dao.VMTemplateZoneDao; -import com.cloud.test.TestAppender; -import com.cloud.user.AccountVO; -import com.cloud.user.ResourceLimitService; -import com.cloud.user.dao.AccountDao; -import com.cloud.utils.component.ComponentContext; -import com.cloud.utils.exception.CloudRuntimeException; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.regex.Pattern; + import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.TemplateDataFactory; @@ -47,8 +47,8 @@ import org.apache.cloudstack.framework.async.AsyncCallFuture; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.events.Event; -import org.apache.cloudstack.framework.events.EventBus; import org.apache.cloudstack.framework.events.EventBusException; +import org.apache.cloudstack.framework.events.EventDistributor; import org.apache.cloudstack.framework.messagebus.MessageBus; import org.apache.cloudstack.secstorage.heuristics.HeuristicType; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; @@ -71,30 +71,31 @@ import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.regex.Pattern; - -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyLong; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.event.EventTypes; +import com.cloud.event.UsageEventUtils; +import com.cloud.event.UsageEventVO; +import com.cloud.event.dao.UsageEventDao; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.org.Grouping; +import com.cloud.server.StatsCollector; +import com.cloud.storage.Storage.ImageFormat; +import com.cloud.storage.TemplateProfile; +import com.cloud.storage.VMTemplateStorageResourceAssoc.Status; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.dao.VMTemplateZoneDao; +import com.cloud.test.TestAppender; +import com.cloud.user.AccountVO; +import com.cloud.user.ResourceLimitService; +import com.cloud.user.dao.AccountDao; +import com.cloud.utils.component.ComponentContext; +import com.cloud.utils.exception.CloudRuntimeException; @RunWith(MockitoJUnitRunner.class) public class HypervisorTemplateAdapterTest { @Mock - EventBus _bus; + EventDistributor eventDistributor; List events = new ArrayList<>(); @Mock @@ -176,16 +177,14 @@ public UsageEventUtils setupUsageUtils() throws EventBusException { Mockito.when(_usageEventDao.listAll()).thenReturn(usageEvents); - doAnswer(new Answer() { - @Override public Void answer(InvocationOnMock invocation) throws Throwable { - Event event = (Event)invocation.getArguments()[0]; - events.add(event); - return null; - } - }).when(_bus).publish(any(Event.class)); + doAnswer((Answer) invocation -> { + Event event = (Event)invocation.getArguments()[0]; + events.add(event); + return null; + }).when(eventDistributor).publish(any(Event.class)); componentContextMocked = Mockito.mockStatic(ComponentContext.class); - when(ComponentContext.getComponent(eq(EventBus.class))).thenReturn(_bus); + when(ComponentContext.getComponent(eq(EventDistributor.class))).thenReturn(eventDistributor); UsageEventUtils utils = new UsageEventUtils(); From 3821fac27ae89d181b8b0bb959e0ccb1a03b5db0 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 16 Feb 2024 13:23:21 +0530 Subject: [PATCH 15/75] changes for testing webhook dispatch Signed-off-by: Abhishek Kumar --- .../mom/webhook/WebhookApiService.java | 2 + .../mom/webhook/WebhookApiServiceImpl.java | 66 ++++- .../mom/webhook/WebhookDispatchThread.java | 20 +- .../cloudstack/mom/webhook/WebhookRule.java | 2 + .../mom/webhook/WebhookService.java | 2 + .../mom/webhook/WebhookServiceImpl.java | 176 +++++++++---- .../command/user/DeleteWebhookRuleCmd.java | 4 +- .../command/user/TestWebhookDispatchCmd.java | 135 ++++++++++ .../command/user/UpdateWebhookRuleCmd.java | 4 +- .../mom/webhook/vo/WebhookDispatchJoinVO.java | 6 +- .../mom/webhook/vo/WebhookDispatchVO.java | 22 +- .../mom/webhook/vo/WebhookRuleVO.java | 17 ++ ui/public/locales/en.json | 2 + ui/src/components/view/ListView.vue | 3 + ui/src/config/section/tools.js | 11 +- ui/src/core/lazy_lib/icons_use.js | 4 + ui/src/views/tools/TestWebhookDispatch.vue | 245 ++++++++++++++++++ 17 files changed, 657 insertions(+), 64 deletions(-) create mode 100644 plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/TestWebhookDispatchCmd.java create mode 100644 ui/src/views/tools/TestWebhookDispatch.vue diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiService.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiService.java index bc44e0065f5b..974ae5d581c2 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiService.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiService.java @@ -23,6 +23,7 @@ import org.apache.cloudstack.mom.webhook.api.command.user.DeleteWebhookRuleCmd; import org.apache.cloudstack.mom.webhook.api.command.user.ListWebhookDispatchHistoryCmd; import org.apache.cloudstack.mom.webhook.api.command.user.ListWebhookRulesCmd; +import org.apache.cloudstack.mom.webhook.api.command.user.TestWebhookDispatchCmd; import org.apache.cloudstack.mom.webhook.api.command.user.UpdateWebhookRuleCmd; import org.apache.cloudstack.mom.webhook.api.response.WebhookDispatchResponse; import org.apache.cloudstack.mom.webhook.api.response.WebhookRuleResponse; @@ -39,4 +40,5 @@ public interface WebhookApiService extends PluggableService { WebhookRuleResponse createWebhookRuleResponse(long webhookRuleId); ListResponse listWebhookDispatchHistory(ListWebhookDispatchHistoryCmd cmd); boolean deleteWebhookDispatchHistory(DeleteWebhookDispatchHistoryCmd cmd) throws CloudRuntimeException; + WebhookDispatchResponse testWebhookDispatch(TestWebhookDispatchCmd cmd) throws CloudRuntimeException; } diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java index dc414ac73bcc..65684d7a221c 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java @@ -25,6 +25,7 @@ import javax.inject.Inject; import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.mom.webhook.api.command.user.CreateWebhookRuleCmd; @@ -32,6 +33,7 @@ import org.apache.cloudstack.mom.webhook.api.command.user.DeleteWebhookRuleCmd; import org.apache.cloudstack.mom.webhook.api.command.user.ListWebhookDispatchHistoryCmd; import org.apache.cloudstack.mom.webhook.api.command.user.ListWebhookRulesCmd; +import org.apache.cloudstack.mom.webhook.api.command.user.TestWebhookDispatchCmd; import org.apache.cloudstack.mom.webhook.api.command.user.UpdateWebhookRuleCmd; import org.apache.cloudstack.mom.webhook.api.response.WebhookDispatchResponse; import org.apache.cloudstack.mom.webhook.api.response.WebhookRuleResponse; @@ -82,6 +84,8 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ WebhookDispatchJoinDao webhookDispatchJoinDao; @Inject ManagementServerHostDao managementServerHostDao; + @Inject + WebhookService webhookService; protected WebhookRuleResponse createWebhookRuleResponse(WebhookRuleJoinVO webhookRuleVO) { WebhookRuleResponse response = new WebhookRuleResponse(); @@ -150,11 +154,10 @@ protected WebhookDispatchResponse createWebhookDispatchResponse(WebhookDispatchJ response.setId(webhookDispatchVO.getUuid()); response.setEventId(webhookDispatchVO.getEventUuid()); response.setEventType(webhookDispatchVO.getEventType()); - response.setWebhookRuleName(webhookDispatchVO.getWebhookRuleName()); response.setWebhookRuleId(webhookDispatchVO.getWebhookRuleUuId()); response.setWebhookRuleName(webhookDispatchVO.getWebhookRuleName()); response.setManagementServerId(webhookDispatchVO.getManagementServerUuId()); - response.setManagementServerName(webhookDispatchVO.getMangementServerName()); + response.setManagementServerName(webhookDispatchVO.getManagementServerName()); response.setPayload(webhookDispatchVO.getPayload()); response.setSuccess(webhookDispatchVO.isSuccess()); response.setResponse(webhookDispatchVO.getResponse()); @@ -422,6 +425,64 @@ public boolean deleteWebhookDispatchHistory(DeleteWebhookDispatchHistoryCmd cmd) return removed > 0; } + @Override + public WebhookDispatchResponse testWebhookDispatch(TestWebhookDispatchCmd cmd) throws CloudRuntimeException { + final CallContext ctx = CallContext.current(); + final Account caller = ctx.getCallingAccount(); + final Long webhookRuleId = cmd.getId(); + final String payloadUrl = cmd.getPayloadUrl(); + final String secretKey = cmd.getSecretKey(); + final Boolean sslVerification = cmd.isSslVerification(); + final String payload = cmd.getPayload(); + final Account owner = accountManager.finalizeOwner(caller, null, null, cmd.getProjectId()); + + if (webhookRuleId == null && StringUtils.isBlank(payloadUrl)) { + throw new InvalidParameterValueException(String.format("Either %s or %s must be specified", ApiConstants.ID, ApiConstants.PAYLOAD_URL)); + } + WebhookRuleVO rule = null; + if (webhookRuleId != null) { + rule = webhookRuleDao.findById(webhookRuleId); + if (rule == null) { + throw new InvalidParameterValueException("Invalid webhook specified"); + } + if (StringUtils.isNotBlank(payloadUrl)) { + rule.setPayloadUrl(payloadUrl); + } + if (StringUtils.isNotBlank(secretKey)) { + rule.setSecretKey(secretKey); + } + if (sslVerification != null) { + rule.setSslVerification(Boolean.TRUE.equals(sslVerification)); + } + } + if (rule == null) { + rule = new WebhookRuleVO(owner.getDomainId(), owner.getId(), payloadUrl, secretKey, + Boolean.TRUE.equals(sslVerification)); + } + WebhookDispatch webhookDispatch = webhookService.testWebhookDispatch(rule, payload); + WebhookDispatchResponse response = new WebhookDispatchResponse(); + response.setObjectName(WebhookDispatch.class.getSimpleName().toLowerCase()); + response.setId(webhookDispatch.getUuid()); +// response.setEventId(webhookDispatch.getEventUuid()); +// response.setEventType(webhookDispatch.getEventType()); + if (webhookRuleId != null) { + response.setWebhookRuleId(webhookDispatch.getUuid()); + response.setWebhookRuleName(rule.getName()); + } + ManagementServerHostVO msHost = + managementServerHostDao.findByMsid(webhookDispatch.getManagementServerId()); + if (msHost != null) { + response.setManagementServerId(msHost.getUuid()); + response.setManagementServerName(msHost.getName()); + } + response.setPayload(webhookDispatch.getPayload()); + response.setSuccess(webhookDispatch.isSuccess()); + response.setResponse(webhookDispatch.getResponse()); + response.setStartTime(webhookDispatch.getStartTime()); + response.setEndTime(webhookDispatch.getEndTime()); + return response; + } + @Override public List> getCommands() { List> cmdList = new ArrayList<>(); @@ -431,6 +492,7 @@ public List> getCommands() { cmdList.add(DeleteWebhookRuleCmd.class); cmdList.add(ListWebhookDispatchHistoryCmd.class); cmdList.add(DeleteWebhookDispatchHistoryCmd.class); + cmdList.add(TestWebhookDispatchCmd.class); return cmdList; } } diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatchThread.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatchThread.java index 7fd8018149c2..080a231979d4 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatchThread.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDispatchThread.java @@ -18,7 +18,6 @@ package org.apache.cloudstack.mom.webhook; import java.io.IOException; -import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -38,8 +37,8 @@ import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Hex; import org.apache.commons.httpclient.HttpStatus; -import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpEntity; import org.apache.http.HttpHeaders; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; @@ -47,6 +46,7 @@ import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; import org.apache.log4j.Logger; public class WebhookDispatchThread implements Runnable { @@ -68,6 +68,10 @@ public class WebhookDispatchThread implements Runnable { AsyncCompletionCallback callback; + protected boolean isValidJson(String json) { + return json.startsWith("}") || json.startsWith("["); //ToDo + } + public WebhookDispatchThread(CloseableHttpClient httpClient, WebhookRule rule, Event event,AsyncCompletionCallback callback) { this.httpClient = httpClient; this.rule = rule; @@ -105,18 +109,19 @@ public void run() { callback.complete(new WebhookDispatchResult(payload, success, response, startTime)); } - protected void updateResponseFromRequest(InputStream is) { + protected void updateResponseFromRequest(HttpEntity entity) { try { - this.response = IOUtils.toString(is, StandardCharsets.UTF_8); + this.response = EntityUtils.toString(entity, StandardCharsets.UTF_8); } catch (IOException e) { LOGGER.error(String.format("Failed to parse response for event: %s, webhook: %s having URL: %s", event.getEventType(), rule.getName(), rule.getPayloadUrl())); + this.response = ""; } } protected boolean dispatch(int attempt) { startTime = new Date(); try { - final URI uri = new URI("http://localhost:8888"); //ToDo: rule.getPayloadUrl() + final URI uri = new URI(WebhookRule.Scope.Local.equals(rule.getScope()) ? rule.getPayloadUrl() : "http://localhost:8888"); //ToDo: rule.getPayloadUrl() HttpPost request = new HttpPost(); RequestConfig.Builder requestConfig = RequestConfig.custom(); requestConfig.setConnectTimeout(deliveryTimeout * 1000); @@ -131,13 +136,16 @@ protected boolean dispatch(int attempt) { if (StringUtils.isNotBlank(rule.getSecretKey())) { request.setHeader(HEADER_X_CS_SIGNATURE, generateHMACSignature(payload, rule.getSecretKey())); } + if (!isValidJson(payload)) { + request.setHeader(HttpHeaders.CONTENT_TYPE, "text/plain; charset=UTF-8"); + } if (LOGGER.isTraceEnabled()) { LOGGER.trace(String.format("Dispatching event: %s for webhook: %s on URL: %s with timeout: %d, attempt #%d", event.getEventType(), rule.getName(), rule.getPayloadUrl(), deliveryTimeout, attempt)); } StringEntity input = new StringEntity(payload, ContentType.APPLICATION_JSON); request.setEntity(input); final CloseableHttpResponse response = httpClient.execute(request); - updateResponseFromRequest(response.getEntity().getContent()); + updateResponseFromRequest(response.getEntity()); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { if (LOGGER.isTraceEnabled()) { LOGGER.trace(String.format("Successfully dispatched event: %s for webhook: %s", event.getEventType(), rule.getName())); diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookRule.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookRule.java index d19fc24dfef2..5329178196d0 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookRule.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookRule.java @@ -24,6 +24,8 @@ import org.apache.cloudstack.api.InternalIdentity; public interface WebhookRule extends ControlledEntity, Identity, InternalIdentity { + public static final long ID_DUMMY_RULE = 0L; + public static final String NAME_DUMMY_RULE = "Test"; enum State { Enabled, Disabled; }; diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java index eedf5fdcada2..ef1ad4e79828 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java @@ -22,6 +22,7 @@ import org.apache.cloudstack.framework.events.Event; import com.cloud.utils.component.PluggableService; +import com.cloud.utils.exception.CloudRuntimeException; public interface WebhookService extends PluggableService, Configurable { @@ -51,4 +52,5 @@ public interface WebhookService extends PluggableService, Configurable { false, ConfigKey.Scope.Global); void handleEvent(Event event); + WebhookDispatch testWebhookDispatch(WebhookRule rule, String payload) throws CloudRuntimeException; } 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 552e3a20c64d..167a2418b59d 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 @@ -21,6 +21,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -29,7 +31,10 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import org.apache.cloudstack.framework.async.AsyncCallFuture; import org.apache.cloudstack.framework.async.AsyncCallbackDispatcher; +import org.apache.cloudstack.framework.async.AsyncCompletionCallback; +import org.apache.cloudstack.framework.async.AsyncRpcContext; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.events.Event; import org.apache.cloudstack.managed.context.ManagedContextRunnable; @@ -39,6 +44,7 @@ import org.apache.cloudstack.mom.webhook.vo.WebhookRuleVO; import org.apache.cloudstack.utils.identity.ManagementServerNode; import org.apache.cloudstack.webhook.WebhookHelper; +import org.apache.commons.lang3.StringUtils; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.log4j.Logger; @@ -47,11 +53,15 @@ import com.cloud.cluster.dao.ManagementServerHostDao; import com.cloud.domain.dao.DomainDao; import com.cloud.event.EventCategory; +import com.cloud.server.ManagementService; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; import com.cloud.utils.Pair; import com.cloud.utils.component.ComponentContext; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.concurrency.NamedThreadFactory; import com.cloud.utils.db.GlobalLock; +import com.cloud.utils.exception.CloudRuntimeException; public class WebhookServiceImpl extends ManagerBase implements WebhookService, WebhookHelper { public static final Logger LOGGER = Logger.getLogger(WebhookApiServiceImpl.class.getName()); @@ -68,6 +78,93 @@ public class WebhookServiceImpl extends ManagerBase implements WebhookService, W ManagementServerHostDao managementServerHostDao; @Inject DomainDao domainDao; + @Inject + AccountManager accountManager; + + protected WebhookDispatchThread getDispatchJob(Event event, WebhookRule rule, Pair configs) { + WebhookDispatchThread.WebhookDispatchContext context = + new WebhookDispatchThread.WebhookDispatchContext<>(null, event.getEventId(), rule.getId()); + AsyncCallbackDispatcher caller = + AsyncCallbackDispatcher.create(this); + caller.setCallback(caller.getTarget().dispatchCompleteCallback(null, null)) + .setContext(context); + WebhookDispatchThread job = new WebhookDispatchThread(closeableHttpClient, rule, event, caller); + job = ComponentContext.inject(job); + job.setDispatchRetries(configs.first()); + job.setDeliveryTimeout(configs.second()); + return job; + } + + protected List getDispatchJobs(Event event) { + List jobs = new ArrayList<>(); + if (!EventCategory.ACTION_EVENT.getName().equals(event.getEventCategory()) || + event.getResourceAccountId() == null) { + return jobs; + } + List domainIds = new ArrayList<>(); + if (event.getResourceDomainId() != null) { + domainIds.add(event.getResourceDomainId()); + domainIds.addAll(domainDao.getDomainParentIds(event.getResourceDomainId())); + } + List rules = webhookRuleDao.listByEnabledRulesForDispatch(event.getResourceAccountId(), domainIds); + Map> domainConfigs = new HashMap<>(); + for (WebhookRuleVO rule : rules) { + if (!domainConfigs.containsKey(rule.getDomainId())) { + domainConfigs.put(rule.getDomainId(), new Pair<>(WebhookDispatchRetries.valueIn(rule.getDomainId()), + WebhookDeliveryTimeout.valueIn(rule.getDomainId()))); + } + Pair configs = domainConfigs.get(rule.getDomainId()); + WebhookDispatchThread job = getDispatchJob(event, rule, configs); + jobs.add(job); + } + return jobs; + } + + protected Runnable getTestDispatchJobs(WebhookRule rule, String payload, + AsyncCallFuture future) { + if (StringUtils.isBlank(payload)) { + payload = "{ \"CloudStack\": \"works!\" }"; + } + Event event = new Event(ManagementService.Name, EventCategory.ACTION_EVENT.toString(), + "TEST.WEBHOOK", null, null); + event.setEventId(WebhookRule.ID_DUMMY_RULE); + event.setEventUuid(UUID.randomUUID().toString()); + event.setDescription(payload); + Account account = accountManager.getAccount(rule.getAccountId()); + event.setResourceAccountId(account.getId()); + event.setResourceAccountUuid(account.getUuid()); + event.setResourceDomainId(account.getDomainId()); + Pair configs = new Pair<>(WebhookDispatchRetries.valueIn(rule.getDomainId()), + WebhookDeliveryTimeout.valueIn(rule.getDomainId())); +// WebhookDispatchThread job = getDispatchJob(event, rule, configs); + TestDispatchContext context = + new TestDispatchContext<>(null, rule, future); + AsyncCallbackDispatcher caller = + AsyncCallbackDispatcher.create(this); + caller.setCallback(caller.getTarget().testDispatchCompleteCallback(null, null)) + .setContext(context); + WebhookDispatchThread job = new WebhookDispatchThread(closeableHttpClient, rule, event, caller); + return job; + } + + protected Void dispatchCompleteCallback( + AsyncCallbackDispatcher callback, + WebhookDispatchThread.WebhookDispatchContext context) { + WebhookDispatchThread.WebhookDispatchResult result = callback.getResult(); + WebhookDispatchVO dispatchVO = new WebhookDispatchVO(context.getEventId(), context.getRuleId(), + ManagementServerNode.getManagementServerId(), result.getPayload(), result.isSuccess(), + result.getResult(), result.getStarTime(), result.getEndTime()); + webhookDispatchDao.persist(dispatchVO); + return null; + } + + protected Void testDispatchCompleteCallback( + AsyncCallbackDispatcher callback, + TestDispatchContext context) { + WebhookDispatchThread.WebhookDispatchResult result = callback.getResult(); + context.future.complete(result); + return null; + } @Override public boolean configure(String name, Map params) throws ConfigurationException { @@ -111,6 +208,11 @@ public ConfigKey[] getConfigKeys() { }; } + @Override + public void deleteRulesForAccount(long accountId) { + webhookRuleDao.deleteByAccount(accountId); + } + @Override public void handleEvent(Event event) { List jobs = getDispatchJobs(event); @@ -119,62 +221,42 @@ public void handleEvent(Event event) { } } + @Override + public WebhookDispatch testWebhookDispatch(WebhookRule rule, String payload) throws CloudRuntimeException { + AsyncCallFuture future = new AsyncCallFuture<>(); + Runnable job = getTestDispatchJobs(rule, payload, future); + webhookJobExecutor.submit(job); + WebhookDispatchThread.WebhookDispatchResult result = null; + WebhookDispatchVO webhookDispatchVO; + try { + result = future.get(); + webhookDispatchVO = new WebhookDispatchVO(ManagementServerNode.getManagementServerId(), + result.getPayload(), result.isSuccess(), result.getResult(), + result.getStarTime(), result.getEndTime()); + } catch (InterruptedException | ExecutionException e) { + LOGGER.error(String.format("Failed to execute test webhook dispatch due to: %s", e.getMessage()), e); + throw new CloudRuntimeException("Failed to execute test webhook dispatch"); + } + return webhookDispatchVO; + } + @Override public List> getCommands() { return new ArrayList<>(); } - protected List getDispatchJobs(Event event) { - List jobs = new ArrayList<>(); - if (!EventCategory.ACTION_EVENT.getName().equals(event.getEventCategory()) || - event.getResourceAccountId() == null) { - return jobs; - } - List domainIds = new ArrayList<>(); - if (event.getResourceDomainId() != null) { - domainIds.add(event.getResourceDomainId()); - domainIds.addAll(domainDao.getDomainParentIds(event.getResourceDomainId())); - } - List rules = webhookRuleDao.listByEnabledRulesForDispatch(event.getResourceAccountId(), domainIds); - Map> domainConfigs = new HashMap<>(); - for (WebhookRuleVO rule : rules) { - if (!domainConfigs.containsKey(rule.getDomainId())) { - domainConfigs.put(rule.getDomainId(), new Pair<>(WebhookDispatchRetries.valueIn(rule.getDomainId()), - WebhookDeliveryTimeout.valueIn(rule.getDomainId()))); - } - Pair configs = domainConfigs.get(rule.getDomainId()); - WebhookDispatchThread.WebhookDispatchContext context = - new WebhookDispatchThread.WebhookDispatchContext<>(null, event.getEventId(), rule.getId()); - AsyncCallbackDispatcher caller = - AsyncCallbackDispatcher.create(this); - caller.setCallback(caller.getTarget().dispatchCompleteCallback(null, null)) - .setContext(context); - WebhookDispatchThread job = new WebhookDispatchThread(closeableHttpClient, rule, event, caller); - job = ComponentContext.inject(job); - job.setDispatchRetries(configs.first()); - job.setDeliveryTimeout(configs.second()); - jobs.add(job); - } - return jobs; - } + static public class TestDispatchContext extends AsyncRpcContext { + final WebhookRule webhookRule; + final AsyncCallFuture future; - protected Void dispatchCompleteCallback( - AsyncCallbackDispatcher callback, - WebhookDispatchThread.WebhookDispatchContext context) { - WebhookDispatchThread.WebhookDispatchResult result = callback.getResult();; - WebhookDispatchVO dispatchVO = new WebhookDispatchVO(context.getEventId(), context.getRuleId(), - ManagementServerNode.getManagementServerId(), result.getPayload(), result.isSuccess(), - result.getResult(), result.getStarTime(), result.getEndTime()); - webhookDispatchDao.persist(dispatchVO); - return null; - } + public TestDispatchContext(AsyncCompletionCallback callback, WebhookRule rule, AsyncCallFuture future) { + super(callback); + this.webhookRule = rule; + this.future = future; + } - @Override - public void deleteRulesForAccount(long accountId) { - webhookRuleDao.deleteByAccount(accountId); } - public class WebhookDispatchCleanupWorker extends ManagedContextRunnable { @Override protected void runInContext() { diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookRuleCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookRuleCmd.java index 68d1f8674b65..8ab0fd098e2a 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookRuleCmd.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookRuleCmd.java @@ -50,8 +50,8 @@ public class DeleteWebhookRuleCmd extends BaseCmd { ///////////////////////////////////////////////////// @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = WebhookRuleResponse.class, - description = "The ID of the Webhook rule", - required = true) + required = true, + description = "The ID of the Webhook rule") private Long id; ///////////////////////////////////////////////////// diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/TestWebhookDispatchCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/TestWebhookDispatchCmd.java new file mode 100644 index 000000000000..01953bfa791a --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/TestWebhookDispatchCmd.java @@ -0,0 +1,135 @@ +// 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.mom.webhook.api.command.user; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; +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.ProjectResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.mom.webhook.WebhookApiService; +import org.apache.cloudstack.mom.webhook.WebhookDispatch; +import org.apache.cloudstack.mom.webhook.api.response.WebhookDispatchResponse; +import org.apache.cloudstack.mom.webhook.api.response.WebhookRuleResponse; + +import com.cloud.utils.exception.CloudRuntimeException; + + +@APICommand(name = "testWebhookDispatch", + description = "Test a Webhook", + responseObject = WebhookDispatchResponse.class, + entityType = {WebhookDispatch.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "4.20.0") +public class TestWebhookDispatchCmd extends BaseCmd { + + @Inject + WebhookApiService webhookApiService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, + entityType = WebhookRuleResponse.class, + description = "The ID of the Webhook rule") + private Long id; + + @Parameter(name = ApiConstants.PAYLOAD_URL, + type = BaseCmd.CommandType.STRING, + description = "Payload URL of the Webhook dispatch") + private String payloadUrl; + + @Parameter(name = ApiConstants.SECRET_KEY, type = BaseCmd.CommandType.STRING, description = "Secret key of the Webhook dispatch") + private String secretKey; + + @Parameter(name = ApiConstants.SSL_VERIFICATION, type = BaseCmd.CommandType.BOOLEAN, description = "If set to true then SSL verification will be done for the Webhook dispatch otherwise not") + private Boolean sslVerification; + + @Parameter(name = ApiConstants.PAYLOAD, + type = BaseCmd.CommandType.STRING, + description = "Payload of the Webhook dispatch") + private String payload; + + @ACL(accessType = SecurityChecker.AccessType.UseEntry) + @Parameter(name = ApiConstants.PROJECT_ID, type = BaseCmd.CommandType.UUID, entityType = ProjectResponse.class, + description = "Project for the Webhook dispatch") + private Long projectId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + + public Long getId() { + return id; + } + + public String getPayloadUrl() { + return payloadUrl; + } + + public String getSecretKey() { + return secretKey; + } + + public Boolean isSslVerification() { + return sslVerification; + } + + public String getPayload() { + return payload; + } + + public Long getProjectId() { + return projectId; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccountId(); + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ServerApiException { + try { + WebhookDispatchResponse response = webhookApiService.testWebhookDispatch(this); + if (response == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to test Webhook dispatch"); + } + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (CloudRuntimeException ex) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage()); + } + + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookRuleCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookRuleCmd.java index 49678012a6e1..fd59bf71a996 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookRuleCmd.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookRuleCmd.java @@ -50,8 +50,8 @@ public class UpdateWebhookRuleCmd extends BaseCmd { ///////////////////////////////////////////////////// @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = WebhookRuleResponse.class, - description = "The ID of the Webhook rule", - required = true) + required = true, + description = "The ID of the Webhook rule") private Long id; @Parameter(name = ApiConstants.NAME, type = BaseCmd.CommandType.STRING, description = "Name for the Webhook rule") private String name; diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookDispatchJoinVO.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookDispatchJoinVO.java index f300fea86fd8..e1ee700501f5 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookDispatchJoinVO.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookDispatchJoinVO.java @@ -75,7 +75,7 @@ public class WebhookDispatchJoinVO extends BaseViewVO implements InternalIdentit private long managementServerMsId; @Column(name = "mshost_name") - private String mangementServerName; + private String managementServerName; @Column(name = "payload", length = 65535) private String payload; @@ -140,8 +140,8 @@ public long getManagementServerMsId() { return managementServerMsId; } - public String getMangementServerName() { - return mangementServerName; + public String getManagementServerName() { + return managementServerName; } public String getPayload() { diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookDispatchVO.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookDispatchVO.java index 87295292fa35..46f01ef2a13b 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookDispatchVO.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookDispatchVO.java @@ -31,6 +31,7 @@ import javax.persistence.TemporalType; import org.apache.cloudstack.mom.webhook.WebhookDispatch; +import org.apache.cloudstack.mom.webhook.WebhookRule; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; @Entity @@ -93,7 +94,7 @@ public long getWebhookRuleId() { @Override public long getManagementServerId() { - return 0; + return mangementServerId; } @Override @@ -142,4 +143,23 @@ public WebhookDispatchVO(long eventId, long webhookRuleId, long managementServer this.startTime = startTime; this.endTime = endTime; } + + + + /* + * For creating a dummy object for testing dispatch + */ + public WebhookDispatchVO(long managementServerId, String payload, boolean success, + String response, Date startTime, Date endTime) { + this.id = WebhookRule.ID_DUMMY_RULE; + this.uuid = UUID.randomUUID().toString(); + this.eventId = WebhookRule.ID_DUMMY_RULE; + this.webhookRuleId = WebhookRule.ID_DUMMY_RULE; + this.mangementServerId = managementServerId; + this.payload = payload; + this.success = success; + this.response = response; + this.startTime = startTime; + this.endTime = endTime; + } } diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookRuleVO.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookRuleVO.java index 7ec1a1e11c55..1bc8c5fad5ea 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookRuleVO.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookRuleVO.java @@ -211,4 +211,21 @@ public WebhookRuleVO(String name, String description, State state, long domainId this.sslVerification = sslVerification; this.scope = scope; } + + /* + * For creating a dummy rule for testing dispatch + */ + public WebhookRuleVO(long domainId, long accountId, String payloadUrl, String secretKey, boolean sslVerification) { + this.uuid = UUID.randomUUID().toString(); + this.id = ID_DUMMY_RULE; + this.name = NAME_DUMMY_RULE; + this.description = NAME_DUMMY_RULE; + this.state = State.Enabled; + this.domainId = domainId; + this.accountId = accountId; + this.payloadUrl = payloadUrl; + this.secretKey = secretKey; + this.sslVerification = sslVerification; + this.scope = Scope.Local; + } } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index aa6ce947983b..ef875305af1d 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -2080,6 +2080,8 @@ "label.templatetype": "Template type", "label.templateversion": "Template version", "label.term.type": "Term type", +"label.test": "Test", +"label.test.webhook.dispatch": "Test Webhook Dispatch", "label.tftpdir": "TFTP root directory", "label.theme.alert": "The setting is only visible to the current browser. To apply the setting, please download the JSON file and replace its content in the `theme` section of the `config.json` file under the path: `/public/config.json`", "label.theme.color": "Theme color", diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index f862e89a5b8e..b8654e1952a9 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -370,6 +370,9 @@ +