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 acce2bc77264..e9c15ed553b4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -119,12 +119,15 @@ public class ApiConstants { public static final String CN = "cn"; public static final String COMMAND = "command"; public static final String CMD_EVENT_TYPE = "cmdeventtype"; + public static final String CLIENT_ADDRESS = "clientaddress"; public static final String COMPONENT = "component"; + public static final String CONNECTED = "connected"; public static final String CPU_CORE_PER_SOCKET = "cpucorepersocket"; public static final String CPU_NUMBER = "cpunumber"; public static final String CPU_SPEED = "cpuspeed"; public static final String CPU_LOAD_AVERAGE = "cpuloadaverage"; public static final String CREATED = "created"; + public static final String CREATOR_ADDRESS = "creatoraddress"; public static final String CTX_ACCOUNT_ID = "ctxaccountid"; public static final String CTX_DETAILS = "ctxDetails"; public static final String CTX_USER_ID = "ctxuserid"; @@ -140,6 +143,7 @@ public class ApiConstants { public static final String ENCRYPT_FORMAT = "encryptformat"; public static final String ENCRYPT_ROOT = "encryptroot"; public static final String ENCRYPTION_SUPPORTED = "encryptionsupported"; + public static final String FILTERS = "filters"; public static final String MIN_IOPS = "miniops"; public static final String MAX_IOPS = "maxiops"; public static final String HYPERVISOR_SNAPSHOT_RESERVE = "hypervisorsnapshotreserve"; @@ -270,6 +274,7 @@ public class ApiConstants { public static final String INTERNET_PROTOCOL = "internetprotocol"; public static final String INTERVAL_TYPE = "intervaltype"; public static final String LOCATION_TYPE = "locationtype"; + public static final String LOG_IDS = "logids"; public static final String IOPS_READ_RATE = "iopsreadrate"; public static final String IOPS_READ_RATE_MAX = "iopsreadratemax"; public static final String IOPS_READ_RATE_MAX_LENGTH = "iopsreadratemaxlength"; @@ -1190,6 +1195,8 @@ public class ApiConstants { public static final String WEBHOOK_ID = "webhookid"; public static final String WEBHOOK_NAME = "webhookname"; + public static final String WEBSOCKET = "websocket"; + public static final String NFS_MOUNT_OPTIONS = "nfsmountopts"; public static final String MOUNT_OPTIONS = "mountopts"; @@ -1206,6 +1213,8 @@ public class ApiConstants { public static final String OBJECT_STORAGE_LIMIT = "objectstoragelimit"; public static final String OBJECT_STORAGE_TOTAL = "objectstoragetotal"; + public static final String LOGS_WEB_SERVER_ENABLED = "logswebserverenabled"; + public static final String PARAMETER_DESCRIPTION_ACTIVATION_RULE = "Quota tariff's activation rule. It can receive a JS script that results in either " + "a boolean or a numeric value: if it results in a boolean value, the tariff value will be applied according to the result; if it results in a numeric value, the " + "numeric value will be applied; if the result is neither a boolean nor a numeric value, the tariff will not be applied. If the rule is not informed, the tariff " + diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseResponse.java b/api/src/main/java/org/apache/cloudstack/api/BaseResponse.java index 45016c1a2a26..f37d22092366 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseResponse.java @@ -16,6 +16,10 @@ // under the License. package org.apache.cloudstack.api; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + import com.google.gson.annotations.SerializedName; import com.cloud.serializer.Param; @@ -32,6 +36,10 @@ public abstract class BaseResponse implements ResponseObject { @Param(description = "the current status of the latest async job acting on this object") private Integer jobStatus; + @SerializedName(ApiConstants.LOG_IDS) + @Param(description = "the IDs of the logs for the request") + private List logsIds; + public BaseResponse() { } @@ -83,4 +91,17 @@ public Integer getJobStatus() { public void setJobStatus(Integer jobStatus) { this.jobStatus = jobStatus; } + + @Override + public List getLogIds() { + return logsIds; + } + + @Override + public void addLogIds(String... logId) { + if (this.logsIds == null) { + logsIds = new ArrayList<>(); + } + this.logsIds.addAll(Arrays.asList(logId)); + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/ResponseObject.java b/api/src/main/java/org/apache/cloudstack/api/ResponseObject.java index ff2e172b70b3..80c487a49711 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ResponseObject.java +++ b/api/src/main/java/org/apache/cloudstack/api/ResponseObject.java @@ -16,6 +16,8 @@ // under the License. package org.apache.cloudstack.api; +import java.util.List; + public interface ResponseObject { /** * Get the name of the API response @@ -76,6 +78,9 @@ public interface ResponseObject { */ void setJobStatus(Integer jobStatus); + List getLogIds(); + void addLogIds(String... contextId); + public enum ResponseView { Full, Restricted diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ListCfgsByCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ListCfgsByCmd.java index e365d8bc2dc7..74f04e57926c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ListCfgsByCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ListCfgsByCmd.java @@ -19,23 +19,23 @@ import java.util.ArrayList; import java.util.List; -import org.apache.cloudstack.api.ApiErrorCode; -import org.apache.cloudstack.api.ServerApiException; -import org.apache.cloudstack.api.response.DomainResponse; -import org.apache.commons.lang3.StringUtils; - import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseListCmd; import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.ClusterResponse; import org.apache.cloudstack.api.response.ConfigurationResponse; +import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.ImageStoreResponse; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ManagementServerResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.config.Configuration; +import org.apache.commons.lang3.StringUtils; import com.cloud.exception.InvalidParameterValueException; import com.cloud.utils.Pair; @@ -94,6 +94,13 @@ public class ListCfgsByCmd extends BaseListCmd { description = "the ID of the Image Store to update the parameter value for corresponding image store") private Long imageStoreId; + @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, + type = CommandType.UUID, + entityType = ManagementServerResponse.class, + description = "the ID of the Management Server to update the parameter value for corresponding management server", + since = "4.21.0") + private Long managementServerId; + @Parameter(name = ApiConstants.GROUP, type = CommandType.STRING, description = "lists configuration by group name (primarily used for UI)", since = "4.18.0") private String groupName; @@ -139,6 +146,10 @@ public Long getImageStoreId() { return imageStoreId; } + public Long getManagementServerId() { + return managementServerId; + } + public String getGroupName() { return groupName; } @@ -200,6 +211,9 @@ private void setScope(ConfigurationResponse cfgResponse) { if (getImageStoreId() != null){ cfgResponse.setScope("imagestore"); } + if (getManagementServerId() != null){ + cfgResponse.setScope("managementserver"); + } } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ResetCfgCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ResetCfgCmd.java index f114b263b634..2c8a39113ea7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ResetCfgCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ResetCfgCmd.java @@ -23,16 +23,16 @@ import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; -import org.apache.cloudstack.api.response.ImageStoreResponse; -import org.apache.cloudstack.framework.config.ConfigKey; - import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.ClusterResponse; import org.apache.cloudstack.api.response.ConfigurationResponse; import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ImageStoreResponse; +import org.apache.cloudstack.api.response.ManagementServerResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.config.Configuration; +import org.apache.cloudstack.framework.config.ConfigKey; import com.cloud.user.Account; import com.cloud.utils.Pair; @@ -84,6 +84,13 @@ public class ResetCfgCmd extends BaseCmd { description = "the ID of the Image Store to reset the parameter value for corresponding image store") private Long imageStoreId; + @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, + type = CommandType.UUID, + entityType = ManagementServerResponse.class, + description = "the ID of the Management Server to update the parameter value for corresponding management server", + since = "4.21.0") + private Long managementServerId; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -116,6 +123,10 @@ public Long getImageStoreId() { return imageStoreId; } + public Long getManagementServerId() { + return managementServerId; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -149,6 +160,9 @@ public void execute() { if (getImageStoreId() != null) { response.setScope(ConfigKey.Scope.ImageStore.name()); } + if (getManagementServerId() != null) { + response.setScope(ConfigKey.Scope.ManagementServer.name()); + } response.setValue(cfg.second()); this.setResponseObject(response); } else { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/UpdateCfgCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/UpdateCfgCmd.java index dbf478df7012..24aa37603dc9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/UpdateCfgCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/UpdateCfgCmd.java @@ -30,6 +30,7 @@ import org.apache.cloudstack.api.response.ClusterResponse; import org.apache.cloudstack.api.response.ConfigurationResponse; import org.apache.cloudstack.api.response.ImageStoreResponse; +import org.apache.cloudstack.api.response.ManagementServerResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.config.Configuration; @@ -88,6 +89,14 @@ public class UpdateCfgCmd extends BaseCmd { validations = ApiArgValidator.PositiveNumber) private Long imageStoreId; + @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, + type = CommandType.UUID, + entityType = ManagementServerResponse.class, + description = "the ID of the Management Server to update the parameter value for corresponding management server", + validations = ApiArgValidator.PositiveNumber, + since = "4.21.0") + private Long managementServerId; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -112,7 +121,7 @@ public Long getClusterId() { return clusterId; } - public Long getStoragepoolId() { + public Long getStoragePoolId() { return storagePoolId; } @@ -128,6 +137,10 @@ public Long getImageStoreId() { return imageStoreId; } + public Long getManagementServerId() { + return managementServerId; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -184,7 +197,7 @@ public ConfigurationResponse setResponseScopes(ConfigurationResponse response) { if (getClusterId() != null) { response.setScope("cluster"); } - if (getStoragepoolId() != null) { + if (getStoragePoolId() != null) { response.setScope("storagepool"); } if (getAccountId() != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java index 0cecbb370202..2e03bd180130 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java @@ -72,6 +72,7 @@ public void execute() { response.setInstancesDisksStatsRetentionTime((Integer) capabilities.get(ApiConstants.INSTANCES_DISKS_STATS_RETENTION_TIME)); response.setSharedFsVmMinCpuCount((Integer)capabilities.get(ApiConstants.SHAREDFSVM_MIN_CPU_COUNT)); response.setSharedFsVmMinRamSize((Integer)capabilities.get(ApiConstants.SHAREDFSVM_MIN_RAM_SIZE)); + response.setLogsWebServerEnabled((Boolean)capabilities.get(ApiConstants.LOGS_WEB_SERVER_ENABLED)); response.setObjectName("capability"); response.setResponseName(getCommandName()); this.setResponseObject(response); diff --git a/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java index 3861ac455ed5..27041dd1addb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java @@ -136,6 +136,10 @@ public class CapabilitiesResponse extends BaseResponse { @Param(description = "the min Ram size for the service offering used by the shared filesystem instance", since = "4.20.0") private Integer sharedFsVmMinRamSize; + @SerializedName(ApiConstants.LOGS_WEB_SERVER_ENABLED) + @Param(description = "true if Logs Web Server plugin is enabled, false otherwise", since = "4.21.0") + private boolean logsWebServerEnabled; + public void setSecurityGroupsEnabled(boolean securityGroupsEnabled) { this.securityGroupsEnabled = securityGroupsEnabled; } @@ -247,4 +251,8 @@ public void setSharedFsVmMinCpuCount(Integer sharedFsVmMinCpuCount) { public void setSharedFsVmMinRamSize(Integer sharedFsVmMinRamSize) { this.sharedFsVmMinRamSize = sharedFsVmMinRamSize; } + + public void setLogsWebServerEnabled(boolean logsWebServerEnabled) { + this.logsWebServerEnabled = logsWebServerEnabled; + } } diff --git a/client/pom.xml b/client/pom.xml index 2b673d7750e9..162e72e01d24 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -246,6 +246,11 @@ cloud-plugin-user-two-factor-authenticator-staticpin ${project.version} + + org.apache.cloudstack + cloud-plugin-logs-web-server + ${project.version} + org.apache.cloudstack cloud-plugin-metrics diff --git a/engine/schema/src/main/java/org/apache/cloudstack/util/StringListJsonConverter.java b/engine/schema/src/main/java/org/apache/cloudstack/util/StringListJsonConverter.java new file mode 100644 index 000000000000..85e83a10526c --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/util/StringListJsonConverter.java @@ -0,0 +1,49 @@ +// 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.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; +import java.io.IOException; +import java.util.List; + +@Converter +public class StringListJsonConverter implements AttributeConverter, String> { + + private static final ObjectMapper mapper = new ObjectMapper(); + + @Override + public String convertToDatabaseColumn(List attribute) { + try { + return attribute == null ? null : mapper.writeValueAsString(attribute); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Error converting list to JSON", e); + } + } + + @Override + public List convertToEntityAttribute(String dbData) { + try { + return dbData == null ? null : mapper.readValue(dbData, List.class); + } catch (IOException e) { + throw new IllegalArgumentException("Error converting JSON to list", e); + } + } +} diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index d05635f4614c..523035abcc1b 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -116,6 +116,7 @@ + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100-cleanup.sql b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100-cleanup.sql index 5f257f2965bd..b28f85147bde 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100-cleanup.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100-cleanup.sql @@ -18,3 +18,5 @@ --; -- Schema upgrade cleanup from 4.20.1.0 to 4.21.0.0 --; + +DROP TABLE IF EXISTS `cloud`.`logs_web_session`; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql index 292da4a466bd..a82863584eb9 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql @@ -65,3 +65,31 @@ CREATE TABLE IF NOT EXISTS `cloud`.`reconcile_commands` ( CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.snapshot_store_ref', 'kvm_checkpoint_path', 'varchar(255)'); CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.snapshot_store_ref', 'end_of_chain', 'int(1) unsigned'); + +-- Fix ManagementServer scope for ConfigKey +CREATE TABLE IF NOT EXISTS `management_server_details` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', + `management_server_id` bigint unsigned NOT NULL COMMENT 'management server the detail is related to', + `name` varchar(255) NOT NULL COMMENT 'name of the detail', + `value` varchar(255) NOT NULL, + `display` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'True if the detail can be displayed to the end user', + PRIMARY KEY (`id`), + CONSTRAINT `fk_management_server_details__management_server_id` FOREIGN KEY `fk_management_server_details__management_server_id`(`management_server_id`) REFERENCES `mshost`(`id`) ON DELETE CASCADE, + KEY `i_management_server_details__name__value` (`name`(128),`value`(128)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Feature: Logs in UI +CREATE TABLE IF NOT EXISTS `cloud`.`logs_web_session` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY, + `uuid` varchar(40) NOT NULL COMMENT 'UUID generated for the session', + `filter` varchar(64) DEFAULT NULL COMMENT 'Filter keyword for the session', + `created` datetime NOT NULL COMMENT 'When the session was created', + `domain_id` bigint(20) unsigned NOT NULL COMMENT 'Domain of the account who generated the session', + `account_id` bigint(20) unsigned NOT NULL COMMENT 'Account who generated the session', + `creator_address` VARCHAR(45) DEFAULT NULL COMMENT 'Address of the creator of the session', + `connections` int unsigned NOT NULL DEFAULT 0 COMMENT 'Number of connections for the session', + `connected_time` datetime DEFAULT NULL COMMENT 'When the session was connected', + `client_address` VARCHAR(45) DEFAULT NULL COMMENT 'Address of the client that connected to the session', + `removed` datetime COMMENT 'When the session was removed/used', + CONSTRAINT `uc_logs_web_session__uuid` UNIQUE (`uuid`) +); diff --git a/framework/cluster/pom.xml b/framework/cluster/pom.xml index b2e89704c89c..5cdfe41883f4 100644 --- a/framework/cluster/pom.xml +++ b/framework/cluster/pom.xml @@ -48,6 +48,12 @@ cloud-api ${project.version} + + org.apache.cloudstack + cloud-engine-schema + 4.21.0.0-SNAPSHOT + compile + diff --git a/framework/cluster/src/main/java/com/cloud/cluster/ManagementServerHostDetailVO.java b/framework/cluster/src/main/java/com/cloud/cluster/ManagementServerHostDetailVO.java new file mode 100644 index 000000000000..fcaa2a22e341 --- /dev/null +++ b/framework/cluster/src/main/java/com/cloud/cluster/ManagementServerHostDetailVO.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 com.cloud.cluster; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.apache.cloudstack.api.ResourceDetail; + +@Entity +@Table(name = "management_server_details") +public class ManagementServerHostDetailVO implements ResourceDetail { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + long id; + + @Column(name = "management_server_id") + long resourceId; + + @Column(name = "name") + String name; + + @Column(name = "value") + String value; + + @Column(name = "display") + private boolean display = true; + + public ManagementServerHostDetailVO(long poolId, String name, String value, boolean display) { + this.resourceId = poolId; + this.name = name; + this.value = value; + this.display = display; + } + + public ManagementServerHostDetailVO() { + } + + @Override + public long getId() { + return id; + } + + @Override + public long getResourceId() { + return resourceId; + } + + @Override + public String getName() { + return name; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public String getValue() { + return value; + } + + @Override + public boolean isDisplay() { + return display; + } +} diff --git a/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDao.java b/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDao.java new file mode 100644 index 000000000000..24fd60d21b3c --- /dev/null +++ b/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDao.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 com.cloud.cluster.dao; + +import org.apache.cloudstack.resourcedetail.ResourceDetailsDao; + +import com.cloud.cluster.ManagementServerHostDetailVO; +import com.cloud.utils.db.GenericDao; + +public interface ManagementServerHostDetailsDao extends GenericDao, ResourceDetailsDao { +} diff --git a/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDaoImpl.java b/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDaoImpl.java new file mode 100644 index 000000000000..5865bee0926b --- /dev/null +++ b/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDaoImpl.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 com.cloud.cluster.dao; + +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.ScopedConfigStorage; +import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase; + +import com.cloud.cluster.ManagementServerHostDetailVO; + +public class ManagementServerHostDetailsDaoImpl extends ResourceDetailsDaoBase implements ManagementServerHostDetailsDao, ScopedConfigStorage { + + public ManagementServerHostDetailsDaoImpl() { + } + + @Override + public ConfigKey.Scope getScope() { + return ConfigKey.Scope.ManagementServer; + } + + @Override + public String getConfigValue(long id, String key) { + ManagementServerHostDetailVO vo = findDetail(id, key); + return vo == null ? null : vo.getValue(); + } + + @Override + public void addDetail(long resourceId, String key, String value, boolean display) { + super.addDetail(new ManagementServerHostDetailVO(resourceId, key, value, display)); + } +} diff --git a/plugins/logs-web-server/pom.xml b/plugins/logs-web-server/pom.xml new file mode 100644 index 000000000000..d01fecd9d387 --- /dev/null +++ b/plugins/logs-web-server/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + cloud-plugin-logs-web-server + Apache CloudStack Plugin - Logs Web Server + + org.apache.cloudstack + cloudstack-plugins + 4.21.0.0-SNAPSHOT + ../pom.xml + + + + org.apache.cloudstack + cloud-api + ${project.version} + + + io.netty + netty-all + + + diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSession.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSession.java new file mode 100644 index 000000000000..c77ab28800c5 --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSession.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.logsws; + +import java.util.Date; +import java.util.List; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface LogsWebSession extends ControlledEntity, Identity, InternalIdentity { + long getId(); + List getFilters(); + long getDomainId(); + long getAccountId(); + int getConnections(); + Date getConnectedTime(); + String getCreatorAddress(); + String getClientAddress(); + Date getCreated(); +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionApiService.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionApiService.java new file mode 100644 index 000000000000..85e8f2893a7d --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionApiService.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.logsws; + +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.logsws.api.command.admin.CreateLogsWebSessionCmd; +import org.apache.cloudstack.logsws.api.command.admin.DeleteLogsWebSession; +import org.apache.cloudstack.logsws.api.command.admin.ListLogsWebSessionsCmd; +import org.apache.cloudstack.logsws.api.response.LogsWebSessionResponse; + +import com.cloud.utils.component.PluggableService; +import com.cloud.utils.exception.CloudRuntimeException; + +public interface LogsWebSessionApiService extends PluggableService { + + ListResponse listLogsWebSessions(ListLogsWebSessionsCmd cmd); + LogsWebSessionResponse createLogsWebSession(CreateLogsWebSessionCmd cmd) throws CloudRuntimeException; + boolean deleteLogsWebSession(DeleteLogsWebSession cmd) throws CloudRuntimeException; + LogsWebSessionResponse createLogsWebSessionResponse(long logsEndpointId); +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionApiServiceImpl.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionApiServiceImpl.java new file mode 100644 index 000000000000..eb9d1529d401 --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionApiServiceImpl.java @@ -0,0 +1,174 @@ +// 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.logsws; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.logsws.api.command.admin.CreateLogsWebSessionCmd; +import org.apache.cloudstack.logsws.api.command.admin.DeleteLogsWebSession; +import org.apache.cloudstack.logsws.api.command.admin.ListLogsWebSessionsCmd; +import org.apache.cloudstack.logsws.api.response.LogsWebSessionResponse; +import org.apache.cloudstack.logsws.api.response.LogsWebSessionWebSocketResponse; +import org.apache.cloudstack.logsws.dao.LogsWebSessionDao; +import org.apache.cloudstack.logsws.vo.LogsWebSessionVO; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; + +import com.cloud.api.ApiServlet; +import com.cloud.domain.Domain; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.user.Account; +import com.cloud.user.AccountService; +import com.cloud.user.DomainService; +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.exception.CloudRuntimeException; + +public class LogsWebSessionApiServiceImpl implements LogsWebSessionApiService { + + @Inject + LogsWebSessionManager logsWSManager; + @Inject + LogsWebSessionDao logsWebSessionDao; + @Inject + AccountService accountService; + @Inject + DomainService domainService; + + @Override + public ListResponse listLogsWebSessions(ListLogsWebSessionsCmd cmd) { + final Long id = cmd.getId(); + List responsesList = new ArrayList<>(); + SearchBuilder sb = logsWebSessionDao.createSearchBuilder(); + sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); + SearchCriteria sc = sb.create(); + if (id != null) { + sc.setParameters("id", id); + } + + Filter searchFilter = new Filter(LogsWebSessionVO.class, "id", true, cmd.getStartIndex(), + cmd.getPageSizeVal()); + Pair, Integer> webhooksAndCount = logsWebSessionDao.searchAndCount(sc, searchFilter); + for (LogsWebSessionVO webhook : webhooksAndCount.first()) { + LogsWebSessionResponse response = createLogsWebSessionResponse(webhook); + responsesList.add(response); + } + ListResponse response = new ListResponse<>(); + response.setResponses(responsesList, webhooksAndCount.second()); + return response; + } + + @Override + public LogsWebSessionResponse createLogsWebSession(CreateLogsWebSessionCmd cmd) throws CloudRuntimeException { + final List filters = cmd.getFilters(); + final String extraSecurityToken = cmd.getExtraSecurityToken(); + String clientAddress = null; + Map params = cmd.getFullUrlParams(); + if (MapUtils.isNotEmpty(params)) { + clientAddress = params.get(ApiServlet.CLIENT_INET_ADDRESS_KEY); + } + for (String filter : filters) { + if (StringUtils.isBlank(filter)) { + throw new InvalidParameterValueException(String.format("Invalid value for parameter - %s", + ApiConstants.FILTERS)); + } + } + if (!logsWSManager.canCreateNewLogsWebSession()) { + throw new CloudRuntimeException("Max Logs Web Session limit reached"); + } + final Account account = CallContext.current().getCallingAccount(); + LogsWebSessionVO logsWebSessionVO = new LogsWebSessionVO(filters, account.getDomainId(), account.getAccountId(), + clientAddress); + logsWebSessionVO = logsWebSessionDao.persist(logsWebSessionVO); + return createLogsWebSessionResponse(logsWebSessionVO); + } + + @Override + public boolean deleteLogsWebSession(DeleteLogsWebSession cmd) throws CloudRuntimeException { + final long id = cmd.getId(); + return logsWebSessionDao.remove(id); + } + + protected Set getLogsWebSessionWebSocketResponses( + final LogsWebSessionVO logsWebSessionVO) { + Set responses = new HashSet<>(); + List webSockets = logsWSManager.getLogsWebSessionWebSockets(logsWebSessionVO); + for (LogsWebSessionWebSocket socket : webSockets) { + LogsWebSessionWebSocketResponse webSocketResponse = new LogsWebSessionWebSocketResponse(); + webSocketResponse.setManagementServerId(socket.getManagementServerHost().getUuid()); + webSocketResponse.setManagementServerName(socket.getManagementServerHost().getName()); + webSocketResponse.setHost(socket.getManagementServerHost().getServiceIP()); + webSocketResponse.setPort(socket.getPort()); + webSocketResponse.setPath(socket.getPath()); + responses.add(webSocketResponse); + } + return responses; + } + + protected LogsWebSessionResponse createLogsWebSessionResponse(final LogsWebSessionVO logsWebSessionVO) { + LogsWebSessionResponse response = new LogsWebSessionResponse(); + response.setObjectName("logswebsession"); + response.setId(logsWebSessionVO.getUuid()); + response.setFilters(logsWebSessionVO.getFilters()); + Account account = accountService.getAccount(logsWebSessionVO.getAccountId()); + response.setAccountName(account.getAccountName()); + Domain domain = domainService.getDomain(logsWebSessionVO.getDomainId()); + response.setDomainId(domain.getUuid()); + response.setDomainName(domain.getName()); + response.setDomainPath(domain.getName()); + response.setCreatorAddress(logsWebSessionVO.getCreatorAddress()); + response.setConnected(logsWebSessionVO.getConnections()); + response.setClientAddress(logsWebSessionVO.getClientAddress()); + response.setCreated(logsWebSessionVO.getCreated()); + response.setWebsocketResponse(getLogsWebSessionWebSocketResponses(logsWebSessionVO)); + return response; + } + + @Override + public LogsWebSessionResponse createLogsWebSessionResponse(long logsEndpointId) { + LogsWebSessionVO logsWebSessionVO = logsWebSessionDao.findById(logsEndpointId); + if (logsWebSessionVO == null) { + return null; + } + return createLogsWebSessionResponse(logsWebSessionVO); + } + + @Override + public List> getCommands() { + if (!LogsWebSessionManager.LogsWebServerEnabled.value()) { + return Collections.emptyList(); + } + return List.of( + CreateLogsWebSessionCmd.class, + ListLogsWebSessionsCmd.class, + DeleteLogsWebSession.class + ); + } +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionManager.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionManager.java new file mode 100644 index 000000000000..9150ce866472 --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionManager.java @@ -0,0 +1,80 @@ +// 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.logsws; + +import java.util.List; + +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; + +import com.cloud.utils.component.PluggableService; + +public interface LogsWebSessionManager extends PluggableService, Configurable { + int WS_PORT = 8822; + String WS_PATH = "/logger"; + + ConfigKey LogsWebServerEnabled = new ConfigKey<>("Advanced", Boolean.class, + "logs.web.server.enabled", "false", + "Indicates whether Logs Web Server plugin is enabled or not", + false); + + ConfigKey LogsWebServerConcurrentSessions = new ConfigKey<>("Advanced", Integer.class, + "logs.web.server.concurrent.sessions", "1", + "Number of concurrent sessions that can be created at a time. To allow unlimited a value of zero can used", + true); + + ConfigKey LogsWebServerSessionStaleCleanupInterval = new ConfigKey<>("Advanced", Integer.class, + "logs.web.server.session.stale.cleanup.interval", "600", + "Time(in seconds) after which a stale (not connected or disconnected) Logs Web Server session will be automatically deleted", + false); + + ConfigKey LogsWebServerPort = new ConfigKey<>("Advanced", Integer.class, + "logs.web.server.port", String.valueOf(WS_PORT), + "The port to be used for Logs Web Server", + false, + ConfigKey.Scope.ManagementServer); + + ConfigKey LogsWebServerPath = new ConfigKey<>("Advanced", String.class, + "logs.web.server.path", WS_PATH, + "The path prefix to be used for Logs Web Server", + false, + ConfigKey.Scope.ManagementServer); + + ConfigKey LogsWebServerSessionIdleTimeout = new ConfigKey<>("Advanced", Integer.class, + "logs.web.server.session.idle.timeout", "60", + "Time(in seconds) after which a Logs Web Server session will be automatically disconnected if in idle state", + false, + ConfigKey.Scope.ManagementServer); + + ConfigKey LogsWebServerLogFile = new ConfigKey<>("Advanced", String.class, + "logs.web.server.log.file", "/var/logs/cloudstack/management/management-server.log", + "Log file to be used by Logs Web Server", + true, + ConfigKey.Scope.ManagementServer); + + ConfigKey LogsWebServerSessionTailExistingLines = new ConfigKey<>("Advanced", Integer.class, + "logs.web.server.session.tail.existing.lines", "512", + "Number of existing lines to be read from the logs file on session connect", + true, + ConfigKey.Scope.ManagementServer); + + void startWebSocketServer(); + void stopWebSocketServer(); + List getLogsWebSessionWebSockets(final LogsWebSession logsWebSession); + boolean canCreateNewLogsWebSession(); +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionManagerImpl.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionManagerImpl.java new file mode 100644 index 000000000000..804216130e2e --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionManagerImpl.java @@ -0,0 +1,281 @@ +// 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.logsws; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +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.config.ConfigKey; +import org.apache.cloudstack.logsws.dao.LogsWebSessionDao; +import org.apache.cloudstack.logsws.server.LogsWebSocketServer; +import org.apache.cloudstack.logsws.server.LogsWebSocketServerHelper; +import org.apache.cloudstack.logsws.vo.LogsWebSessionVO; +import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.management.ManagementServerHost; +import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.commons.lang3.StringUtils; + +import com.cloud.cluster.ManagementServerHostVO; +import com.cloud.cluster.dao.ManagementServerHostDao; +import com.cloud.utils.DateUtil; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.concurrency.NamedThreadFactory; +import com.cloud.utils.db.GlobalLock; + +public class LogsWebSessionManagerImpl extends ManagerBase implements LogsWebSessionManager, LogsWebSocketServerHelper { + + @Inject + LogsWebSessionDao logsWebSessionDao; + @Inject + ManagementServerHostDao managementServerHostDao; + + private int serverPort; + private String serverPath; + private int serverIdleTimeoutSeconds; + private LogsWebSocketServer loggerWebSocketServer; + private ScheduledExecutorService staleLogsWebSessionCleanupExecutor; + private Long managementServerId = null; + + protected Long getManagementServerId() { + if (managementServerId != null) { + ManagementServerHostVO managementServerHostVO = + managementServerHostDao.findByMsid(ManagementServerNode.getManagementServerId()); + if (managementServerHostVO != null) { + managementServerId = managementServerHostVO.getId(); + } + } + return managementServerId; + } + + @Override + public String getConfigComponentName() { + return LogsWebSessionManager.class.getSimpleName(); + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + try { + staleLogsWebSessionCleanupExecutor = Executors.newScheduledThreadPool(1, + new NamedThreadFactory("Logs-Web-Sessions-Stale-Cleanup-Worker")); + } catch (final Exception e) { + throw new ConfigurationException("Unable to to configure " + LogsWebSessionManagerImpl.class.getSimpleName()); + } + return true; + } + + @Override + public boolean start() { + if (!LogsWebServerEnabled.value()) { + return true; + } + serverPort = LogsWebServerPort.valueIn(getManagementServerId()); + serverPath = LogsWebServerPath.valueIn(getManagementServerId()); + serverIdleTimeoutSeconds = LogsWebServerSessionIdleTimeout.valueIn(getManagementServerId()); + startWebSocketServer(); + long staleLogsWebSessionCleanupInterval = LogsWebServerSessionStaleCleanupInterval.value(); + staleLogsWebSessionCleanupExecutor.scheduleWithFixedDelay(new StaleLogsWebSessionCleanupWorker(), + staleLogsWebSessionCleanupInterval, staleLogsWebSessionCleanupInterval, TimeUnit.SECONDS); + return true; + } + + @Override + public boolean stop() { + stopWebSocketServer(1); + logsWebSessionDao.markAllActiveAsDisconnected(); + return true; + } + + @Override + public void startWebSocketServer() { + if (loggerWebSocketServer != null && loggerWebSocketServer.isRunning()) { + logger.info("Logger Web Socket Server is already running!"); + return; + } + loggerWebSocketServer = new LogsWebSocketServer(serverPort, serverPath, serverIdleTimeoutSeconds, + this); + try { + loggerWebSocketServer.start(); + } catch (InterruptedException e) { + logger.error("Failed to start Logger Web Socket Server", e); + } + } + + protected void stopWebSocketServer(Integer maxWaitSeconds) { + if (loggerWebSocketServer == null || !loggerWebSocketServer.isRunning()) { + logger.info("Logger Web Socket Server is already stopped!"); + return; + } + loggerWebSocketServer.stop(maxWaitSeconds == null ? 5 : maxWaitSeconds); + loggerWebSocketServer = null; + } + + @Override + public void stopWebSocketServer() { + stopWebSocketServer(null); + } + + private String getLogsWebSessionWebSocketPathUsingVO(long msId, LogsWebSession session) { + LogsWebSessionVO sessionVO = null; + if (session instanceof LogsWebSessionVO) { + sessionVO = (LogsWebSessionVO)session; + } else { + sessionVO = logsWebSessionDao.findById(session.getId()); + } + String path = serverPath; + if (!Objects.equals(msId, getManagementServerId())) { + serverPath = LogsWebServerPath.valueIn(msId); + } + return String.format("%s/%s", path, sessionVO.getUuid()); + } + + @Override + public List> getCommands() { + return List.of(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{ + LogsWebServerEnabled, + LogsWebServerPort, + LogsWebServerPath, + LogsWebServerConcurrentSessions, + LogsWebServerLogFile, + LogsWebServerSessionTailExistingLines, + LogsWebServerSessionIdleTimeout, + LogsWebServerSessionStaleCleanupInterval + }; + } + + @Override + public String getServerPath() { + return serverPath; + } + + @Override + public String getLogFile() { + return LogsWebServerLogFile.valueIn(getManagementServerId()); + } + + @Override + public int getMaxReadExistingLines() { + return LogsWebServerSessionTailExistingLines.valueIn(getManagementServerId()); + } + + @Override + public LogsWebSession getSession(String route) { + if (StringUtils.isBlank(route)) { + return null; + } + return logsWebSessionDao.findByUuid(route); + } + + @Override + public void updateSessionConnection(long sessionId, String clientAddress) { + LogsWebSessionVO logsWebSessionVO = logsWebSessionDao.findById(sessionId); + if (logsWebSessionVO == null) { + return; + } + if (StringUtils.isNotBlank(clientAddress)) { + logsWebSessionVO.setConnections(logsWebSessionVO.getConnections() + 1); + logsWebSessionVO.setConnectedTime(new Date()); + logsWebSessionVO.setClientAddress(clientAddress); + } else { + if (logsWebSessionVO.getConnections() == 0) { + return; + } + logsWebSessionVO.setConnections(Math.max(0, logsWebSessionVO.getConnections() - 1)); + } + logger.trace("Updating session: {}, is connected: {}, connections: {}", + logsWebSessionVO.getUuid(), + StringUtils.isBlank(clientAddress), + logsWebSessionVO.getConnections()); + logsWebSessionDao.update(sessionId, logsWebSessionVO); + } + + @Override + public List getLogsWebSessionWebSockets(final LogsWebSession logsWebSession) { + List webSockets = new ArrayList<>(); + final List activeMsList = + managementServerHostDao.listBy(ManagementServerHost.State.Up); + for (ManagementServerHostVO managementServerHostVO : activeMsList) { + LogsWebSessionWebSocket logsWebSessionWebSocket = new LogsWebSessionWebSocket(managementServerHostVO, + LogsWebServerPort.valueIn(managementServerHostVO.getId()), + getLogsWebSessionWebSocketPathUsingVO(managementServerHostVO.getId(), logsWebSession)); + webSockets.add(logsWebSessionWebSocket); + } + return webSockets; + } + + @Override + public boolean canCreateNewLogsWebSession() { + int maxSessions = LogsWebServerConcurrentSessions.valueIn(getManagementServerId()); + if (maxSessions <= 0) { + return true; + } + return maxSessions > logsWebSessionDao.countConnected(); + } + + public class StaleLogsWebSessionCleanupWorker extends ManagedContextRunnable { + + protected void runCleanupForStaleLogsWebSessions() { + try { + ManagementServerHostVO msHost = managementServerHostDao.findOneByLongestRuntime(); + if (msHost == null || (msHost.getMsid() != ManagementServerNode.getManagementServerId())) { + logger.debug("Skipping the stale logs web sessions cleanup task on this management server"); + return; + } + long cutOffSeconds = LogsWebServerSessionStaleCleanupInterval.value(); + Date cutOffDate = new Date(System.currentTimeMillis() - (cutOffSeconds * 1000)); + String cutOffDateString = DateUtil.getOutputString(cutOffDate); + logger.debug("Clearing stale stale logs web sessions older than {} using management server {}", + cutOffDateString, msHost); + long processed = logsWebSessionDao.removeStaleForCutOff(cutOffDate); + logger.debug("Cleared {} stale stale logs web sessions older than {}", processed, + cutOffDateString); + } catch (Exception e) { + logger.warn("Cleanup task failed to stale logs web sessions", e); + } + } + + @Override + protected void runInContext() { + GlobalLock gcLock = GlobalLock.getInternLock("LogsWebSessionsCleanup"); + try { + if (gcLock.lock(3)) { + try { + runCleanupForStaleLogsWebSessions(); + } finally { + gcLock.unlock(); + } + } + } finally { + gcLock.releaseRef(); + } + } + } +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionWebSocket.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionWebSocket.java new file mode 100644 index 000000000000..f897e8bb8499 --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/LogsWebSessionWebSocket.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.logsws; + +import com.cloud.cluster.ManagementServerHostVO; + +public class LogsWebSessionWebSocket { + + private ManagementServerHostVO managementServerHost; + private int port; + private String path; + + public LogsWebSessionWebSocket(final ManagementServerHostVO managementServerHost, final int port, + final String path) { + this.managementServerHost = managementServerHost; + this.port = port; + this.path = path; + } + + public ManagementServerHostVO getManagementServerHost() { + return managementServerHost; + } + + public int getPort() { + return port; + } + + public String getPath() { + return path; + } +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/api/command/admin/CreateLogsWebSessionCmd.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/api/command/admin/CreateLogsWebSessionCmd.java new file mode 100644 index 000000000000..2f7e8a7a607a --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/api/command/admin/CreateLogsWebSessionCmd.java @@ -0,0 +1,101 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.logsws.api.command.admin; + + +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.logsws.LogsWebSessionApiService; +import org.apache.cloudstack.logsws.LogsWebSession; +import org.apache.cloudstack.logsws.api.response.LogsWebSessionResponse; + +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = "createLogsWebSession", + description = "Creates a session to connect to logs web socket server", + responseObject = LogsWebSessionResponse.class, + responseView = ResponseObject.ResponseView.Restricted, + entityType = {LogsWebSession.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true, + authorized = {RoleType.Admin}, + since = "4.21.0") +public class CreateLogsWebSessionCmd extends BaseCmd { + + @Inject + LogsWebSessionApiService logsWebSessionApiService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.FILTERS, type = CommandType.LIST, collectionType = CommandType.STRING, + description = "List of filter keywords") + private List filters; + + @Parameter(name = ApiConstants.TOKEN, type = CommandType.STRING, + description = "(Optional) extra security token, valid when the extra validation is enabled") + private String extraSecurityToken; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public List getFilters() { + return filters; + } + + public String getExtraSecurityToken() { + return extraSecurityToken; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccountId(); + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ServerApiException { + try { + LogsWebSessionResponse response = logsWebSessionApiService.createLogsWebSession(this); + if (response == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create logs web session"); + } + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (CloudRuntimeException ex) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage()); + } + } + +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/api/command/admin/DeleteLogsWebSession.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/api/command/admin/DeleteLogsWebSession.java new file mode 100644 index 000000000000..01ec709d9eba --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/api/command/admin/DeleteLogsWebSession.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.logsws.api.command.admin; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.logsws.LogsWebSessionApiService; +import org.apache.cloudstack.logsws.LogsWebSession; +import org.apache.cloudstack.logsws.api.response.LogsWebSessionResponse; + +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = "deleteLogsWebSession", + description = "Deletes a logs web session", + responseObject = SuccessResponse.class, + entityType = {LogsWebSession.class}, + authorized = {RoleType.Admin}, + since = "4.21.0") +public class DeleteLogsWebSession extends BaseCmd { + + @Inject + LogsWebSessionApiService logsWebSessionApiService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.ID, type = BaseCmd.CommandType.UUID, + entityType = LogsWebSessionResponse.class, + required = true, + description = "The ID of the logs web session") + 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 (!logsWebSessionApiService.deleteLogsWebSession(this)) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, + String.format("Failed to delete log web session 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/logs-web-server/src/main/java/org/apache/cloudstack/logsws/api/command/admin/ListLogsWebSessionsCmd.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/api/command/admin/ListLogsWebSessionsCmd.java new file mode 100644 index 000000000000..499f97cad26c --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/api/command/admin/ListLogsWebSessionsCmd.java @@ -0,0 +1,70 @@ +// 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.logsws.api.command.admin; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListAccountResourcesCmd; +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.logsws.LogsWebSessionApiService; +import org.apache.cloudstack.logsws.LogsWebSession; +import org.apache.cloudstack.logsws.api.response.LogsWebSessionResponse; + +@APICommand(name = "listLogsWebSessions", + description = "Lists logs web sessions", + responseObject = LogsWebSessionResponse.class, + responseView = ResponseObject.ResponseView.Restricted, + entityType = {LogsWebSession.class}, + authorized = {RoleType.Admin}, + since = "4.21.0") +public class ListLogsWebSessionsCmd extends BaseListAccountResourcesCmd { + + @Inject + LogsWebSessionApiService logsWebSessionApiService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, + entityType = LogsWebSessionResponse.class, + description = "The ID of the logs web session") + private Long id; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + public Long getId() { + return id; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + @Override + public void execute() throws ServerApiException { + ListResponse response = logsWebSessionApiService.listLogsWebSessions(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/api/response/LogsWebSessionResponse.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/api/response/LogsWebSessionResponse.java new file mode 100644 index 000000000000..3fcba9271ac9 --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/api/response/LogsWebSessionResponse.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.logsws.api.response; + + +import java.util.Date; +import java.util.List; +import java.util.Set; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.api.response.ControlledViewEntityResponse; +import org.apache.cloudstack.logsws.LogsWebSession; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = {LogsWebSession.class}) +public class LogsWebSessionResponse extends BaseResponse implements ControlledViewEntityResponse { + @SerializedName(ApiConstants.ID) + @Param(description = "The ID of the logs web session") + private String id; + + @SerializedName(ApiConstants.FILTERS) + @Param(description = "The filters for the logs web session") + private List filters; + + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "The ID of the domain of the logs web session creator") + private String domainId; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "The name of the domain of the logs web session creator") + private String domainName; + + @SerializedName(ApiConstants.DOMAIN_PATH) + @Param(description = "The path of the domain of the logs web session creator") + private String domainPath; + + @SerializedName(ApiConstants.ACCOUNT) + @Param(description = "The account which created the logs web session") + private String accountName; + + @SerializedName(ApiConstants.CREATOR_ADDRESS) + @Param(description = "The address of creator for this logs web session") + private String creatorAddress; + + @SerializedName(ApiConstants.CONNECTED) + @Param(description = "The number of clients connected for this logs web session") + private Integer connected; + + @SerializedName(ApiConstants.CLIENT_ADDRESS) + @Param(description = "The address of the last connected client for this logs web session") + private String clientAddress; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "The date when this logs web session was created") + private Date created; + + @SerializedName(ApiConstants.WEBSOCKET) + @Param(description = "The logs web session websocket options") + private Set websocketResponses; + + public void setId(String id) { + this.id = id; + } + + public void setFilters(List filters) { + this.filters = filters; + } + + @Override + public void setDomainId(String domainId) { + this.domainId = domainId; + } + + @Override + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + @Override + public void setDomainPath(String domainPath) { + this.domainPath = domainPath; + } + + @Override + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + @Override + public void setProjectId(String projectId) { + } + + @Override + public void setProjectName(String projectName) { + } + + public void setCreatorAddress(String creatorAddress) { + this.creatorAddress = creatorAddress; + } + + public void setConnected(Integer connected) { + this.connected = connected; + } + + public void setClientAddress(String clientAddress) { + this.clientAddress = clientAddress; + } + + public void setCreated(Date created) { + this.created = created; + } + + public void setWebsocketResponse(Set websocketResponse) { + this.websocketResponses = websocketResponse; + } +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/api/response/LogsWebSessionWebSocketResponse.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/api/response/LogsWebSessionWebSocketResponse.java new file mode 100644 index 000000000000..60c2d0f9fcf8 --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/api/response/LogsWebSessionWebSocketResponse.java @@ -0,0 +1,86 @@ +// 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.logsws.api.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class LogsWebSessionWebSocketResponse extends BaseResponse { + @SerializedName(ApiConstants.MANAGEMENT_SERVER_ID) + @Param(description = "The ID of the management for this websocket") + private String managementServerId; + + @SerializedName(ApiConstants.MANAGEMENT_SERVER_NAME) + @Param(description = "The name of the management for this websocket") + private String managementServerName; + + @SerializedName("host") + @Param(description = "the websocket host") + private String host; + + @SerializedName(ApiConstants.PORT) + @Param(description = "the websocket port") + private Integer port; + + @SerializedName(ApiConstants.PATH) + @Param(description = "the websocket path") + private String path; + + public String getManagementServerId() { + return managementServerId; + } + + public void setManagementServerId(String managementServerId) { + this.managementServerId = managementServerId; + } + + public String getManagementServerName() { + return managementServerName; + } + + public void setManagementServerName(String managementServerName) { + this.managementServerName = managementServerName; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/dao/LogsWebSessionDao.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/dao/LogsWebSessionDao.java new file mode 100644 index 000000000000..2d9784c847ec --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/dao/LogsWebSessionDao.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.logsws.dao; + +import java.util.Date; +import java.util.List; + +import org.apache.cloudstack.logsws.vo.LogsWebSessionVO; + +import com.cloud.utils.db.GenericDao; + +public interface LogsWebSessionDao extends GenericDao { + List listByAccount(long accountId); + void deleteByAccount(long accountId); + void markAllActiveAsDisconnected(); + int removeStaleForCutOff(Date cutOff); + int countConnected(); +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/dao/LogsWebSessionDaoImpl.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/dao/LogsWebSessionDaoImpl.java new file mode 100644 index 000000000000..95415f27a78d --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/dao/LogsWebSessionDaoImpl.java @@ -0,0 +1,101 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.logsws.dao; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.logsws.vo.LogsWebSessionVO; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.GenericSearchBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.UpdateBuilder; + +public class LogsWebSessionDaoImpl extends GenericDaoBase implements LogsWebSessionDao { + SearchBuilder accountIdSearch; + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + super.configure(name, params); + + accountIdSearch = createSearchBuilder(); + accountIdSearch.and("accountId", accountIdSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + + return true; + } + + @Override + public void deleteByAccount(long accountId) { + SearchCriteria sc = accountIdSearch.create(); + sc.setParameters("accountId", accountId); + remove(sc); + } + + @Override + public List listByAccount(long accountId) { + SearchCriteria sc = accountIdSearch.create(); + sc.setParameters("accountId", accountId); + return listBy(sc); + } + + @Override + public void markAllActiveAsDisconnected() { + SearchBuilder sb = createSearchBuilder(); + sb.and("connections", sb.entity().getConnections(), SearchCriteria.Op.GT); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("connections", 0); + LogsWebSessionVO logsWebSessionVO = createForUpdate(); + logsWebSessionVO.setConnections(0); + UpdateBuilder updateBuilder = getUpdateBuilder(logsWebSessionVO); + update(updateBuilder, sc, null); + } + + @Override + public int removeStaleForCutOff(Date cutOff) { + SearchBuilder sb = createSearchBuilder(); + sb.and("connections", sb.entity().getConnections(), SearchCriteria.Op.EQ); + sb.and().op("connected_time", sb.entity().getConnectedTime(), SearchCriteria.Op.LT); + sb.or().op("null_connected_time", sb.entity().getConnectedTime(), SearchCriteria.Op.NULL); + sb.and("created", sb.entity().getCreated(), SearchCriteria.Op.LT); + sb.cp(); + sb.cp(); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("connections", 0); + sc.setParameters("connected_time", cutOff); + sc.setParameters("created", cutOff); + return remove(sc); + } + + @Override + public int countConnected() { + GenericSearchBuilder sb = createSearchBuilder(Integer.class); + sb.and("connections", sb.entity().getConnections(), SearchCriteria.Op.GT); + sb.select(null, SearchCriteria.Func.COUNT, sb.entity().getId()); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("connections", 0); + return customSearch(sc, null).get(0); + } +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/logreader/FilteredLogTailerListener.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/logreader/FilteredLogTailerListener.java new file mode 100644 index 000000000000..33c88815ef7b --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/logreader/FilteredLogTailerListener.java @@ -0,0 +1,71 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.logsws.logreader; + +import java.util.List; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.io.input.TailerListenerAdapter; +import org.apache.commons.lang3.StringUtils; + +import io.netty.channel.Channel; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; + +public class FilteredLogTailerListener extends TailerListenerAdapter { + private final List filters; + private final Channel channel; + private final boolean isFilterEmpty; + private boolean isLastLineValid; + + public static boolean isValidLine(String line, boolean isFilterEmpty, + boolean isLastLineValid, List filters) { + if (StringUtils.isBlank(line)) { + return false; + } + if (isFilterEmpty) { + return true; + } + if (isLastLineValid && !line.startsWith("2025")) { + return true; + } + for (String filter : filters) { + if (line.contains(filter)) { + return true; + } + } + return false; + } + + public FilteredLogTailerListener(List filters, Channel channel) { + this.filters = filters; + this.channel = channel; + isFilterEmpty = CollectionUtils.isEmpty(filters); + isLastLineValid = false; + } + + @Override + public void handle(String line) { + // Check if the line contains the filter string + if (isValidLine(line, isFilterEmpty, isLastLineValid, filters)) { + channel.writeAndFlush(new TextWebSocketFrame(line)); + isLastLineValid = true; + } else { + isLastLineValid = false; + } + } +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/server/LogsWebSocketBroadcastHandler.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/server/LogsWebSocketBroadcastHandler.java new file mode 100644 index 000000000000..f9640715f4d1 --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/server/LogsWebSocketBroadcastHandler.java @@ -0,0 +1,175 @@ +// 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.logsws.server; + +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.apache.cloudstack.logsws.LogsWebSession; +import org.apache.cloudstack.logsws.logreader.FilteredLogTailerListener; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.io.input.ReversedLinesFileReader; +import org.apache.commons.io.input.Tailer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.util.ReferenceCountUtil; + +public class LogsWebSocketBroadcastHandler extends ChannelInboundHandlerAdapter { + protected static Logger LOGGER = LogManager.getLogger(LogsWebSocketBroadcastHandler.class); + private String route; + private Tailer tailer; + private ExecutorService tailerExecutor; + private final LogsWebSocketServerHelper serverHelper; + private LogsWebSession logsWebSession; + + public LogsWebSocketBroadcastHandler(final LogsWebSocketServerHelper serverHelper) { + this.serverHelper = serverHelper; + } + + private void startTestBroadcasting(final ChannelHandlerContext ctx) { + String route = ctx.channel().attr(LogsWebSocketRoutingHandler.LOGGER_ROUTE_ATTR).get(); + // Schedule a periodic task to send messages every 5 seconds. + ctx.executor().scheduleAtFixedRate(() -> { + if (ctx.channel().isActive()) { + String msg = String.format("Hello from Logger broadcaster! Route: %s", route); + ctx.writeAndFlush(new TextWebSocketFrame(msg)); + LOGGER.debug("Broadcasting message: '{}' for context: {}", msg, ctx.hashCode()); + } + }, 0, 5, TimeUnit.SECONDS); + } + + private void processExistingLines(final ChannelHandlerContext ctx, File logFile, final List filters) { + try (ReversedLinesFileReader reader = new ReversedLinesFileReader(logFile, StandardCharsets.UTF_8)) { + List lastLines = new ArrayList<>(); + String line; + int count = 0; + // Read lines in reverse order up to 200 lines + while ((line = reader.readLine()) != null && count < serverHelper.getMaxReadExistingLines()) { + lastLines.add(line); + count++; + } + // Reverse to restore original order + Collections.reverse(lastLines); + // Process each line that matches the filter + boolean isFilterEmpty = CollectionUtils.isEmpty(filters); + boolean isLastLineValid = false; + for (String l : lastLines) { + if (FilteredLogTailerListener.isValidLine(l, isFilterEmpty, isLastLineValid, filters)) { + ctx.writeAndFlush(new TextWebSocketFrame(l)); + isLastLineValid = true; + } else { + isLastLineValid = false; + } + } + } catch (IOException e) { + ctx.writeAndFlush(new TextWebSocketFrame("Error reading existing log lines: " + e.getMessage())); + } + } + + private void startLogTailing(ChannelHandlerContext ctx, List filters, File logFile) { + // Create the listener to filter new log lines + FilteredLogTailerListener listener = new FilteredLogTailerListener(filters, ctx.channel()); + // Use 'true' to start tailing from the end of the file (since we've already processed existing lines) + tailer = new Tailer(logFile, listener, 100, true); + + // Use an executor service for managing the tailer thread + tailerExecutor = Executors.newSingleThreadExecutor(); + tailerExecutor.submit(tailer); + } + + private void startLogsBroadcasting(final ChannelHandlerContext ctx) { + route = ctx.channel().attr(LogsWebSocketRoutingHandler.LOGGER_ROUTE_ATTR).get(); + logsWebSession = serverHelper.getSession(route); + if (logsWebSession == null) { + LOGGER.warn("Unauthorized session for route: {}", route); + ctx.close(); + return; + } + File logFile = new File(serverHelper.getLogFile()); + if (!logFile.exists() || !logFile.canRead()) { + ctx.channel().writeAndFlush(new TextWebSocketFrame("Log file not available or cannot be read.")); + return; + } + InetSocketAddress clientAddress = (InetSocketAddress) ctx.channel().remoteAddress(); + serverHelper.updateSessionConnection(logsWebSession.getId(), clientAddress.getAddress().getHostAddress()); + processExistingLines(ctx, logFile, logsWebSession.getFilters()); + startLogTailing(ctx, logsWebSession.getFilters(), logFile); + } + + private void stopLogsBroadcasting() { + if (tailer != null) { + tailer.stop(); + } + if (tailerExecutor != null && !tailerExecutor.isShutdown()) { + tailerExecutor.shutdownNow(); + } + if (logsWebSession != null) { + serverHelper.updateSessionConnection(logsWebSession.getId(), null); + } + } + + @Override + public void channelActive(final ChannelHandlerContext ctx) throws Exception { + LOGGER.debug("Channel is active, context: {}", ctx.hashCode()); + super.channelActive(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + LOGGER.debug("Channel is being closed for route: {}, context: {}", route, ctx.hashCode()); + stopLogsBroadcasting(); + super.channelInactive(ctx); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + LOGGER.debug("User event triggered: {}, context: {}", evt, ctx.hashCode()); + if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) { + startLogsBroadcasting(ctx); + } else if (evt instanceof IdleStateEvent) { + IdleStateEvent event = (IdleStateEvent) evt; + if (IdleState.WRITER_IDLE.equals(event.state())) { + ctx.channel().writeAndFlush(new TextWebSocketFrame("Connection idle for 1 minute, closing connection.")); + ctx.close(); + return; + } + } + super.userEventTriggered(ctx, evt); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + // Discard any messages received from the client. + ReferenceCountUtil.release(msg); + } +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/server/LogsWebSocketRouteManager.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/server/LogsWebSocketRouteManager.java new file mode 100644 index 000000000000..be47b9457044 --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/server/LogsWebSocketRouteManager.java @@ -0,0 +1,51 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.logsws.server; + +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import io.netty.channel.group.ChannelGroup; +import io.netty.channel.group.DefaultChannelGroup; +import io.netty.util.concurrent.GlobalEventExecutor; + +public class LogsWebSocketRouteManager { + protected static Logger LOGGER = LogManager.getLogger(LogsWebSocketRouteManager.class); + private final ConcurrentHashMap routeMap = new ConcurrentHashMap<>(); + + public void addRoute(String route) { + routeMap.putIfAbsent(route, new DefaultChannelGroup(GlobalEventExecutor.INSTANCE)); + LOGGER.debug("Added route: {}", route); + } + + public void removeRoute(String route) { + ChannelGroup group = routeMap.remove(route); + if (group == null) { + LOGGER.debug("Route: {} doesn't exist", route); + return; + } + group.close(); + LOGGER.debug("Removed route: {}", route); + } + + public ChannelGroup getRouteGroup(String route) { + return routeMap.get(route); + } +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/server/LogsWebSocketRoutingHandler.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/server/LogsWebSocketRoutingHandler.java new file mode 100644 index 000000000000..b2c69fb08dfb --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/server/LogsWebSocketRoutingHandler.java @@ -0,0 +1,107 @@ +// 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.logsws.server; + +import org.apache.cloudstack.logsws.LogsWebSession; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.group.ChannelGroup; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.util.AttributeKey; + +public class LogsWebSocketRoutingHandler extends ChannelInboundHandlerAdapter { + protected static Logger LOGGER = LogManager.getLogger(LogsWebSocketRoutingHandler.class); + public static final AttributeKey LOGGER_ROUTE_ATTR = AttributeKey.valueOf("loggerRoute"); + private final LogsWebSocketRouteManager routeManager; + private final LogsWebSocketServerHelper serverHelper; + + public LogsWebSocketRoutingHandler(LogsWebSocketRouteManager routeManager, + LogsWebSocketServerHelper serverHelper) { + this.routeManager = routeManager; + this.serverHelper = serverHelper; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (!(msg instanceof FullHttpRequest)) { + ctx.fireChannelRead(msg); + return; + } + FullHttpRequest req = (FullHttpRequest) msg; + String uri = req.uri(); + LOGGER.debug("Original URI: {}", uri); + final String serverPath = serverHelper.getServerPath(); + final String expectedPathPrefix = serverPath + "/"; + if (!uri.startsWith(expectedPathPrefix)) { + ctx.close(); + return; + } + // Extract the route portion. + String route = uri.substring(expectedPathPrefix.length()); + if (route.isEmpty()) { + ctx.close(); + return; + } + + LogsWebSession session = serverHelper.getSession(route); + if (session == null) { + LOGGER.warn("Unauthorized connection attempt for route: {}", route); + ctx.close(); + return; + } + // Retrieve or add the route. + ChannelGroup group = routeManager.getRouteGroup(route); + if (group == null) { + routeManager.addRoute(route); + group = routeManager.getRouteGroup(route); + } else { + // If there's already a connection, close it to allow only one connection per route. + if (!group.isEmpty()) { + LOGGER.debug("Closing existing connection(s) for route: {}", route); + group.close(); // This will close all existing channels in the group. + } + } + + LOGGER.debug("Connecting to route: {} for context: {}", route, ctx.hashCode()); + ctx.channel().attr(LOGGER_ROUTE_ATTR).set(route); + group.add(ctx.channel()); + + // Rewrite the URI so that the handshake matches the expected sever path + if (req instanceof DefaultFullHttpRequest) { + ((DefaultFullHttpRequest) req).setUri(serverPath); + } else { + DefaultFullHttpRequest newReq = new DefaultFullHttpRequest( + req.protocolVersion(), req.method(), serverPath, req.content().retain()); + newReq.headers().setAll(req.headers()); + req.release(); + req = newReq; + } + LOGGER.debug("Rewritten URI: {}", req.uri()); + ctx.fireChannelRead(req); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + LOGGER.error("Exception in LoggerWebSocketRoutingHandler", cause); + ctx.close(); + } +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/server/LogsWebSocketServer.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/server/LogsWebSocketServer.java new file mode 100644 index 000000000000..fb2965ab1960 --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/server/LogsWebSocketServer.java @@ -0,0 +1,123 @@ +// 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.logsws.server; + +import java.util.concurrent.TimeUnit; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.timeout.IdleStateHandler; + +public class LogsWebSocketServer { + + protected static Logger LOGGER = LogManager.getLogger(LogsWebSocketServer.class); + + private final int port; + private final String path; + private final int idleTimeoutSeconds; + private EventLoopGroup bossGroup; + private EventLoopGroup workerGroup; + private Channel serverChannel; + private boolean running; + private final LogsWebSocketRouteManager routeManager; + private final LogsWebSocketServerHelper serverHelper; + + public LogsWebSocketServer(final int port, final String path, final int idleTimeoutSeconds, + final LogsWebSocketServerHelper serverHelper) { + this.port = port; + this.path = path; + this.idleTimeoutSeconds = idleTimeoutSeconds; + this.serverHelper = serverHelper; + this.routeManager = new LogsWebSocketRouteManager(); + } + + public void start() throws InterruptedException { + bossGroup = new NioEventLoopGroup(1); + workerGroup = new NioEventLoopGroup(); + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(new HttpServerCodec()); + pipeline.addLast(new HttpObjectAggregator(65536)); + pipeline.addLast(new LogsWebSocketRoutingHandler(routeManager, serverHelper)); + pipeline.addLast(new WebSocketServerProtocolHandler(path, null, true)); + pipeline.addLast("idleStateHandler", new IdleStateHandler(0, idleTimeoutSeconds, 0, TimeUnit.SECONDS)); + pipeline.addLast(new LogsWebSocketBroadcastHandler(serverHelper)); + } + }); + + // Bind and store the server channel. + serverChannel = b.bind(port).sync().channel(); + LOGGER.debug("Logger WebSocket server started on port {}", port); + // Note: We do not block here with serverChannel.closeFuture().sync() + running = true; + } + + // Stop the server gracefully. + public void stop() { + stop(5); + } + + public void stop(long maxWaitSeconds) { + try { + if (serverChannel != null) { + serverChannel.close().sync(); + } + if (bossGroup != null) { + bossGroup.shutdownGracefully(0, maxWaitSeconds, TimeUnit.SECONDS).sync(); + } + if (workerGroup != null) { + workerGroup.shutdownGracefully(0, maxWaitSeconds, TimeUnit.SECONDS).sync(); + } + } catch (InterruptedException e) { + LOGGER.error("Failed to stop WebSocket server properly with timeout {}s, forcefully stopping", + maxWaitSeconds, e); + if (serverChannel != null && serverChannel.isOpen()) { + serverChannel.close(); + } + if (bossGroup != null && !bossGroup.isTerminated()) { + bossGroup.shutdownGracefully(0, 0, TimeUnit.SECONDS); + } + if (workerGroup != null && !workerGroup.isTerminated()) { + workerGroup.shutdownGracefully(0, 0, TimeUnit.SECONDS); + } + } + LOGGER.debug("Logger WebSocket server stopped"); + running = false; + } + + public boolean isRunning() { + return running; + } +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/server/LogsWebSocketServerHelper.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/server/LogsWebSocketServerHelper.java new file mode 100644 index 000000000000..c630f3a4d26a --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/server/LogsWebSocketServerHelper.java @@ -0,0 +1,28 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.logsws.server; + +import org.apache.cloudstack.logsws.LogsWebSession; + +public interface LogsWebSocketServerHelper { + String getServerPath(); + String getLogFile(); + int getMaxReadExistingLines(); + LogsWebSession getSession(String route); + void updateSessionConnection(long sessionId, String clientAddress); +} diff --git a/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/vo/LogsWebSessionVO.java b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/vo/LogsWebSessionVO.java new file mode 100644 index 000000000000..e1cef92b206a --- /dev/null +++ b/plugins/logs-web-server/src/main/java/org/apache/cloudstack/logsws/vo/LogsWebSessionVO.java @@ -0,0 +1,192 @@ +// 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.logsws.vo; + + +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Convert; +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.logsws.LogsWebSession; +import org.apache.cloudstack.util.StringListJsonConverter; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name = "logs_web_session") +public class LogsWebSessionVO implements LogsWebSession { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "filter", columnDefinition = "json") + @Convert(converter = StringListJsonConverter.class) + private List filters; + + @Column(name = "domain_id") + private long domainId; + + @Column(name = "account_id") + private long accountId; + + @Column(name = "creator_address") + private String creatorAddress; + + @Column(name = "connections") + private int connections; + + @Column(name = "connected_time") + @Temporal(value = TemporalType.TIMESTAMP) + private Date connectedTime; + + @Column(name = "client_address") + private String clientAddress; + + @Column(name = GenericDao.CREATED_COLUMN) + private Date created; + + @Column(name = GenericDao.REMOVED_COLUMN) + private Date removed; + + @Override + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + @Override + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + @Override + public List getFilters() { + return filters; + } + + @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 int getConnections() { + return connections; + } + + public void setConnections(int connections) { + this.connections = connections; + } + + @Override + public Date getConnectedTime() { + return connectedTime; + } + + public void setConnectedTime(Date connectedTime) { + this.connectedTime = connectedTime; + } + + @Override + public String getCreatorAddress() { + return creatorAddress; + } + + public void setCreatorAddress(String creatorAddress) { + this.creatorAddress = creatorAddress; + } + + @Override + public String getClientAddress() { + return clientAddress; + } + + public void setClientAddress(String clientAddress) { + this.clientAddress = clientAddress; + } + + @Override + public Date getCreated() { + return created; + } + + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + public LogsWebSessionVO() { + this.uuid = UUID.randomUUID().toString(); + } + + public LogsWebSessionVO(List filters, long domainId, long accountId, String creatorAddress) { + this.filters = filters; + this.uuid = UUID.randomUUID().toString(); + this.domainId = domainId; + this.accountId = accountId; + this.creatorAddress = creatorAddress; + } + + @Override + public Class getEntityType() { + return LogsWebSession.class; + } + + @Override + public String getName() { + return uuid; + } +} diff --git a/plugins/logs-web-server/src/main/resources/META-INF/cloudstack/logs-web-server/module.properties b/plugins/logs-web-server/src/main/resources/META-INF/cloudstack/logs-web-server/module.properties new file mode 100644 index 000000000000..1839e8819b2f --- /dev/null +++ b/plugins/logs-web-server/src/main/resources/META-INF/cloudstack/logs-web-server/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=logs-web-server +parent=backend diff --git a/plugins/logs-web-server/src/main/resources/META-INF/cloudstack/logs-web-server/spring-logs-web-server-context.xml b/plugins/logs-web-server/src/main/resources/META-INF/cloudstack/logs-web-server/spring-logs-web-server-context.xml new file mode 100644 index 000000000000..dc42491d9570 --- /dev/null +++ b/plugins/logs-web-server/src/main/resources/META-INF/cloudstack/logs-web-server/spring-logs-web-server-context.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/plugins/logs-web-server/src/test/java/org/apache/cloudstack/logsws/LogsWebSessionManagerImplTest.java b/plugins/logs-web-server/src/test/java/org/apache/cloudstack/logsws/LogsWebSessionManagerImplTest.java new file mode 100644 index 000000000000..df4062f03c19 --- /dev/null +++ b/plugins/logs-web-server/src/test/java/org/apache/cloudstack/logsws/LogsWebSessionManagerImplTest.java @@ -0,0 +1,52 @@ +// 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.logsws; + +import java.util.UUID; + +import org.apache.cloudstack.logsws.dao.LogsWebSessionDao; +import org.apache.cloudstack.logsws.vo.LogsWebSessionVO; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class LogsWebSessionManagerImplTest { + + @Mock + LogsWebSessionDao logsWebSessionDao; + + @InjectMocks + LogsWebSessionManagerImpl logsWSManager = new LogsWebSessionManagerImpl(); + + @Test + public void test_getSession_nullRoute() { + Assert.assertNull(logsWSManager.getSession(null)); + Assert.assertNull(logsWSManager.getSession("abc")); + } + + @Test + public void test_getSession_validRoute() { + String uuid = UUID.randomUUID().toString(); + Mockito.when(logsWebSessionDao.findByUuid(uuid)).thenReturn(Mockito.mock(LogsWebSessionVO.class)); + Assert.assertNotNull(logsWSManager.getSession(uuid)); + } +} diff --git a/plugins/pom.xml b/plugins/pom.xml index db228881a915..d43fc10a0169 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -98,6 +98,8 @@ integrations/prometheus integrations/kubernetes-service + logs-web-server + metrics network-elements/bigswitch diff --git a/pom.xml b/pom.xml index 3e30ff3fe997..7f20a947d897 100644 --- a/pom.xml +++ b/pom.xml @@ -190,6 +190,7 @@ 5.3.26 0.5.4 3.1.7 + 4.1.95.Final @@ -737,6 +738,11 @@ java-linstor ${cs.java-linstor.version} + + io.netty + netty-all + ${cs.netty.all.version} + diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java index b8227ef9d589..5f3788716957 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -16,6 +16,9 @@ // under the License. package com.cloud.api; +import static com.cloud.user.AccountManagerImpl.apiKeyAccess; +import static org.apache.cloudstack.user.UserPasswordResetManager.UserPasswordResetEnabled; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InterruptedIOException; @@ -57,15 +60,6 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import com.cloud.cluster.ManagementServerHostVO; -import com.cloud.cluster.dao.ManagementServerHostDao; -import com.cloud.user.Account; -import com.cloud.user.AccountManager; -import com.cloud.user.AccountManagerImpl; -import com.cloud.user.DomainManager; -import com.cloud.user.User; -import com.cloud.user.UserAccount; -import com.cloud.user.UserVO; import org.apache.cloudstack.acl.APIChecker; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; @@ -104,6 +98,7 @@ import org.apache.cloudstack.api.response.LoginCmdResponse; import org.apache.cloudstack.config.ApiServiceConfiguration; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.context.LogContext; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.events.EventDistributor; @@ -153,6 +148,8 @@ import com.cloud.api.dispatch.DispatchChainFactory; import com.cloud.api.dispatch.DispatchTask; import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.cluster.ManagementServerHostVO; +import com.cloud.cluster.dao.ManagementServerHostDao; import com.cloud.domain.Domain; import com.cloud.domain.DomainVO; import com.cloud.domain.dao.DomainDao; @@ -171,14 +168,22 @@ import com.cloud.exception.UnavailableCommandException; import com.cloud.projects.dao.ProjectDao; import com.cloud.storage.VolumeApiService; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.user.AccountManagerImpl; +import com.cloud.user.DomainManager; +import com.cloud.user.User; +import com.cloud.user.UserAccount; +import com.cloud.user.UserVO; import com.cloud.utils.ConstantTimeComparator; import com.cloud.utils.DateUtil; import com.cloud.utils.HttpUtils; -import com.cloud.utils.HttpUtils.ApiSessionKeySameSite; import com.cloud.utils.HttpUtils.ApiSessionKeyCheckOption; +import com.cloud.utils.HttpUtils.ApiSessionKeySameSite; import com.cloud.utils.Pair; import com.cloud.utils.ReflectUtil; import com.cloud.utils.StringUtils; +import com.cloud.utils.UuidUtils; import com.cloud.utils.component.ComponentContext; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.component.PluggableService; @@ -191,9 +196,6 @@ import com.cloud.utils.net.NetUtils; import com.google.gson.reflect.TypeToken; -import static com.cloud.user.AccountManagerImpl.apiKeyAccess; -import static org.apache.cloudstack.user.UserPasswordResetManager.UserPasswordResetEnabled; - @Component public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiServerService, Configurable { private static final Logger ACCESSLOGGER = LogManager.getLogger("apiserver." + ApiServer.class.getName()); @@ -720,12 +722,17 @@ public String handleRequest(final Map params, final String responseType, final S return response; } + private String getCurrentLogContextId() { + return UuidUtils.first(LogContext.current().getLogContextId()); + } + private String getBaseAsyncResponse(final long jobId, final BaseAsyncCmd cmd) { final AsyncJobResponse response = new AsyncJobResponse(); final AsyncJob job = entityMgr.findByIdIncludingRemoved(AsyncJob.class, jobId); response.setJobId(job.getUuid()); response.setResponseName(cmd.getCommandName()); + response.addLogIds(UuidUtils.first(job.getUuid()), getCurrentLogContextId()); return ApiResponseSerializer.toSerializedString(response, cmd.getResponseType()); } @@ -735,6 +742,7 @@ private String getBaseAsyncCreateResponse(final long jobId, final BaseAsyncCreat response.setJobId(job.getUuid()); response.setId(objectUuid); response.setResponseName(cmd.getCommandName()); + response.addLogIds(UuidUtils.first(job.getUuid()), getCurrentLogContextId()); return ApiResponseSerializer.toSerializedString(response, cmd.getResponseType()); } @@ -852,7 +860,9 @@ private String queueCommand(final BaseCmd cmdObj, final Map para } SerializationContext.current().setUuidTranslation(true); - return ApiResponseSerializer.toSerializedStringWithSecureLogs((ResponseObject)cmdObj.getResponseObject(), cmdObj.getResponseType(), log); + ResponseObject responseObject = (ResponseObject)cmdObj.getResponseObject(); + responseObject.addLogIds(getCurrentLogContextId()); + return ApiResponseSerializer.toSerializedStringWithSecureLogs(responseObject, cmdObj.getResponseType(), log); } } @@ -887,6 +897,7 @@ private void buildAsyncListResponse(final BaseListCmd command, final Account acc final AsyncJob job = objectJobMap.get(response.getObjectId()); response.setJobId(job.getUuid()); response.setJobStatus(job.getStatus().ordinal()); + response.addLogIds(getCurrentLogContextId()); } } } diff --git a/server/src/main/java/com/cloud/api/ApiServlet.java b/server/src/main/java/com/cloud/api/ApiServlet.java index 4994c42bb4dc..fe93d6743371 100644 --- a/server/src/main/java/com/cloud/api/ApiServlet.java +++ b/server/src/main/java/com/cloud/api/ApiServlet.java @@ -78,6 +78,7 @@ public class ApiServlet extends HttpServlet { private static final Logger ACCESSLOGGER = LogManager.getLogger("apiserver." + ApiServlet.class.getName()); private static final String REPLACEMENT = "_"; private static final String LOGGER_REPLACEMENTS = "[\n\r\t]"; + public static final String CLIENT_INET_ADDRESS_KEY = "client-inet-address"; @Inject ApiServerService apiServer; diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 56a86e65da02..56c6c844fd03 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -149,6 +149,9 @@ import com.cloud.api.query.vo.NetworkOfferingJoinVO; import com.cloud.capacity.CapacityManager; import com.cloud.capacity.dao.CapacityDao; +import com.cloud.cluster.ManagementServerHostVO; +import com.cloud.cluster.dao.ManagementServerHostDao; +import com.cloud.cluster.dao.ManagementServerHostDetailsDao; import com.cloud.configuration.Resource.ResourceType; import com.cloud.dc.AccountVlanMapVO; import com.cloud.dc.ClusterDetailsDao; @@ -448,6 +451,10 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati @Inject ImageStoreDetailsDao _imageStoreDetailsDao; @Inject + ManagementServerHostDao managementServerHostDao; + @Inject + ManagementServerHostDetailsDao managementServerHostDetailsDao; + @Inject MessageBus messageBus; @Inject AgentManager _agentManager; @@ -805,6 +812,13 @@ public String updateConfiguration(final long userId, final String name, final St } break; + case ManagementServer: + final ManagementServerHostVO managementServer = managementServerHostDao.findById(resourceId); + Preconditions.checkState(managementServer != null); + resourceType = ApiCommandResourceType.ManagementServer; + managementServerHostDetailsDao.addDetail(resourceId, name, value, true); + break; + default: throw new InvalidParameterValueException("Scope provided is invalid"); } @@ -938,8 +952,9 @@ public Configuration updateConfiguration(final UpdateCfgCmd cmd) throws InvalidP String value = cmd.getValue(); final Long zoneId = cmd.getZoneId(); final Long clusterId = cmd.getClusterId(); - final Long storagepoolId = cmd.getStoragepoolId(); + final Long storagepoolId = cmd.getStoragePoolId(); final Long imageStoreId = cmd.getImageStoreId(); + final Long managementServerId = cmd.getManagementServerId(); Long accountId = cmd.getAccountId(); Long domainId = cmd.getDomainId(); // check if config value exists @@ -1016,6 +1031,11 @@ public Configuration updateConfiguration(final UpdateCfgCmd cmd) throws InvalidP id = imageStoreId; paramCountCheck++; } + if (managementServerId != null) { + scope = ConfigKey.Scope.ManagementServer.toString(); + id = managementServerId; + paramCountCheck++; + } if (paramCountCheck > 1) { throw new InvalidParameterValueException("cannot handle multiple IDs, provide only one ID corresponding to the scope"); @@ -1068,6 +1088,7 @@ public Pair resetConfiguration(final ResetCfgCmd cmd) thr final Long accountId = cmd.getAccountId(); final Long domainId = cmd.getDomainId(); final Long imageStoreId = cmd.getImageStoreId(); + final Long managementServerId = cmd.getManagementServerId(); ConfigKey configKey = null; Optional optionalValue; String defaultValue; @@ -1101,6 +1122,7 @@ public Pair resetConfiguration(final ResetCfgCmd cmd) thr scopeMap.put(ConfigKey.Scope.Account.toString(), accountId); scopeMap.put(ConfigKey.Scope.StoragePool.toString(), storagepoolId); scopeMap.put(ConfigKey.Scope.ImageStore.toString(), imageStoreId); + scopeMap.put(ConfigKey.Scope.ManagementServer.toString(), managementServerId); ParamCountPair paramCountPair = getParamCount(scopeMap); id = paramCountPair.getId(); @@ -1196,6 +1218,16 @@ public Pair resetConfiguration(final ResetCfgCmd cmd) thr newValue = optionalValue.isPresent() ? optionalValue.get().toString() : defaultValue; break; + case ManagementServer: + final ManagementServerHostVO managementServer = managementServerHostDao.findById(id); + if (managementServer == null) { + throw new InvalidParameterValueException("unable to find management server by id " + id); + } + managementServerHostDetailsDao.removeDetail(id, name); + optionalValue = Optional.ofNullable(configKey != null ? configKey.valueIn(id) : config.getValue()); + newValue = optionalValue.isPresent() ? optionalValue.get().toString() : defaultValue; + break; + default: if (!_configDao.update(name, category, defaultValue)) { logger.error("Failed to reset configuration option, name: " + name + ", defaultValue:" + defaultValue); diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index d2ddbddcb484..aad6cdbc9b8d 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -2202,6 +2202,7 @@ public Pair, Integer> searchForConfigurations(fina final Long clusterId = cmd.getClusterId(); final Long storagepoolId = cmd.getStoragepoolId(); final Long imageStoreId = cmd.getImageStoreId(); + final Long managementServerId = cmd.getManagementServerId(); Long accountId = cmd.getAccountId(); Long domainId = cmd.getDomainId(); final String groupName = cmd.getGroupName(); @@ -2255,6 +2256,11 @@ public Pair, Integer> searchForConfigurations(fina id = imageStoreId; paramCountCheck++; } + if (managementServerId != null) { + scope = ConfigKey.Scope.ManagementServer; + id = managementServerId; + paramCountCheck++; + } if (paramCountCheck > 1) { throw new InvalidParameterValueException("cannot handle multiple IDs, provide only one ID corresponding to the scope"); @@ -4489,6 +4495,7 @@ public Map listCapabilities(final ListCapabilitiesCmd cmd) { final boolean kubernetesServiceEnabled = Boolean.parseBoolean(_configDao.getValue("cloud.kubernetes.service.enabled")); final boolean kubernetesClusterExperimentalFeaturesEnabled = Boolean.parseBoolean(_configDao.getValue("cloud.kubernetes.cluster.experimental.features.enabled")); + final boolean logsWebServerEnabled = Boolean.parseBoolean(_configDao.getValue("logs.web.server.enabled")); // check if region-wide secondary storage is used boolean regionSecondaryEnabled = false; @@ -4529,6 +4536,7 @@ public Map listCapabilities(final ListCapabilitiesCmd cmd) { } capabilities.put(ApiConstants.SHAREDFSVM_MIN_CPU_COUNT, fsVmMinCpu); capabilities.put(ApiConstants.SHAREDFSVM_MIN_RAM_SIZE, fsVmMinRam); + capabilities.put(ApiConstants.LOGS_WEB_SERVER_ENABLED, logsWebServerEnabled); return capabilities; } diff --git a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java index 8c714b57cdbb..26d2d9d5b934 100644 --- a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java +++ b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java @@ -866,14 +866,9 @@ public void shouldValidateConfigRangeTestValueIsNotNullAndConfigHasRangeReturnTr @Test public void testResetConfigurations() { Long poolId = 1L; - ResetCfgCmd cmd = Mockito.mock(ResetCfgCmd.class); - Mockito.when(cmd.getCfgName()).thenReturn("pool.storage.capacity.disablethreshold"); - Mockito.when(cmd.getStoragepoolId()).thenReturn(poolId); - Mockito.when(cmd.getZoneId()).thenReturn(null); - Mockito.when(cmd.getClusterId()).thenReturn(null); - Mockito.when(cmd.getAccountId()).thenReturn(null); - Mockito.when(cmd.getDomainId()).thenReturn(null); - Mockito.when(cmd.getImageStoreId()).thenReturn(null); + ResetCfgCmd cmd = new ResetCfgCmd(); + ReflectionTestUtils.setField(cmd, "storagePoolId", poolId); + ReflectionTestUtils.setField(cmd, "cfgName", "pool.storage.capacity.disablethreshold"); ConfigurationVO cfg = new ConfigurationVO("Advanced", "DEFAULT", "test", "pool.storage.capacity.disablethreshold", null, "description"); cfg.setScope(10); diff --git a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java index 8b9928733ec9..9c85cadf0409 100644 --- a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java +++ b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java @@ -716,14 +716,9 @@ public void testSearchForConfigurationsMultipleIds() { @Test public void testSearchForConfigurations() { Long poolId = 1L; - ListCfgsByCmd cmd = Mockito.mock(ListCfgsByCmd.class); - Mockito.when(cmd.getConfigName()).thenReturn("pool.storage.capacity.disablethreshold"); - Mockito.when(cmd.getStoragepoolId()).thenReturn(poolId); - Mockito.when(cmd.getZoneId()).thenReturn(null); - Mockito.when(cmd.getClusterId()).thenReturn(null); - Mockito.when(cmd.getAccountId()).thenReturn(null); - Mockito.when(cmd.getDomainId()).thenReturn(null); - Mockito.when(cmd.getImageStoreId()).thenReturn(null); + ListCfgsByCmd cmd = new ListCfgsByCmd(); + ReflectionTestUtils.setField(cmd, "storagePoolId", poolId); + ReflectionTestUtils.setField(cmd, "configName", "pool.storage.capacity.disablethreshold"); SearchCriteria sc = Mockito.mock(SearchCriteria.class); Mockito.when(configDao.createSearchCriteria()).thenReturn(sc); diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index c05b8fe27987..567b35e79679 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -257,6 +257,8 @@ 'deleteASNRange': 'AS Number Range', 'listASNumbers': 'AS Number', 'releaseASNumber': 'AS Number', + 'LogsWebSession': 'Logs Web Session', + 'LogsWebSessions': 'Logs Web Session', } diff --git a/tools/marvin/setup.py b/tools/marvin/setup.py index 11a63a96aced..803829736892 100644 --- a/tools/marvin/setup.py +++ b/tools/marvin/setup.py @@ -27,7 +27,7 @@ raise RuntimeError("python setuptools is required to build Marvin") -VERSION = "4.21.0.0-SNAPSHOT" +VERSION = "4.21.0.0" setup(name="Marvin", version=VERSION, diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index ebfe6bda1b2c..f3006f2fe822 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -519,6 +519,7 @@ "label.clear.list": "Clear list", "label.clear.notification": "Clear notification", "label.clientid": "Provider Client ID", +"label.clientaddress": "Client Address", "label.close": "Close", "label.cloud.managed": "CloudManaged", "label.cloudian.admin.password": "Admin Service Password", @@ -583,6 +584,7 @@ "label.confirmdeclineinvitation": "Are you sure you want to decline this project invitation?", "label.confirmpassword": "Confirm password", "label.confirmpassword.description": "Please type the same password again.", +"label.connected": "Connected Clients", "label.connected.agents": "Connected Agents", "label.connect": "Connect", "label.connectiontimeout": "Connection timeout", @@ -724,6 +726,7 @@ "label.delete.internal.lb": "Delete internal LB", "label.delete.ipv4.subnet": "Delete IPv4 subnet", "label.delete.ip.v6.prefix": "Delete IPv6 prefix", +"label.delete.logs.web.sessions": "Delete Logs Web Session", "label.delete.netscaler": "Delete NetScaler", "label.delete.niciranvp": "Remove Nvp controller", "label.delete.opendaylight.device": "Delete OpenDaylight controller", @@ -1001,6 +1004,7 @@ "label.shared.filesystems": "Shared FileSystems", "label.filesystem": "Filesystem", "label.filter": "Filter", +"label.filters": "Filters", "label.filter.annotations.all": "All comments", "label.filter.annotations.self": "Created by me", "label.filterby": "Filter by", @@ -1375,6 +1379,7 @@ "label.login.portal": "Portal login", "label.login.single.signon": "Single sign-on", "label.logout": "Logout", +"label.logs.web.sessions": "Logs Web Sessions", "label.lun": "LUN", "label.lun.number": "LUN #", "label.lxc": "LXC", @@ -2490,6 +2495,7 @@ "label.view": "View", "label.view.all": "View all", "label.view.console": "View console", +"label.view.logs": "View logs", "label.viewing": "Viewing", "label.virtualmachine": "Instance", "label.virtualmachinecount": "Instances Count", @@ -2997,6 +3003,7 @@ "message.delete.failed": "Delete fail", "message.delete.gateway": "Please confirm you want to delete the gateway.", "message.delete.ip.v6.prefix.processing": "Deleting IPv6 prefix...", +"message.delete.logs.web.session": "Deleting logs web session...", "message.delete.port.forward.processing": "Deleting port forwarding rule...", "message.delete.project": "Are you sure you want to delete this project?", "message.delete.rule.processing": "Deleting rule...", @@ -3475,6 +3482,7 @@ "message.setup.physical.network.during.zone.creation.basic": "When adding a basic zone, you can set up one physical Network, which corresponds to a NIC on the hypervisor. The Network carries several types of traffic.

You may also add other traffic types onto the physical Network.", "message.shared.network.offering.warning": "Domain admins and regular Users can only create shared Networks from Network offering with the setting specifyvlan=false. Please contact an administrator to create a Network offering if this list is empty.", "message.shared.network.unsupported.for.nsx": "Shared networks aren't supported for NSX enabled zones", +"message.showing.logs": "Showing logs for '%x'", "message.shutdown.triggered": "Shutdown has been triggered. This Management Server will not accept new jobs", "message.maintenance.initiated": "Maintenance has been initiated. This Management Server will not accept new jobs", "message.snapshot.additional.zones": "Snapshots will always be created in its native zone - %x, here you can select additional zone(s) where it will be copied to at creation time", diff --git a/ui/src/components/page/GlobalLayout.vue b/ui/src/components/page/GlobalLayout.vue index 2002ca3bfc85..74e1e63e66d6 100644 --- a/ui/src/components/page/GlobalLayout.vue +++ b/ui/src/components/page/GlobalLayout.vue @@ -69,6 +69,11 @@ + + +