From 0c192f36095fc60b16b479c09812a05ce32d1535 Mon Sep 17 00:00:00 2001 From: Marcus Sorensen Date: Fri, 4 Nov 2022 01:15:57 -0600 Subject: [PATCH 01/30] When VM start fails at host for admin, report error (#208) * When VM start fails at host for admin, report error Signed-off-by: Marcus Sorensen * Report ResourceUnavailableExceptions that result in InsufficientCapacityException to admin * Update error message to be more straightforward Signed-off-by: Marcus Sorensen Co-authored-by: Marcus Sorensen (cherry picked from commit 8885d252f07a2dc0d46b3d1d4e09d919e2599787) Signed-off-by: Rohit Yadav --- .../cloud/vm/VirtualMachineManagerImpl.java | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index cf188cbf58d..706fa9fc7d6 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -837,8 +837,15 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac public void start(final String vmUuid, final Map params, final DeploymentPlan planToDeploy, final DeploymentPlanner planner) { try { advanceStart(vmUuid, params, planToDeploy, planner); - } catch (ConcurrentOperationException | InsufficientCapacityException e) { - throw new CloudRuntimeException(String.format("Unable to start a VM [%s] due to [%s].", vmUuid, e.getMessage()), e).add(VirtualMachine.class, vmUuid); + } catch (final ConcurrentOperationException e) { + throw new CloudRuntimeException("Unable to start a VM due to concurrent operation", e).add(VirtualMachine.class, vmUuid); + } catch (final InsufficientCapacityException e) { + final CallContext cctxt = CallContext.current(); + final Account account = cctxt.getCallingAccount(); + if (account.getType() == Account.ACCOUNT_TYPE_ADMIN) { + throw new CloudRuntimeException("Unable to start a VM due to insufficient capacity: " + e.getMessage(), e).add(VirtualMachine.class, vmUuid); + } + throw new CloudRuntimeException("Unable to start a VM due to insufficient capacity", e).add(VirtualMachine.class, vmUuid); } catch (final ResourceUnavailableException e) { if (e.getScope() != null && e.getScope().equals(VirtualRouter.class)){ throw new CloudRuntimeException("Network is unavailable. Please contact administrator", e).add(VirtualMachine.class, vmUuid); @@ -1133,6 +1140,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac resourceCountIncrement(owner.getAccountId(),new Long(offering.getCpu()), new Long(offering.getRamSize())); } + String adminError = null; boolean canRetry = true; ExcludeList avoids = null; try { @@ -1222,6 +1230,10 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac reuseVolume = false; continue; } + if (account.getType() == Account.ACCOUNT_TYPE_ADMIN && adminError != null) { + String message = String.format("Unable to create a deployment for %s. Previous error: %s", vmProfile, adminError); + throw new InsufficientServerCapacityException(message, DataCenter.class, plan.getDataCenterId(), areAffinityGroupsAssociated(vmProfile)); + } throw new InsufficientServerCapacityException("Unable to create a deployment for " + vmProfile, DataCenter.class, plan.getDataCenterId(), areAffinityGroupsAssociated(vmProfile)); } @@ -1386,7 +1398,9 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac throw new ExecutionException("Unable to start VM:"+vm.getUuid()+" due to error in finalizeStart, not retrying"); } } - s_logger.info("Unable to start VM on " + dest.getHost() + " due to " + (startAnswer == null ? " no start answer" : startAnswer.getDetails())); + adminError = startAnswer == null ? " no start answer" : startAnswer.getDetails(); + s_logger.info("Unable to start VM on " + dest.getHost() + " due to " + adminError); + if (startAnswer != null && startAnswer.getContextParam("stopRetry") != null) { break; } @@ -1399,7 +1413,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac canRetry = false; throw new AgentUnavailableException("Unable to start " + vm.getHostName(), destHostId, e); } catch (final ResourceUnavailableException e) { - s_logger.warn("Unable to contact resource.", e); + s_logger.info("Unable to contact resource.", e); + adminError = e.getMessage(); if (!avoids.add(e)) { if (e.getScope() == Volume.class || e.getScope() == Nic.class) { throw e; @@ -1455,6 +1470,9 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } if (startedVm == null) { + if (account.getType() == Account.ACCOUNT_TYPE_ADMIN && adminError != null) { + throw new CloudRuntimeException("Unable to start instance '" + vm.getHostName() + "' (" + vm.getUuid() + "): " + adminError); + } throw new CloudRuntimeException("Unable to start instance '" + vm.getHostName() + "' (" + vm.getUuid() + "), see management server log for details"); } } From c8f02a8b34f28c9fb4c98ddc981f29fa159deb54 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Fri, 15 Sep 2023 13:15:34 +0530 Subject: [PATCH 02/30] engine/orchestartion: fix/refactor to Account.Type.ADMIN Signed-off-by: Rohit Yadav --- .../main/java/com/cloud/vm/VirtualMachineManagerImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index 706fa9fc7d6..300cef3ac58 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -842,7 +842,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } catch (final InsufficientCapacityException e) { final CallContext cctxt = CallContext.current(); final Account account = cctxt.getCallingAccount(); - if (account.getType() == Account.ACCOUNT_TYPE_ADMIN) { + if (account.getType() == Account.Type.ADMIN) { throw new CloudRuntimeException("Unable to start a VM due to insufficient capacity: " + e.getMessage(), e).add(VirtualMachine.class, vmUuid); } throw new CloudRuntimeException("Unable to start a VM due to insufficient capacity", e).add(VirtualMachine.class, vmUuid); @@ -1230,7 +1230,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac reuseVolume = false; continue; } - if (account.getType() == Account.ACCOUNT_TYPE_ADMIN && adminError != null) { + if (account.getType() == Account.Type.ADMIN && adminError != null) { String message = String.format("Unable to create a deployment for %s. Previous error: %s", vmProfile, adminError); throw new InsufficientServerCapacityException(message, DataCenter.class, plan.getDataCenterId(), areAffinityGroupsAssociated(vmProfile)); } @@ -1470,7 +1470,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } if (startedVm == null) { - if (account.getType() == Account.ACCOUNT_TYPE_ADMIN && adminError != null) { + if (account.getType() == Account.Type.ADMIN && adminError != null) { throw new CloudRuntimeException("Unable to start instance '" + vm.getHostName() + "' (" + vm.getUuid() + "): " + adminError); } throw new CloudRuntimeException("Unable to start instance '" + vm.getHostName() + "' (" + vm.getUuid() + "), see management server log for details"); From 8597758ed8da528c9aaf260d17d78eaf075c1c55 Mon Sep 17 00:00:00 2001 From: Marcus Sorensen Date: Wed, 22 Mar 2023 14:38:43 -0600 Subject: [PATCH 03/30] Support Jetty's live cert reload on HTTPS frontend (#7355) * Support Jetty's live cert reload Signed-off-by: Marcus Sorensen * Update ServerDaemon.java --------- Signed-off-by: Marcus Sorensen Co-authored-by: Marcus Sorensen (cherry picked from commit 9ca5f287eb4ad6c136fa49a8f752ff0a535bf088) Signed-off-by: Rohit Yadav --- .../main/java/org/apache/cloudstack/ServerDaemon.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/src/main/java/org/apache/cloudstack/ServerDaemon.java b/client/src/main/java/org/apache/cloudstack/ServerDaemon.java index 08f856655dc..63cdc45b8dc 100644 --- a/client/src/main/java/org/apache/cloudstack/ServerDaemon.java +++ b/client/src/main/java/org/apache/cloudstack/ServerDaemon.java @@ -45,6 +45,7 @@ import org.eclipse.jetty.server.handler.MovedContextHandler; import org.eclipse.jetty.server.handler.RequestLogHandler; import org.eclipse.jetty.server.handler.gzip.GzipHandler; import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.util.ssl.KeyStoreScanner; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; @@ -241,6 +242,14 @@ public class ServerDaemon implements Daemon { sslConnector.setPort(httpsPort); sslConnector.setHost(bindInterface); server.addConnector(sslConnector); + + // add scanner to auto-reload certs + try { + KeyStoreScanner scanner = new KeyStoreScanner(sslContextFactory); + server.addBean(scanner); + } catch (Exception ex) { + LOG.error("failed to set up keystore scanner, manual refresh of certificates will be required", ex); + } } } From 20952b48424a4332a7b403e895930bd9a7f6260f Mon Sep 17 00:00:00 2001 From: Nicolas Vazquez Date: Tue, 4 Apr 2023 08:33:37 -0300 Subject: [PATCH 04/30] Auto Enable/Disable KVM hosts (#7170) * Auto Enable Disable KVM hosts * Improve health check result * Fix corner cases * Script path refactor * Fix sonar cloud reports * Fix last code smells * Add marvin tests * Fix new line on agent.properties to prevent host add failures * Send alert on auto-enable-disable and add annotations when the setting is enabled * Address reviews * Add a reason for enabling or disabling a host when the automatic feature is enabled * Fix comment on the marvin test description * Fix for disabling the feature if the admin has manually updated the host resource state before any health check result (cherry picked from commit be66eb2a35bd6a5aae74f97a9140a9ec01f2a838) Signed-off-by: Rohit Yadav --- agent/conf/agent.properties | 4 + .../agent/properties/AgentProperties.java | 3 + .../com/cloud/resource/ResourceService.java | 2 + .../apache/cloudstack/api/ApiConstants.java | 1 + .../api/command/admin/host/UpdateHostCmd.java | 4 - .../cloud/agent/api/PingRoutingCommand.java | 9 + .../agent/api/StartupRoutingCommand.java | 9 + .../java/com/cloud/agent/AgentManager.java | 7 + .../cloud/agent/manager/AgentManagerImpl.java | 53 +++- .../resource/LibvirtComputingResource.java | 62 ++++- .../cloud/resource/ResourceManagerImpl.java | 222 +++++++++++++---- .../resource/MockResourceManagerImpl.java | 5 + .../smoke/test_host_control_state.py | 226 +++++++++++++++++- ui/src/config/section/infra/hosts.js | 10 +- ui/src/views/infra/HostEnableDisable.vue | 133 +++++++++++ 15 files changed, 688 insertions(+), 62 deletions(-) create mode 100644 ui/src/views/infra/HostEnableDisable.vue diff --git a/agent/conf/agent.properties b/agent/conf/agent.properties index 27c0e387fa3..9174da7fd7b 100644 --- a/agent/conf/agent.properties +++ b/agent/conf/agent.properties @@ -398,3 +398,7 @@ iscsi.session.cleanup.enabled=false # The number of iothreads. There should be only 1 or 2 IOThreads per VM CPU (default is 1). The recommended number of iothreads is 1 # iothreads=1 + +# The path of an executable file/script for host health check for CloudStack to Auto Disable/Enable the host +# depending on the return value of the file/script +# agent.health.check.script.path= diff --git a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java index 5c7f4ed4b23..75248fb01bf 100644 --- a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java +++ b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java @@ -312,6 +312,9 @@ public class AgentProperties{ */ public static final Property OPENVSWITCH_DPDK_OVS_PATH = new Property<>("openvswitch.dpdk.ovs.path", null, String.class); + public static final Property HEALTH_CHECK_SCRIPT_PATH = + new Property<>("agent.health.check.script.path", null, String.class); + /** * Sets the hypervisor type.
* Possible values: kvm | lxc
diff --git a/api/src/main/java/com/cloud/resource/ResourceService.java b/api/src/main/java/com/cloud/resource/ResourceService.java index e2b84ba8720..2757c918ed6 100644 --- a/api/src/main/java/com/cloud/resource/ResourceService.java +++ b/api/src/main/java/com/cloud/resource/ResourceService.java @@ -49,6 +49,8 @@ public interface ResourceService { */ Host updateHost(UpdateHostCmd cmd) throws NoTransitionException; + Host autoUpdateHostAllocationState(Long hostId, ResourceState.Event resourceEvent) throws NoTransitionException; + Host cancelMaintenance(CancelMaintenanceCmd cmd); Host reconnectHost(ReconnectHostCmd cmd) throws AgentUnavailableException; 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 9f0418a8d9c..5b7b4f674ca 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1020,6 +1020,7 @@ public class ApiConstants { public static final String PUBLIC_MTU = "publicmtu"; public static final String PRIVATE_MTU = "privatemtu"; public static final String MTU = "mtu"; + public static final String AUTO_ENABLE_KVM_HOST = "autoenablekvmhost"; public static final String LIST_APIS = "listApis"; /** diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java index 5ca53c07740..e3ff130e2d4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java @@ -19,7 +19,6 @@ package org.apache.cloudstack.api.command.admin.host; import com.cloud.host.Host; import com.cloud.user.Account; import org.apache.cloudstack.acl.RoleType; -import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -117,9 +116,6 @@ public class UpdateHostCmd extends BaseCmd { Host result; try { result = _resourceService.updateHost(this); - if(getAnnotation() != null) { - annotationService.addAnnotation(getAnnotation(), AnnotationService.EntityType.HOST, result.getUuid(), true); - } HostResponse hostResponse = _responseGenerator.createHostResponse(result); hostResponse.setResponseName(getCommandName()); this.setResponseObject(hostResponse); diff --git a/core/src/main/java/com/cloud/agent/api/PingRoutingCommand.java b/core/src/main/java/com/cloud/agent/api/PingRoutingCommand.java index d7733ee9197..ce529ad4bcb 100644 --- a/core/src/main/java/com/cloud/agent/api/PingRoutingCommand.java +++ b/core/src/main/java/com/cloud/agent/api/PingRoutingCommand.java @@ -29,6 +29,7 @@ public class PingRoutingCommand extends PingCommand { boolean _gatewayAccessible = true; boolean _vnetAccessible = true; + private Boolean hostHealthCheckResult; protected PingRoutingCommand() { } @@ -57,4 +58,12 @@ public class PingRoutingCommand extends PingCommand { public void setVnetAccessible(boolean vnetAccessible) { _vnetAccessible = vnetAccessible; } + + public Boolean getHostHealthCheckResult() { + return hostHealthCheckResult; + } + + public void setHostHealthCheckResult(Boolean hostHealthCheckResult) { + this.hostHealthCheckResult = hostHealthCheckResult; + } } diff --git a/core/src/main/java/com/cloud/agent/api/StartupRoutingCommand.java b/core/src/main/java/com/cloud/agent/api/StartupRoutingCommand.java index b459f884969..b4f9d20df5e 100644 --- a/core/src/main/java/com/cloud/agent/api/StartupRoutingCommand.java +++ b/core/src/main/java/com/cloud/agent/api/StartupRoutingCommand.java @@ -44,6 +44,7 @@ public class StartupRoutingCommand extends StartupCommand { List hostTags = new ArrayList(); String hypervisorVersion; HashMap> groupDetails = new HashMap>(); + private Boolean hostHealthCheckResult; public StartupRoutingCommand() { super(Host.Type.Routing); @@ -188,4 +189,12 @@ public class StartupRoutingCommand extends StartupCommand { public void setSupportsClonedVolumes(boolean supportsClonedVolumes) { this.supportsClonedVolumes = supportsClonedVolumes; } + + public Boolean getHostHealthCheckResult() { + return hostHealthCheckResult; + } + + public void setHostHealthCheckResult(Boolean hostHealthCheckResult) { + this.hostHealthCheckResult = hostHealthCheckResult; + } } diff --git a/engine/components-api/src/main/java/com/cloud/agent/AgentManager.java b/engine/components-api/src/main/java/com/cloud/agent/AgentManager.java index 818e0a75e64..6ba0c3b4fa0 100644 --- a/engine/components-api/src/main/java/com/cloud/agent/AgentManager.java +++ b/engine/components-api/src/main/java/com/cloud/agent/AgentManager.java @@ -39,6 +39,13 @@ import com.cloud.resource.ServerResource; public interface AgentManager { static final ConfigKey Wait = new ConfigKey("Advanced", Integer.class, "wait", "1800", "Time in seconds to wait for control commands to return", true); + ConfigKey EnableKVMAutoEnableDisable = new ConfigKey<>(Boolean.class, + "enable.kvm.host.auto.enable.disable", + "Advanced", + "false", + "(KVM only) Enable Auto Disable/Enable KVM hosts in the cluster " + + "according to the hosts health check results", + true, ConfigKey.Scope.Cluster, null); public enum TapAgentsAction { Add, Del, Contains, diff --git a/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java index b74c11cf138..abdee769c1a 100644 --- a/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java @@ -51,6 +51,7 @@ import org.apache.cloudstack.framework.jobs.AsyncJobExecutionContext; import org.apache.cloudstack.managed.context.ManagedContextRunnable; import org.apache.cloudstack.outofbandmanagement.dao.OutOfBandManagementDao; import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.commons.lang3.BooleanUtils; import org.apache.log4j.Logger; import org.apache.log4j.MDC; @@ -1250,6 +1251,52 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl super(type, link, data); } + private void processHostHealthCheckResult(Boolean hostHealthCheckResult, long hostId) { + if (hostHealthCheckResult == null) { + return; + } + HostVO host = _hostDao.findById(hostId); + if (host == null) { + s_logger.error(String.format("Unable to find host with ID: %s", hostId)); + return; + } + if (!BooleanUtils.toBoolean(EnableKVMAutoEnableDisable.valueIn(host.getClusterId()))) { + s_logger.debug(String.format("%s is disabled for the cluster %s, cannot process the health check result " + + "received for the host %s", EnableKVMAutoEnableDisable.key(), host.getClusterId(), host.getName())); + return; + } + + ResourceState.Event resourceEvent = hostHealthCheckResult ? ResourceState.Event.Enable : ResourceState.Event.Disable; + + try { + s_logger.info(String.format("Host health check %s, auto %s KVM host: %s", + hostHealthCheckResult ? "succeeds" : "fails", + hostHealthCheckResult ? "enabling" : "disabling", + host.getName())); + _resourceMgr.autoUpdateHostAllocationState(hostId, resourceEvent); + } catch (NoTransitionException e) { + s_logger.error(String.format("Cannot Auto %s host: %s", resourceEvent, host.getName()), e); + } + } + + private void processStartupRoutingCommand(StartupRoutingCommand startup, long hostId) { + if (startup == null) { + s_logger.error("Empty StartupRoutingCommand received"); + return; + } + Boolean hostHealthCheckResult = startup.getHostHealthCheckResult(); + processHostHealthCheckResult(hostHealthCheckResult, hostId); + } + + private void processPingRoutingCommand(PingRoutingCommand pingRoutingCommand, long hostId) { + if (pingRoutingCommand == null) { + s_logger.error("Empty PingRoutingCommand received"); + return; + } + Boolean hostHealthCheckResult = pingRoutingCommand.getHostHealthCheckResult(); + processHostHealthCheckResult(hostHealthCheckResult, hostId); + } + protected void processRequest(final Link link, final Request request) { final AgentAttache attache = (AgentAttache)link.attachment(); final Command[] cmds = request.getCommands(); @@ -1291,6 +1338,7 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl try { if (cmd instanceof StartupRoutingCommand) { final StartupRoutingCommand startup = (StartupRoutingCommand) cmd; + processStartupRoutingCommand(startup, hostId); answer = new StartupAnswer(startup, attache.getId(), mgmtServiceConf.getPingInterval()); } else if (cmd instanceof StartupProxyCommand) { final StartupProxyCommand startup = (StartupProxyCommand) cmd; @@ -1322,6 +1370,7 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl // if the router is sending a ping, verify the // gateway was pingable if (cmd instanceof PingRoutingCommand) { + processPingRoutingCommand((PingRoutingCommand) cmd, hostId); final boolean gatewayAccessible = ((PingRoutingCommand)cmd).isGatewayAccessible(); final HostVO host = _hostDao.findById(Long.valueOf(cmdHostId)); @@ -1748,8 +1797,8 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl @Override public ConfigKey[] getConfigKeys() { - return new ConfigKey[] { CheckTxnBeforeSending, Workers, Port, Wait, AlertWait, DirectAgentLoadSize, DirectAgentPoolSize, - DirectAgentThreadCap }; + return new ConfigKey[] { CheckTxnBeforeSending, Workers, Port, Wait, AlertWait, DirectAgentLoadSize, + DirectAgentPoolSize, DirectAgentThreadCap, EnableKVMAutoEnableDisable }; } protected class SetHostParamsListener implements Listener { diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index 265e786e8f8..002dca7f0a8 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -322,6 +322,7 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv private String _dcId; private String _clusterId; private final Properties _uefiProperties = new Properties(); + private String hostHealthCheckScriptPath; private long _hvVersion; private Duration _timeout; @@ -717,6 +718,10 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv NATIVE, OPENVSWITCH, TUNGSTEN } + protected enum HealthCheckResult { + SUCCESS, FAILURE, IGNORE + } + protected BridgeType _bridgeType; protected StorageSubsystemCommandHandler storageHandler; @@ -943,6 +948,12 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv throw new ConfigurationException("Unable to find the ovs-pvlan-kvm-vm.sh"); } + hostHealthCheckScriptPath = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HEALTH_CHECK_SCRIPT_PATH); + if (StringUtils.isNotBlank(hostHealthCheckScriptPath) && !new File(hostHealthCheckScriptPath).exists()) { + s_logger.info(String.format("Unable to find the host health check script at: %s, " + + "discarding it", hostHealthCheckScriptPath)); + } + setupTungstenVrouterPath = Script.findScript(tungstenScriptsDir, "setup_tungsten_vrouter.sh"); if (setupTungstenVrouterPath == null) { throw new ConfigurationException("Unable to find the setup_tungsten_vrouter.sh"); @@ -3442,13 +3453,54 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv @Override public PingCommand getCurrentStatus(final long id) { - + PingRoutingCommand pingRoutingCommand; if (!_canBridgeFirewall) { - return new PingRoutingCommand(com.cloud.host.Host.Type.Routing, id, this.getHostVmStateReport()); + pingRoutingCommand = new PingRoutingCommand(com.cloud.host.Host.Type.Routing, id, this.getHostVmStateReport()); } else { final HashMap> nwGrpStates = syncNetworkGroups(id); - return new PingRoutingWithNwGroupsCommand(getType(), id, this.getHostVmStateReport(), nwGrpStates); + pingRoutingCommand = new PingRoutingWithNwGroupsCommand(getType(), id, this.getHostVmStateReport(), nwGrpStates); } + HealthCheckResult healthCheckResult = getHostHealthCheckResult(); + if (healthCheckResult != HealthCheckResult.IGNORE) { + pingRoutingCommand.setHostHealthCheckResult(healthCheckResult == HealthCheckResult.SUCCESS); + } + return pingRoutingCommand; + } + + /** + * The health check result is true, if the script is executed successfully and the exit code is 0 + * The health check result is false, if the script is executed successfully and the exit code is 1 + * The health check result is null, if + * - Script file is not specified, or + * - Script file does not exist, or + * - Script file is not accessible by the user of the cloudstack-agent process, or + * - Script file is not executable + * - There are errors when the script is executed (exit codes other than 0 or 1) + */ + private HealthCheckResult getHostHealthCheckResult() { + if (StringUtils.isBlank(hostHealthCheckScriptPath)) { + s_logger.debug("Host health check script path is not specified"); + return HealthCheckResult.IGNORE; + } + File script = new File(hostHealthCheckScriptPath); + if (!script.exists() || !script.isFile() || !script.canExecute()) { + s_logger.warn(String.format("The host health check script file set at: %s cannot be executed, " + + "reason: %s", hostHealthCheckScriptPath, + !script.exists() ? "file does not exist" : "please check file permissions to execute this file")); + return HealthCheckResult.IGNORE; + } + int exitCode = executeBashScriptAndRetrieveExitValue(hostHealthCheckScriptPath); + if (s_logger.isDebugEnabled()) { + s_logger.debug(String.format("Host health check script exit code: %s", exitCode)); + } + return retrieveHealthCheckResultFromExitCode(exitCode); + } + + private HealthCheckResult retrieveHealthCheckResultFromExitCode(int exitCode) { + if (exitCode != 0 && exitCode != 1) { + return HealthCheckResult.IGNORE; + } + return exitCode == 0 ? HealthCheckResult.SUCCESS : HealthCheckResult.FAILURE; } @Override @@ -3490,6 +3542,10 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv cmd.setGatewayIpAddress(_localGateway); cmd.setIqn(getIqn()); cmd.getHostDetails().put(HOST_VOLUME_ENCRYPTION, String.valueOf(hostSupportsVolumeEncryption())); + HealthCheckResult healthCheckResult = getHostHealthCheckResult(); + if (healthCheckResult != HealthCheckResult.IGNORE) { + cmd.setHostHealthCheckResult(healthCheckResult == HealthCheckResult.SUCCESS); + } if (cmd.getHostDetails().containsKey("Host.OS")) { _hostDistro = cmd.getHostDetails().get("Host.OS"); diff --git a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java index 1909063dfe3..09bd1480b11 100755 --- a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java +++ b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java @@ -36,11 +36,13 @@ import java.util.Random; import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.alert.AlertManager; import com.cloud.exception.StorageConflictException; import com.cloud.exception.StorageUnavailableException; import com.cloud.storage.Volume; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.VolumeDao; +import org.apache.cloudstack.alert.AlertService; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.ApiConstants; @@ -299,6 +301,10 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, private AnnotationDao annotationDao; @Inject private VolumeDao volumeDao; + @Inject + private AlertManager alertManager; + @Inject + private AnnotationService annotationService; private final long _nodeId = ManagementServerNode.getManagementServerId(); @@ -1801,73 +1807,149 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, return hostInMaintenance; } + private ResourceState.Event getResourceEventFromAllocationStateString(String allocationState) { + final ResourceState.Event resourceEvent = ResourceState.Event.toEvent(allocationState); + if (resourceEvent != ResourceState.Event.Enable && resourceEvent != ResourceState.Event.Disable) { + throw new InvalidParameterValueException(String.format("Invalid allocation state: %s, " + + "only Enable/Disable are allowed", allocationState)); + } + return resourceEvent; + } + + private void handleAutoEnableDisableKVMHost(boolean autoEnableDisableKVMSetting, + boolean isUpdateFromHostHealthCheck, + HostVO host, DetailVO hostDetail, + ResourceState.Event resourceEvent) { + if (autoEnableDisableKVMSetting) { + if (!isUpdateFromHostHealthCheck && hostDetail != null && + !Boolean.parseBoolean(hostDetail.getValue()) && resourceEvent == ResourceState.Event.Enable) { + hostDetail.setValue(Boolean.TRUE.toString()); + _hostDetailsDao.update(hostDetail.getId(), hostDetail); + } else if (!isUpdateFromHostHealthCheck && hostDetail != null && + Boolean.parseBoolean(hostDetail.getValue()) && resourceEvent == ResourceState.Event.Disable) { + s_logger.info(String.format("The setting %s is enabled but the host %s is manually set into %s state," + + "ignoring future auto enabling of the host based on health check results", + AgentManager.EnableKVMAutoEnableDisable.key(), host.getName(), resourceEvent)); + hostDetail.setValue(Boolean.FALSE.toString()); + _hostDetailsDao.update(hostDetail.getId(), hostDetail); + } else if (hostDetail == null) { + String autoEnableValue = !isUpdateFromHostHealthCheck ? Boolean.FALSE.toString() : Boolean.TRUE.toString(); + hostDetail = new DetailVO(host.getId(), ApiConstants.AUTO_ENABLE_KVM_HOST, autoEnableValue); + _hostDetailsDao.persist(hostDetail); + } + } + } + private boolean updateHostAllocationState(HostVO host, String allocationState, + boolean isUpdateFromHostHealthCheck) throws NoTransitionException { + boolean autoEnableDisableKVMSetting = AgentManager.EnableKVMAutoEnableDisable.valueIn(host.getClusterId()) && + host.getHypervisorType() == HypervisorType.KVM; + ResourceState.Event resourceEvent = getResourceEventFromAllocationStateString(allocationState); + DetailVO hostDetail = _hostDetailsDao.findDetail(host.getId(), ApiConstants.AUTO_ENABLE_KVM_HOST); + + if ((host.getResourceState() == ResourceState.Enabled && resourceEvent == ResourceState.Event.Enable) || + (host.getResourceState() == ResourceState.Disabled && resourceEvent == ResourceState.Event.Disable)) { + s_logger.info(String.format("The host %s is already on the allocated state", host.getName())); + return false; + } + + if (isAutoEnableAttemptForADisabledHost(autoEnableDisableKVMSetting, isUpdateFromHostHealthCheck, hostDetail, resourceEvent)) { + s_logger.debug(String.format("The setting '%s' is enabled and the health check succeeds on the host, " + + "but the host has been manually disabled previously, ignoring auto enabling", + AgentManager.EnableKVMAutoEnableDisable.key())); + return false; + } + + handleAutoEnableDisableKVMHost(autoEnableDisableKVMSetting, isUpdateFromHostHealthCheck, host, + hostDetail, resourceEvent); + + resourceStateTransitTo(host, resourceEvent, _nodeId); + return true; + } + + private boolean isAutoEnableAttemptForADisabledHost(boolean autoEnableDisableKVMSetting, + boolean isUpdateFromHostHealthCheck, + DetailVO hostDetail, ResourceState.Event resourceEvent) { + return autoEnableDisableKVMSetting && isUpdateFromHostHealthCheck && hostDetail != null && + !Boolean.parseBoolean(hostDetail.getValue()) && resourceEvent == ResourceState.Event.Enable; + } + + private void updateHostName(HostVO host, String name) { + s_logger.debug("Updating Host name to: " + name); + host.setName(name); + _hostDao.update(host.getId(), host); + } + + private void updateHostGuestOSCategory(Long hostId, Long guestOSCategoryId) { + // Verify that the guest OS Category exists + if (!(guestOSCategoryId > 0) || _guestOSCategoryDao.findById(guestOSCategoryId) == null) { + throw new InvalidParameterValueException("Please specify a valid guest OS category."); + } + + final GuestOSCategoryVO guestOSCategory = _guestOSCategoryDao.findById(guestOSCategoryId); + final DetailVO guestOSDetail = _hostDetailsDao.findDetail(hostId, "guest.os.category.id"); + + if (guestOSCategory != null && !GuestOSCategoryVO.CATEGORY_NONE.equalsIgnoreCase(guestOSCategory.getName())) { + // Create/Update an entry for guest.os.category.id + if (guestOSDetail != null) { + guestOSDetail.setValue(String.valueOf(guestOSCategory.getId())); + _hostDetailsDao.update(guestOSDetail.getId(), guestOSDetail); + } else { + final Map detail = new HashMap(); + detail.put("guest.os.category.id", String.valueOf(guestOSCategory.getId())); + _hostDetailsDao.persist(hostId, detail); + } + } else { + // Delete any existing entry for guest.os.category.id + if (guestOSDetail != null) { + _hostDetailsDao.remove(guestOSDetail.getId()); + } + } + } + + private void updateHostTags(HostVO host, Long hostId, List hostTags) { + List activeVMs = _vmDao.listByHostId(hostId); + s_logger.warn(String.format("The following active VMs [%s] are using the host [%s]. " + + "Updating the host tags will not affect them.", activeVMs, host)); + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Updating Host Tags to :" + hostTags); + } + _hostTagsDao.persist(hostId, new ArrayList<>(new HashSet<>(hostTags))); + } + @Override public Host updateHost(final UpdateHostCmd cmd) throws NoTransitionException { - Long hostId = cmd.getId(); - String name = cmd.getName(); - Long guestOSCategoryId = cmd.getOsCategoryId(); + return updateHost(cmd.getId(), cmd.getName(), cmd.getOsCategoryId(), + cmd.getAllocationState(), cmd.getUrl(), cmd.getHostTags(), cmd.getAnnotation(), false); + } + private Host updateHost(Long hostId, String name, Long guestOSCategoryId, String allocationState, + String url, List hostTags, String annotation, boolean isUpdateFromHostHealthCheck) throws NoTransitionException { // Verify that the host exists final HostVO host = _hostDao.findById(hostId); if (host == null) { throw new InvalidParameterValueException("Host with id " + hostId + " doesn't exist"); } - if (cmd.getAllocationState() != null) { - final ResourceState.Event resourceEvent = ResourceState.Event.toEvent(cmd.getAllocationState()); - if (resourceEvent != ResourceState.Event.Enable && resourceEvent != ResourceState.Event.Disable) { - throw new CloudRuntimeException("Invalid allocation state:" + cmd.getAllocationState() + ", only Enable/Disable are allowed"); - } - - resourceStateTransitTo(host, resourceEvent, _nodeId); + boolean isUpdateHostAllocation = false; + if (StringUtils.isNotBlank(allocationState)) { + isUpdateHostAllocation = updateHostAllocationState(host, allocationState, isUpdateFromHostHealthCheck); } if (StringUtils.isNotBlank(name)) { - s_logger.debug("Updating Host name to: " + name); - host.setName(name); - _hostDao.update(host.getId(), host); + updateHostName(host, name); } if (guestOSCategoryId != null) { - // Verify that the guest OS Category exists - if (!(guestOSCategoryId > 0) || _guestOSCategoryDao.findById(guestOSCategoryId) == null) { - throw new InvalidParameterValueException("Please specify a valid guest OS category."); - } - - final GuestOSCategoryVO guestOSCategory = _guestOSCategoryDao.findById(guestOSCategoryId); - final DetailVO guestOSDetail = _hostDetailsDao.findDetail(hostId, "guest.os.category.id"); - - if (guestOSCategory != null && !GuestOSCategoryVO.CATEGORY_NONE.equalsIgnoreCase(guestOSCategory.getName())) { - // Create/Update an entry for guest.os.category.id - if (guestOSDetail != null) { - guestOSDetail.setValue(String.valueOf(guestOSCategory.getId())); - _hostDetailsDao.update(guestOSDetail.getId(), guestOSDetail); - } else { - final Map detail = new HashMap(); - detail.put("guest.os.category.id", String.valueOf(guestOSCategory.getId())); - _hostDetailsDao.persist(hostId, detail); - } - } else { - // Delete any existing entry for guest.os.category.id - if (guestOSDetail != null) { - _hostDetailsDao.remove(guestOSDetail.getId()); - } - } + updateHostGuestOSCategory(hostId, guestOSCategoryId); } - final List hostTags = cmd.getHostTags(); + if (hostTags != null) { - List activeVMs = _vmDao.listByHostId(hostId); - s_logger.warn(String.format("The following active VMs [%s] are using the host [%s]. Updating the host tags will not affect them.", activeVMs, host)); - - if (s_logger.isDebugEnabled()) { - s_logger.debug("Updating Host Tags to :" + hostTags); - } - _hostTagsDao.persist(hostId, new ArrayList(new HashSet(hostTags))); + updateHostTags(host, hostId, hostTags); } - final String url = cmd.getUrl(); if (url != null) { - _storageMgr.updateSecondaryStorage(cmd.getId(), cmd.getUrl()); + _storageMgr.updateSecondaryStorage(hostId, url); } try { _storageMgr.enableHost(hostId); @@ -1876,9 +1958,55 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, } final HostVO updatedHost = _hostDao.findById(hostId); + + sendAlertAndAnnotationForAutoEnableDisableKVMHostFeature(host, allocationState, + isUpdateFromHostHealthCheck, isUpdateHostAllocation, annotation); + return updatedHost; } + private void sendAlertAndAnnotationForAutoEnableDisableKVMHostFeature(HostVO host, String allocationState, + boolean isUpdateFromHostHealthCheck, + boolean isUpdateHostAllocation, String annotation) { + boolean isAutoEnableDisableKVMSettingEnabled = host.getHypervisorType() == HypervisorType.KVM && + AgentManager.EnableKVMAutoEnableDisable.valueIn(host.getClusterId()); + if (!isAutoEnableDisableKVMSettingEnabled) { + if (StringUtils.isNotBlank(annotation)) { + annotationService.addAnnotation(annotation, AnnotationService.EntityType.HOST, host.getUuid(), true); + } + return; + } + + if (!isUpdateHostAllocation) { + return; + } + + String msg = String.format("The host %s (%s) ", host.getName(), host.getUuid()); + ResourceState.Event resourceEvent = getResourceEventFromAllocationStateString(allocationState); + boolean isEventEnable = resourceEvent == ResourceState.Event.Enable; + + if (isUpdateFromHostHealthCheck) { + msg += String.format("is auto-%s after %s health check results", + isEventEnable ? "enabled" : "disabled", + isEventEnable ? "successful" : "failed"); + alertManager.sendAlert(AlertService.AlertType.ALERT_TYPE_HOST, host.getDataCenterId(), + host.getPodId(), msg, msg); + } else { + msg += String.format("is %s despite the setting '%s' is enabled for the cluster %s", + isEventEnable ? "enabled" : "disabled", AgentManager.EnableKVMAutoEnableDisable.key(), + host.getClusterId()); + if (StringUtils.isNotBlank(annotation)) { + msg += String.format(", reason: %s", annotation); + } + } + annotationService.addAnnotation(msg, AnnotationService.EntityType.HOST, host.getUuid(), true); + } + + @Override + public Host autoUpdateHostAllocationState(Long hostId, ResourceState.Event resourceEvent) throws NoTransitionException { + return updateHost(hostId, null, null, resourceEvent.toString(), null, null, null, true); + } + @Override public Cluster getCluster(final Long clusterId) { return _clusterDao.findById(clusterId); diff --git a/server/src/test/java/com/cloud/resource/MockResourceManagerImpl.java b/server/src/test/java/com/cloud/resource/MockResourceManagerImpl.java index 4d5b5ba584b..73d4adf050b 100755 --- a/server/src/test/java/com/cloud/resource/MockResourceManagerImpl.java +++ b/server/src/test/java/com/cloud/resource/MockResourceManagerImpl.java @@ -73,6 +73,11 @@ public class MockResourceManagerImpl extends ManagerBase implements ResourceMana return null; } + @Override + public Host autoUpdateHostAllocationState(Long hostId, ResourceState.Event resourceEvent) throws NoTransitionException { + return null; + } + /* (non-Javadoc) * @see com.cloud.resource.ResourceService#cancelMaintenance(com.cloud.api.commands.CancelMaintenanceCmd) */ diff --git a/test/integration/smoke/test_host_control_state.py b/test/integration/smoke/test_host_control_state.py index 809af7d2a0e..4b8409ecc27 100644 --- a/test/integration/smoke/test_host_control_state.py +++ b/test/integration/smoke/test_host_control_state.py @@ -20,7 +20,7 @@ Tests for host control state """ -from marvin.cloudstackAPI import updateHost +from marvin.cloudstackAPI import (updateHost, updateConfiguration) from nose.plugins.attrib import attr from marvin.cloudstackTestCase import cloudstackTestCase from marvin.lib.common import (get_domain, @@ -28,13 +28,18 @@ from marvin.lib.common import (get_domain, get_template, list_hosts, list_routers, - list_ssvms) + list_ssvms, + list_clusters, + list_hosts) from marvin.lib.base import (Account, Domain, Host, ServiceOffering, VirtualMachine) from marvin.sshClient import SshClient +from marvin.lib.decoratorGenerators import skipTestIf +from marvin.lib.utils import wait_until +import logging import time @@ -250,3 +255,220 @@ class TestHostControlState(cloudstackTestCase): self.enable_host(host_id) self.verify_router_host_control_state(router.id, "Enabled") + + +class TestAutoEnableDisableHost(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + cls.testClient = super(TestAutoEnableDisableHost, cls).getClsTestClient() + cls.apiclient = cls.testClient.getApiClient() + cls.services = cls.testClient.getParsedTestDataConfig() + # Get Zone, Domain and templates + cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) + cls.hypervisor = cls.testClient.getHypervisorInfo() + cls.hostConfig = cls.config.__dict__["zones"][0].__dict__["pods"][0].__dict__["clusters"][0].__dict__["hosts"][0].__dict__ + if cls.hypervisor.lower() not in ['kvm']: + cls.hypervisorNotSupported = True + return + + cls.logger = logging.getLogger('TestAutoEnableDisableHost') + return + + @classmethod + def tearDownClass(cls): + super(TestAutoEnableDisableHost, cls).tearDownClass() + + def tearDown(self): + super(TestAutoEnableDisableHost, self).tearDown() + + def get_ssh_client(self, ip, username, password, retries=10): + """ Setup ssh client connection and return connection """ + try: + ssh_client = SshClient(ip, 22, username, password, retries) + except Exception as e: + raise unittest.SkipTest("Unable to create ssh connection: " % e) + + self.assertIsNotNone( + ssh_client, "Failed to setup ssh connection to ip=%s" % ip) + + return ssh_client + + def wait_until_host_is_in_state(self, hostid, resourcestate, interval=3, retries=20): + def check_resource_state(): + response = Host.list( + self.apiclient, + id=hostid + ) + if isinstance(response, list): + if response[0].resourcestate == resourcestate: + self.logger.debug('Host with id %s is in resource state = %s' % (hostid, resourcestate)) + return True, None + else: + self.logger.debug("Waiting for host " + hostid + + " to reach state " + resourcestate + + ", with current state " + response[0].resourcestate) + return False, None + + done, _ = wait_until(interval, retries, check_resource_state) + if not done: + raise Exception("Failed to wait for host %s to be on resource state %s" % (hostid, resourcestate)) + return True + + def update_config(self, enable_feature): + cmd = updateConfiguration.updateConfigurationCmd() + cmd.name = "enable.kvm.host.auto.enable.disable" + cmd.value = enable_feature + + response = self.apiclient.updateConfiguration(cmd) + self.debug("updated the parameter %s with value %s" % (response.name, response.value)) + + def update_health_check_script(self, ip_address, username, password, exit_code): + health_check_script_path = "/etc/cloudstack/agent/healthcheck.sh" + health_check_agent_property = "agent.health.check.script.path" + agent_properties_file_path = "/etc/cloudstack/agent/agent.properties" + + ssh_client = self.get_ssh_client(ip_address, username, password) + ssh_client.execute("echo 'exit %s' > %s" % (exit_code, health_check_script_path)) + ssh_client.execute("chmod +x %s" % health_check_script_path) + ssh_client.execute("echo '%s=%s' >> %s" % (health_check_agent_property, health_check_script_path, + agent_properties_file_path)) + ssh_client.execute("service cloudstack-agent restart") + + def remove_host_health_check(self, ip_address, username, password): + health_check_script_path = "/etc/cloudstack/agent/healthcheck.sh" + ssh_client = self.get_ssh_client(ip_address, username, password) + ssh_client.execute("rm -f %s" % health_check_script_path) + + def select_host_for_health_checks(self): + clusters = list_clusters( + self.apiclient, + zoneid=self.zone.id + ) + if not clusters: + return None + + for cluster in clusters: + list_hosts_response = list_hosts( + self.apiclient, + clusterid=cluster.id, + type="Routing", + resourcestate="Enabled" + ) + assert isinstance(list_hosts_response, list) + if not list_hosts_response or len(list_hosts_response) < 1: + continue + return list_hosts_response[0] + return None + + def update_host_allocation_state(self, id, enable): + cmd = updateHost.updateHostCmd() + cmd.id = id + cmd.allocationstate = "Enable" if enable else "Disable" + response = self.apiclient.updateHost(cmd) + self.assertEqual(response.resourcestate, "Enabled" if enable else "Disabled") + + @attr(tags=["basic", "advanced"], required_hardware="false") + @skipTestIf("hypervisorNotSupported") + def test_01_auto_enable_disable_kvm_host(self): + """Test to auto-enable and auto-disable a KVM host based on health check results + + # Validate the following: + # 1. Enable the KVM Auto Enable/Disable Feature + # 2. Set a health check script that fails and observe the host is Disabled + # 3. Make the health check script succeed and observe the host is Enabled + """ + + selected_host = self.select_host_for_health_checks() + if not selected_host: + self.skipTest("Cannot find a KVM host to test the auto-enable-disable feature") + + username = self.hostConfig["username"] + password = self.hostConfig["password"] + + # Enable the Auto Enable/Disable Configuration + self.update_config("true") + + # Set health check script for failure + self.update_health_check_script(selected_host.ipaddress, username, password, 1) + self.wait_until_host_is_in_state(selected_host.id, "Disabled", 5, 200) + + # Set health check script for success + self.update_health_check_script(selected_host.ipaddress, username, password, 0) + + self.wait_until_host_is_in_state(selected_host.id, "Enabled", 5, 200) + + @attr(tags=["basic", "advanced"], required_hardware="false") + @skipTestIf("hypervisorNotSupported") + def test_02_disable_host_overrides_auto_enable_kvm_host(self): + """Test to override the auto-enabling of a KVM host by an administrator + + # Validate the following: + # 1. Enable the KVM Auto Enable/Disable Feature + # 2. Set a health check script that succeeds and observe the host is Enabled + # 3. Make the host Disabled + # 4. Verify the host does not get auto-enabled after the previous step + """ + + selected_host = self.select_host_for_health_checks() + if not selected_host: + self.skipTest("Cannot find a KVM host to test the auto-enable-disable feature") + + username = self.hostConfig["username"] + password = self.hostConfig["password"] + + # Enable the Auto Enable/Disable Configuration + self.update_config("true") + + # Set health check script for failure + self.update_health_check_script(selected_host.ipaddress, username, password, 0) + self.wait_until_host_is_in_state(selected_host.id, "Enabled", 5, 200) + + # Manually disable the host + self.update_host_allocation_state(selected_host.id, False) + + # Wait for more than the ping interval + time.sleep(70) + + # Verify the host continues on Disabled state + self.wait_until_host_is_in_state(selected_host.id, "Disabled", 5, 200) + + # Restore the host to Enabled state + self.remove_host_health_check(selected_host.ipaddress, username, password) + self.update_host_allocation_state(selected_host.id, True) + + @attr(tags=["basic", "advanced"], required_hardware="false") + @skipTestIf("hypervisorNotSupported") + def test_03_enable_host_does_not_override_auto_disable_kvm_host(self): + """Test to override the auto-disabling of a KVM host by an administrator + + # Validate the following: + # 1. Enable the KVM Auto Enable/Disable Feature + # 2. Set a health check script that fails and observe the host is Disabled + # 3. Make the host Enabled + # 4. Verify the host does get auto-disabled after the previous step + """ + + selected_host = self.select_host_for_health_checks() + if not selected_host: + self.skipTest("Cannot find a KVM host to test the auto-enable-disable feature") + + username = self.hostConfig["username"] + password = self.hostConfig["password"] + + # Enable the Auto Enable/Disable Configuration + self.update_config("true") + + # Set health check script for failure + self.update_health_check_script(selected_host.ipaddress, username, password, 1) + self.wait_until_host_is_in_state(selected_host.id, "Disabled", 5, 200) + + # Manually enable the host + self.update_host_allocation_state(selected_host.id, True) + + # Verify the host goes back to Disabled state + self.wait_until_host_is_in_state(selected_host.id, "Disabled", 5, 200) + + # Restore the host to Enabled state + self.remove_host_health_check(selected_host.ipaddress, username, password) + self.update_host_allocation_state(selected_host.id, True) diff --git a/ui/src/config/section/infra/hosts.js b/ui/src/config/section/infra/hosts.js index 9f2c6292052..dce830a2a3f 100644 --- a/ui/src/config/section/infra/hosts.js +++ b/ui/src/config/section/infra/hosts.js @@ -102,8 +102,9 @@ export default { label: 'label.disable.host', message: 'message.confirm.disable.host', dataView: true, - defaultArgs: { allocationstate: 'Disable' }, - show: (record) => { return record.resourcestate === 'Enabled' } + show: (record) => { return record.resourcestate === 'Enabled' }, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/infra/HostEnableDisable'))) }, { api: 'updateHost', @@ -111,8 +112,9 @@ export default { label: 'label.enable.host', message: 'message.confirm.enable.host', dataView: true, - defaultArgs: { allocationstate: 'Enable' }, - show: (record) => { return record.resourcestate === 'Disabled' } + show: (record) => { return record.resourcestate === 'Disabled' }, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/infra/HostEnableDisable'))) }, { api: 'prepareHostForMaintenance', diff --git a/ui/src/views/infra/HostEnableDisable.vue b/ui/src/views/infra/HostEnableDisable.vue new file mode 100644 index 00000000000..bc71aa27080 --- /dev/null +++ b/ui/src/views/infra/HostEnableDisable.vue @@ -0,0 +1,133 @@ +// 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. + + + + + + From a9fb36174de190a3e21b6b44d9cfda0a8353324d Mon Sep 17 00:00:00 2001 From: Marcus Sorensen Date: Wed, 27 Sep 2023 12:46:32 +0530 Subject: [PATCH 05/30] Skip volume resize during service offering change when no size exists (#200) --- server/src/main/java/com/cloud/vm/UserVmManagerImpl.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index c9085b385f5..b43ba087a10 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -2093,6 +2093,12 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir for (final VolumeVO rootVolumeOfVm : vols) { DiskOfferingVO currentRootDiskOffering = _diskOfferingDao.findById(rootVolumeOfVm.getDiskOfferingId()); + + if (currentRootDiskOffering.getDiskSize() == 0 && newDiskOffering.getDiskSize() == 0) { + s_logger.debug("This change of service offering doesn't involve custom root disk sizes, skipping volume resize for volume: " + rootVolumeOfVm); + continue; + } + Long rootDiskSize= null; Long rootDiskSizeBytes = null; if (customParameters.containsKey(ApiConstants.ROOT_DISK_SIZE)) { From 5604638b84e0708ba31aedc56864d335f48fe446 Mon Sep 17 00:00:00 2001 From: Marcus Sorensen Date: Wed, 27 Sep 2023 13:11:07 +0530 Subject: [PATCH 06/30] Apple base416 passphrase enc (#240) * Move PassphraseVO to use String instead of byte[] to support Encrypt annotation * Check for unencrypted passphrases before migrating passphrase table --------- Co-authored-by: Marcus Sorensen Fixes #239 This PR moves PassphraseVO passphrase to String type. Since the GenericDaoBase manipulates encrypted fields as Strings we don't improve anything by handling as byte arrays. We still use byte arrays to pass these values down to the agents and we can get some security gains there. This PR also handles cases where the passphrase field may be previously unencrypted, and upgrades them to encrypted fields using the old encryption during cloudstack-migrate-databases. Then the process can upgrade to new encryption normally. --- .../crypt/EncryptionSecretKeyChanger.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/framework/db/src/main/java/com/cloud/utils/crypt/EncryptionSecretKeyChanger.java b/framework/db/src/main/java/com/cloud/utils/crypt/EncryptionSecretKeyChanger.java index 88830b3e3f9..6017dca58c1 100644 --- a/framework/db/src/main/java/com/cloud/utils/crypt/EncryptionSecretKeyChanger.java +++ b/framework/db/src/main/java/com/cloud/utils/crypt/EncryptionSecretKeyChanger.java @@ -486,6 +486,8 @@ public class EncryptionSecretKeyChanger { migrateImageStoreUrlForCifs(conn); migrateStoragePoolPathForSMB(conn); + preparePassphraseTableForMigration(conn); + // migrate columns with annotation @Encrypt migrateEncryptedTableColumns(conn); @@ -665,6 +667,56 @@ public class EncryptionSecretKeyChanger { System.out.println("End migrate user vm deploy_as_is details"); } + // encrypt any unencrypted passphrases using old style encryptor before we migrate + private void preparePassphraseTableForMigration(Connection conn) throws SQLException { + System.out.println("Preparing passphrase table by checking for unencrypted passphrases"); + + try(PreparedStatement selectPstmt = conn.prepareStatement("SELECT id, passphrase FROM passphrase"); + ResultSet rs = selectPstmt.executeQuery(); + PreparedStatement updatePstmt = conn.prepareStatement("UPDATE passphrase SET passphrase=? WHERE id=?") + ) { + while(rs.next()) { + long id = rs.getLong(1); + String value = rs.getString(2); + if (StringUtils.isBlank(value)) { + continue; + } + + // passphrases are 64 bytes long when unencrypted, longer when encrypted + if (value.length() == 64) { + // just confirm it won't decrypt, to be safe, before assuming raw value and encrypting + try { + oldEncryptor.decrypt(value); + System.out.printf("Passphrase table entry db id %d was already encrypted with old encryption\n", id); + } catch(EncryptionException | CloudRuntimeException ex) { + String message = null; + if (ex instanceof CloudRuntimeException && ex.getCause() != null) { + if ((ex.getCause() instanceof EncryptionException)) { + message = ex.getCause().getMessage(); + } + } else if (ex instanceof EncryptionException) { + message = ex.getMessage(); + } + + if (message != null && message.contains("Failed to decrypt")) { + System.out.printf("Encrypting unencrypted passphrase table entry db id %d before migration using old encryption\n", id); + String encrypted = oldEncryptor.encrypt(value); + updatePstmt.setBytes(1, encrypted.getBytes(StandardCharsets.UTF_8)); + updatePstmt.setLong(2, id); + updatePstmt.executeUpdate(); + } else { + throwCloudRuntimeException("Unhandled EncryptionException", ex); + } + } + } + } + } catch (SQLException e) { + throwCloudRuntimeException("Unable to prepare passphrase table", e); + } + + System.out.println("End preparing passphrase table"); + } + private void migrateImageStoreUrlForCifs(Connection conn) { System.out.println("Begin migrate image store url if protocol is cifs"); From 0d9aa70edd15d526b4d06357662f090d3f7585c4 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 7 Jul 2023 18:19:42 +0530 Subject: [PATCH 07/30] test: enable and fix scaleio unit tests (#289) Fixes #275 Signed-off-by: Abhishek Kumar (cherry picked from commit c82bc16a20527d5d07c323d5323bccd56fc604e0) Signed-off-by: Rohit Yadav --- plugins/storage/volume/scaleio/pom.xml | 3 -- .../client/ScaleIOGatewayClientImplTest.java | 4 +- .../ScaleIOPrimaryDataStoreDriverTest.java | 47 ++++++++++--------- .../ScaleIOPrimaryDataStoreLifeCycleTest.java | 41 ++++++---------- 4 files changed, 41 insertions(+), 54 deletions(-) diff --git a/plugins/storage/volume/scaleio/pom.xml b/plugins/storage/volume/scaleio/pom.xml index b7dd1f0c7a2..c829192df03 100644 --- a/plugins/storage/volume/scaleio/pom.xml +++ b/plugins/storage/volume/scaleio/pom.xml @@ -44,9 +44,6 @@ maven-surefire-plugin - - true - integration-test diff --git a/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/client/ScaleIOGatewayClientImplTest.java b/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/client/ScaleIOGatewayClientImplTest.java index 577b918a4d8..cf624c26680 100644 --- a/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/client/ScaleIOGatewayClientImplTest.java +++ b/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/client/ScaleIOGatewayClientImplTest.java @@ -48,7 +48,7 @@ import com.github.tomakehurst.wiremock.junit.WireMockRule; @RunWith(MockitoJUnitRunner.class) public class ScaleIOGatewayClientImplTest { - private final int port = 443; + private final int port = 8443; private final int timeout = 30; private final int maxConnections = 50; private final String username = "admin"; @@ -70,7 +70,7 @@ public class ScaleIOGatewayClientImplTest { .withHeader("content-type", "application/json;charset=UTF-8") .withBody(sessionKey))); - client = new ScaleIOGatewayClientImpl("https://localhost/api", username, password, false, timeout, maxConnections); + client = new ScaleIOGatewayClientImpl(String.format("https://localhost:%d/api", port), username, password, false, timeout, maxConnections); wireMockRule.stubFor(post("/api/types/Volume/instances") .willReturn(aResponse() diff --git a/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriverTest.java b/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriverTest.java index d147582c132..5236322745f 100644 --- a/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriverTest.java +++ b/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/driver/ScaleIOPrimaryDataStoreDriverTest.java @@ -19,23 +19,13 @@ package org.apache.cloudstack.storage.datastore.driver; -import com.cloud.agent.api.Answer; -import com.cloud.agent.api.storage.MigrateVolumeAnswer; -import com.cloud.agent.api.to.DataTO; -import com.cloud.agent.api.to.DiskTO; -import com.cloud.configuration.Config; -import com.cloud.host.Host; -import com.cloud.host.HostVO; -import com.cloud.host.dao.HostDao; -import com.cloud.storage.Storage; -import com.cloud.storage.Volume; -import com.cloud.storage.VolumeVO; -import com.cloud.storage.dao.VolumeDao; -import com.cloud.storage.dao.VolumeDetailsDao; -import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.vm.VMInstanceVO; -import com.cloud.vm.VirtualMachine; -import com.cloud.vm.dao.VMInstanceDao; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import java.util.Optional; + import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; @@ -62,12 +52,23 @@ import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; -import java.util.Optional; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.when; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.storage.MigrateVolumeAnswer; +import com.cloud.agent.api.to.DataTO; +import com.cloud.agent.api.to.DiskTO; +import com.cloud.configuration.Config; +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.storage.Storage; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.storage.dao.VolumeDetailsDao; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.VMInstanceDao; @RunWith(PowerMockRunner.class) @PrepareForTest(RemoteHostEndPoint.class) diff --git a/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/ScaleIOPrimaryDataStoreLifeCycleTest.java b/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/ScaleIOPrimaryDataStoreLifeCycleTest.java index 6cc7b874557..a8f3f194932 100644 --- a/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/ScaleIOPrimaryDataStoreLifeCycleTest.java +++ b/plugins/storage/volume/scaleio/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/ScaleIOPrimaryDataStoreLifeCycleTest.java @@ -24,8 +24,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -40,13 +38,11 @@ import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProviderManag import org.apache.cloudstack.engine.subsystem.api.storage.HypervisorHostListener; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope; -import org.apache.cloudstack.storage.datastore.client.ScaleIOGatewayClient; import org.apache.cloudstack.storage.datastore.client.ScaleIOGatewayClientConnectionPool; import org.apache.cloudstack.storage.datastore.client.ScaleIOGatewayClientImpl; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; -import org.apache.cloudstack.storage.datastore.provider.ScaleIOHostListener; import org.apache.cloudstack.storage.datastore.util.ScaleIOUtil; import org.apache.cloudstack.storage.volume.datastore.PrimaryDataStoreHelper; import org.junit.Before; @@ -55,14 +51,14 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.Spy; +import org.mockito.stubbing.Answer; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.test.util.ReflectionTestUtils; import com.cloud.agent.AgentManager; import com.cloud.agent.api.ModifyStoragePoolAnswer; -import com.cloud.agent.api.ModifyStoragePoolCommand; import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.Status; @@ -81,7 +77,7 @@ import com.cloud.storage.dao.StoragePoolHostDao; import com.cloud.template.TemplateManager; import com.cloud.utils.exception.CloudRuntimeException; -@PrepareForTest(ScaleIOGatewayClient.class) +@PrepareForTest(ScaleIOGatewayClientConnectionPool.class) @RunWith(PowerMockRunner.class) public class ScaleIOPrimaryDataStoreLifeCycleTest { @@ -114,20 +110,16 @@ public class ScaleIOPrimaryDataStoreLifeCycleTest { @Mock ModifyStoragePoolAnswer answer; - @Spy @InjectMocks private StorageManager storageMgr = new StorageManagerImpl(); - @Spy - @InjectMocks - private HypervisorHostListener hostListener = new ScaleIOHostListener(); - @InjectMocks private ScaleIOPrimaryDataStoreLifeCycle scaleIOPrimaryDataStoreLifeCycleTest; @Before public void setUp() { initMocks(this); + ReflectionTestUtils.setField(scaleIOPrimaryDataStoreLifeCycleTest, "storageMgr", storageMgr); } @Test @@ -135,9 +127,11 @@ public class ScaleIOPrimaryDataStoreLifeCycleTest { final DataStore dataStore = mock(DataStore.class); when(dataStore.getId()).thenReturn(1L); - PowerMockito.mockStatic(ScaleIOGatewayClient.class); + PowerMockito.mockStatic(ScaleIOGatewayClientConnectionPool.class); ScaleIOGatewayClientImpl client = mock(ScaleIOGatewayClientImpl.class); - when(ScaleIOGatewayClientConnectionPool.getInstance().getClient(1L, storagePoolDetailsDao)).thenReturn(client); + ScaleIOGatewayClientConnectionPool pool = mock(ScaleIOGatewayClientConnectionPool.class); + when(pool.getClient(1L, storagePoolDetailsDao)).thenReturn(client); + when(ScaleIOGatewayClientConnectionPool.getInstance()).thenAnswer((Answer) invocation -> pool); when(client.haveConnectedSdcs()).thenReturn(true); @@ -157,28 +151,20 @@ public class ScaleIOPrimaryDataStoreLifeCycleTest { when(dataStoreMgr.getDataStore(anyLong(), eq(DataStoreRole.Primary))).thenReturn(store); when(store.getId()).thenReturn(1L); - when(store.getPoolType()).thenReturn(Storage.StoragePoolType.PowerFlex); when(store.isShared()).thenReturn(true); when(store.getName()).thenReturn("ScaleIOPool"); when(store.getStorageProviderName()).thenReturn(ScaleIOUtil.PROVIDER_NAME); when(dataStoreProviderMgr.getDataStoreProvider(ScaleIOUtil.PROVIDER_NAME)).thenReturn(dataStoreProvider); when(dataStoreProvider.getName()).thenReturn(ScaleIOUtil.PROVIDER_NAME); + HypervisorHostListener hostListener = Mockito.mock(HypervisorHostListener.class); + when(hostListener.hostConnect(Mockito.anyLong(), Mockito.anyLong())).thenReturn(true); storageMgr.registerHostListener(ScaleIOUtil.PROVIDER_NAME, hostListener); - when(agentMgr.easySend(anyLong(), Mockito.any(ModifyStoragePoolCommand.class))).thenReturn(answer); - when(answer.getResult()).thenReturn(true); - - when(storagePoolHostDao.findByPoolHost(anyLong(), anyLong())).thenReturn(null); - - when(hostDao.findById(1L)).thenReturn(host1); - when(hostDao.findById(2L)).thenReturn(host2); - when(dataStoreHelper.attachZone(Mockito.any(DataStore.class))).thenReturn(null); - scaleIOPrimaryDataStoreLifeCycleTest.attachZone(dataStore, scope, Hypervisor.HypervisorType.KVM); - verify(storageMgr,times(2)).connectHostToSharedPool(Mockito.any(Long.class), Mockito.any(Long.class)); - verify(storagePoolHostDao,times(2)).persist(Mockito.any(StoragePoolHostVO.class)); + boolean result = scaleIOPrimaryDataStoreLifeCycleTest.attachZone(dataStore, scope, Hypervisor.HypervisorType.KVM); + assertThat(result).isTrue(); } @Test(expected = CloudRuntimeException.class) @@ -239,6 +225,9 @@ public class ScaleIOPrimaryDataStoreLifeCycleTest { List poolHostVOs = new ArrayList<>(); when(storagePoolHostDao.listByPoolId(anyLong())).thenReturn(poolHostVOs); when(dataStoreHelper.deletePrimaryDataStore(any(DataStore.class))).thenReturn(true); + PowerMockito.mockStatic(ScaleIOGatewayClientConnectionPool.class); + ScaleIOGatewayClientConnectionPool pool = mock(ScaleIOGatewayClientConnectionPool.class); + when(ScaleIOGatewayClientConnectionPool.getInstance()).thenAnswer((Answer) invocation -> pool); final boolean result = scaleIOPrimaryDataStoreLifeCycleTest.deleteDataStore(store); assertThat(result).isTrue(); } From d8db09ac58f2e208c15fa13ab5b0f007ae3b33b0 Mon Sep 17 00:00:00 2001 From: Marcus Sorensen Date: Thu, 24 Aug 2023 10:39:22 -0600 Subject: [PATCH 08/30] Publish event for VM.STOP when out of band stop is detected Signed-off-by: Marcus Sorensen (cherry picked from commit 72a3d54db0a0bb0afbcbee35abc1e98ef102aa49) Signed-off-by: Rohit Yadav --- .../main/java/com/cloud/vm/VirtualMachineManagerImpl.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index 300cef3ac58..77c21195d88 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -47,6 +47,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; import javax.persistence.EntityExistsException; +import com.cloud.event.ActionEventUtils; import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; @@ -4822,6 +4823,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac VM_SYNC_ALERT_SUBJECT, "VM " + vm.getHostName() + "(" + vm.getInstanceName() + ") state is sync-ed (" + vm.getState() + " -> Running) from out-of-context transition. VM network environment may need to be reset"); + ActionEventUtils.onActionEvent(User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM, vm.getDomainId(), + EventTypes.EVENT_VM_START, "Out of band VM power on", vm.getId(), ApiCommandResourceType.VirtualMachine.toString()); s_logger.info("VM " + vm.getInstanceName() + " is sync-ed to at Running state according to power-on report from hypervisor"); break; @@ -4855,6 +4858,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac case Stopping: case Running: case Stopped: + ActionEventUtils.onActionEvent(User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM,vm.getDomainId(), + EventTypes.EVENT_VM_STOP, "Out of band VM power off", vm.getId(), ApiCommandResourceType.VirtualMachine.toString()); case Migrating: if (s_logger.isInfoEnabled()) { s_logger.info( From a571cde7a9e4210daf1754fd747761e98be03fb3 Mon Sep 17 00:00:00 2001 From: Marcus Sorensen Date: Thu, 14 Sep 2023 04:55:54 -0600 Subject: [PATCH 09/30] Increase reserve on ScaleIO disk formatting for fragmentation (#317) Signed-off-by: Marcus Sorensen Co-authored-by: Marcus Sorensen (cherry picked from commit 59217e3fe19fc01863d678a34a618914afaf901b) Signed-off-by: Rohit Yadav --- .../cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptor.java | 4 ++-- .../hypervisor/kvm/storage/ScaleIOStorageAdaptorTest.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptor.java index 607dd620f61..b85be041e48 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptor.java @@ -552,12 +552,12 @@ public class ScaleIOStorageAdaptor implements StorageAdaptor { /** * Calculates usable size from raw size, assuming qcow2 requires 192k/1GB for metadata - * We also remove 32MiB for potential encryption/safety factor. + * We also remove 128MiB for encryption/fragmentation/safety factor. * @param raw size in bytes * @return usable size in bytesbytes */ public static long getUsableBytesFromRawBytes(Long raw) { - long usable = raw - (32 << 20) - ((raw >> 30) * 200704); + long usable = raw - (128 << 20) - ((raw >> 30) * 200704); if (usable < 0) { usable = 0L; } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptorTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptorTest.java index c06442c6ae3..6115dade07c 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptorTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptorTest.java @@ -23,9 +23,9 @@ import org.junit.Test; public class ScaleIOStorageAdaptorTest { @Test public void getUsableBytesFromRawBytesTest() { - Assert.assertEquals("Overhead calculated for 8Gi size", 8554774528L, ScaleIOStorageAdaptor.getUsableBytesFromRawBytes(8L << 30)); - Assert.assertEquals("Overhead calculated for 4Ti size", 4294130925568L, ScaleIOStorageAdaptor.getUsableBytesFromRawBytes(4000L << 30)); - Assert.assertEquals("Overhead calculated for 500Gi size", 536737005568L, ScaleIOStorageAdaptor.getUsableBytesFromRawBytes(500L << 30)); + Assert.assertEquals("Overhead calculated for 8Gi size", 8454111232L, ScaleIOStorageAdaptor.getUsableBytesFromRawBytes(8L << 30)); + Assert.assertEquals("Overhead calculated for 4Ti size", 4294030262272L, ScaleIOStorageAdaptor.getUsableBytesFromRawBytes(4000L << 30)); + Assert.assertEquals("Overhead calculated for 500Gi size", 536636342272L, ScaleIOStorageAdaptor.getUsableBytesFromRawBytes(500L << 30)); Assert.assertEquals("Unsupported small size", 0, ScaleIOStorageAdaptor.getUsableBytesFromRawBytes(1L)); } } From be2bea64a9e2fc848d62875dc0a44a5d3dc99a9f Mon Sep 17 00:00:00 2001 From: Oscar Sandoval Date: Fri, 22 Sep 2023 08:01:05 -0700 Subject: [PATCH 10/30] fix units label (#325) (cherry picked from commit d708603b6346033f0d023edfebe85113fcc928eb) Signed-off-by: Rohit Yadav --- .../orchestration/service/VolumeOrchestrationService.java | 2 +- .../schema/src/main/resources/META-INF/db/schema-301to302.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java index 2666cfadc70..15f5b231be2 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java @@ -79,7 +79,7 @@ public interface VolumeOrchestrationService { Long.class, "storage.max.volume.size", "2000", - "The maximum size for a volume (in GB).", + "The maximum size for a volume (in GiB).", true); VolumeInfo moveVolume(VolumeInfo volume, long destPoolDcId, Long destPoolPodId, Long destPoolClusterId, HypervisorType dataDiskHyperType) diff --git a/engine/schema/src/main/resources/META-INF/db/schema-301to302.sql b/engine/schema/src/main/resources/META-INF/db/schema-301to302.sql index f33fcb436d8..4532757d052 100755 --- a/engine/schema/src/main/resources/META-INF/db/schema-301to302.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-301to302.sql @@ -51,7 +51,7 @@ INSERT IGNORE INTO `cloud`.`configuration` VALUES ('Advanced', 'DEFAULT', 'manag INSERT IGNORE INTO `cloud`.`configuration` VALUES ('Advanced', 'DEFAULT', 'management-server', 'secstorage.capacity.standby', '10', 'The minimal number of command execution sessions that system is able to serve immediately(standby capacity)'); INSERT IGNORE INTO `cloud`.`configuration` VALUES ('Advanced', 'DEFAULT', 'management-server', 'secstorage.cmd.execution.time.max', '30', 'The max command execution time in minute'); INSERT IGNORE INTO `cloud`.`configuration` VALUES ('Advanced', 'DEFAULT', 'management-server', 'secstorage.session.max', '50', 'The max number of command execution sessions that a SSVM can handle'); -INSERT IGNORE INTO `cloud`.`configuration` VALUES ('Storage', 'DEFAULT', 'management-server', 'storage.max.volume.size', '2000', 'The maximum size for a volume (in GB).'); +INSERT IGNORE INTO `cloud`.`configuration` VALUES ('Storage', 'DEFAULT', 'management-server', 'storage.max.volume.size', '2000', 'The maximum size for a volume (in GiB).'); INSERT IGNORE INTO `cloud`.`configuration` VALUES ('Advanced', 'DEFAULT', 'management-server', 'task.cleanup.retry.interval', '600', 'Time (in seconds) to wait before retrying cleanup of tasks if the cleanup failed previously. 0 means to never retry.'); INSERT IGNORE INTO `cloud`.`configuration` VALUES ('Advanced', 'DEFAULT', 'management-server', 'vmware.additional.vnc.portrange.start', '50000', 'Start port number of additional VNC port range'); INSERT IGNORE INTO `cloud`.`configuration` VALUES ('Advanced', 'DEFAULT', 'management-server', 'vmware.percluster.host.max', '8', 'maxmium hosts per vCenter cluster(do not let it grow over 8)'); From fe50018fba726b293e0910997dd060f5ed4e209d Mon Sep 17 00:00:00 2001 From: Marcus Sorensen Date: Tue, 26 Sep 2023 03:21:11 -0600 Subject: [PATCH 11/30] Allow configkey to set 'cloud-name' cloud-init metadata (#7964) * Allow configkey to set 'cloud-name' cloud-init metadata * Update engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java Co-authored-by: Daniel Augusto Veronezi Salvador <38945620+GutoVeronezi@users.noreply.github.com> * Update server/src/main/java/com/cloud/network/NetworkModelImpl.java Co-authored-by: Daniel Augusto Veronezi Salvador <38945620+GutoVeronezi@users.noreply.github.com> * Update server/src/main/java/com/cloud/network/router/CommandSetupHelper.java Co-authored-by: Daniel Augusto Veronezi Salvador <38945620+GutoVeronezi@users.noreply.github.com> * Revert "Update server/src/main/java/com/cloud/network/router/CommandSetupHelper.java" This reverts commit 8abc3e38c4f973227e77253a8bcf3c94827d187f. * Revert "Update server/src/main/java/com/cloud/network/NetworkModelImpl.java" This reverts commit 7f239be919112913d764b141e943acca4473f755. * Rework/Fix review code suggestions --------- Co-authored-by: Marcus Sorensen Co-authored-by: Daniel Augusto Veronezi Salvador <38945620+GutoVeronezi@users.noreply.github.com> (cherry picked from commit 155a30748cfdcdffe31ce3b13f54008323f4774a) Signed-off-by: Rohit Yadav --- api/src/main/java/com/cloud/network/NetworkModel.java | 4 +++- .../src/main/java/com/cloud/vm/VirtualMachineManager.java | 3 +++ .../main/java/com/cloud/vm/VirtualMachineManagerImpl.java | 2 +- server/src/main/java/com/cloud/network/NetworkModelImpl.java | 5 +++++ .../java/com/cloud/network/router/CommandSetupHelper.java | 5 +++++ 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/com/cloud/network/NetworkModel.java b/api/src/main/java/com/cloud/network/NetworkModel.java index 96f38b64bcd..53ac735cf05 100644 --- a/api/src/main/java/com/cloud/network/NetworkModel.java +++ b/api/src/main/java/com/cloud/network/NetworkModel.java @@ -73,6 +73,7 @@ public interface NetworkModel { String HYPERVISOR_HOST_NAME_FILE = "hypervisor-host-name"; String CLOUD_DOMAIN_FILE = "cloud-domain"; String CLOUD_DOMAIN_ID_FILE = "cloud-domain-id"; + String CLOUD_NAME_FILE = "cloud-name"; int CONFIGDATA_DIR = 0; int CONFIGDATA_FILE = 1; int CONFIGDATA_CONTENT = 2; @@ -83,11 +84,12 @@ public interface NetworkModel { .put(PUBLIC_HOSTNAME_FILE, "name") .put(CLOUD_DOMAIN_FILE, CLOUD_DOMAIN_FILE) .put(CLOUD_DOMAIN_ID_FILE, CLOUD_DOMAIN_ID_FILE) + .put(CLOUD_NAME_FILE, CLOUD_NAME_FILE) .put(HYPERVISOR_HOST_NAME_FILE, HYPERVISOR_HOST_NAME_FILE) .build(); List metadataFileNames = new ArrayList<>(Arrays.asList(SERVICE_OFFERING_FILE, AVAILABILITY_ZONE_FILE, LOCAL_HOSTNAME_FILE, LOCAL_IPV4_FILE, PUBLIC_HOSTNAME_FILE, PUBLIC_IPV4_FILE, - INSTANCE_ID_FILE, VM_ID_FILE, PUBLIC_KEYS_FILE, CLOUD_IDENTIFIER_FILE, HYPERVISOR_HOST_NAME_FILE)); + INSTANCE_ID_FILE, VM_ID_FILE, PUBLIC_KEYS_FILE, CLOUD_IDENTIFIER_FILE, CLOUD_NAME_FILE, HYPERVISOR_HOST_NAME_FILE)); static final ConfigKey MACIdentifier = new ConfigKey<>("Advanced",Integer.class, "mac.identifier", "0", "This value will be used while generating the mac addresses for isolated and shared networks. The hexadecimal equivalent value will be present at the 2nd octet of the mac address. Default value is zero (0) which means that the DB id of the zone will be used.", true, ConfigKey.Scope.Zone); diff --git a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java index 82396cf4635..8cd67f25331 100644 --- a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java +++ b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java @@ -83,6 +83,9 @@ public interface VirtualMachineManager extends Manager { ConfigKey AllowExposeDomainInMetadata = new ConfigKey<>("Advanced", Boolean.class, "metadata.allow.expose.domain", "false", "If set to true, it allows the VM's domain to be seen in metadata.", true, ConfigKey.Scope.Domain); + ConfigKey MetadataCustomCloudName = new ConfigKey<>("Advanced", String.class, "metadata.custom.cloud.name", "", + "If provided, a custom cloud-name in cloud-init metadata", true, ConfigKey.Scope.Zone); + interface Topics { String VM_POWER_STATE = "vm.powerstate"; } diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index 77c21195d88..8325d770309 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -4727,7 +4727,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac VmOpLockStateRetry, VmOpWaitInterval, ExecuteInSequence, VmJobCheckInterval, VmJobTimeout, VmJobStateReportInterval, VmConfigDriveLabel, VmConfigDriveOnPrimaryPool, VmConfigDriveForceHostCacheUse, VmConfigDriveUseHostCacheOnUnsupportedPool, HaVmRestartHostUp, ResourceCountRunningVMsonly, AllowExposeHypervisorHostname, AllowExposeHypervisorHostnameAccountLevel, SystemVmRootDiskSize, - AllowExposeDomainInMetadata + AllowExposeDomainInMetadata, MetadataCustomCloudName }; } diff --git a/server/src/main/java/com/cloud/network/NetworkModelImpl.java b/server/src/main/java/com/cloud/network/NetworkModelImpl.java index beb416cab57..2b251b1fdd3 100644 --- a/server/src/main/java/com/cloud/network/NetworkModelImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkModelImpl.java @@ -2673,6 +2673,11 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi vmData.add(new String[]{METATDATA_DIR, CLOUD_DOMAIN_ID_FILE, domain.getUuid()}); } + String customCloudName = VirtualMachineManager.MetadataCustomCloudName.valueIn(datacenterId); + if (org.apache.commons.lang3.StringUtils.isNotBlank(customCloudName)) { + vmData.add(new String[]{METATDATA_DIR, CLOUD_NAME_FILE, customCloudName}); + } + return vmData; } diff --git a/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java b/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java index 6bd01998600..a7ed6478efa 100644 --- a/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java +++ b/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java @@ -231,6 +231,11 @@ public class CommandSetupHelper { vmDataCommand.addVmData(NetworkModel.METATDATA_DIR, NetworkModel.CLOUD_DOMAIN_ID_FILE, domain.getUuid()); } + String customCloudName = VirtualMachineManager.MetadataCustomCloudName.valueIn(vm.getDataCenterId()); + if (org.apache.commons.lang3.StringUtils.isNotBlank(customCloudName)) { + vmDataCommand.addVmData(NetworkModel.METATDATA_DIR, NetworkModel.CLOUD_NAME_FILE, customCloudName); + } + cmds.addCommand("vmdata", vmDataCommand); } } From 4dfa38aae971138deada82c873aa048e22b4478d Mon Sep 17 00:00:00 2001 From: Nicolas Vazquez Date: Wed, 16 Aug 2023 12:23:24 -0300 Subject: [PATCH 12/30] plugins: Add Custom hypervisor minimal changes (#7692) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design document: https://cwiki.apache.org/confluence/display/CLOUDSTACK/%5BDRAFT%5D+Minimal+changes+to+allow+new+dynamic+hypervisor+type%3A+Custom+Hypervisor This PR introduces the minimal changes to add a new hypervisor type (internally named Custom in the codebase, and configurable display name), allowing to write an external hypervisor plugin as a Custom Hypervisor to CloudStack The custom hypervisor name is set by the setting: 'hypervisor.custom.display.name'. The new hypervisor type does not affect the behaviour of any CloudStack operation, it simply introduces a new hypervisor type into the system. CloudStack does not have any means to dynamically add new hypervisor types. The hypervisor types are internally preset by an enum defined within the CloudStack codebase and unless a new version supports a new hypervisor it is not possible to add a host of a hypervisor that is not in part of the enum. It is possible to implement minimal changes in CloudStack to support a new hypervisor plugin that may be developed privately This PR is an initial work on allowing new dynamic hypervisor types (adds a new element to the HypervisorType enum, but allows variable display name for the hypervisor) Replace the HypervisorType from a fixed enum to an extensible registry mechanism, registered from the hypervisor plugin - The new hypervisor type is internally named 'Custom' to the CloudStack services (management server and agent services, database records). - A new global setting ‘hypervisor.custom.display.name’ allows administrators to set the display name of the hypervisor type. The display name will be shown in the CloudStack UI and API. - In case the ‘hypervisor.list’ setting contains the display name of the new hypervisor type, the setting value is automatically updated after the ‘hypervisor.custom.display.name’ setting is updated. - The new Custom hypervisor type supports: - Direct downloads (the ability to download templates into primary storage from the hypervisor hosts without using secondary storage) - Local storage (use hypervisor hosts local storage as primary storage) - Template format: RAW format (the templates to be registered on the new hypervisor type must be in RAW format) - The UI is also extended to display the new hypervisor type and the supported features listed above. - The above are the minimal changes for CloudStack to support the new hypervisor type, which can be tested by integrating the plugin codebase with this feature. This PR allows the cloud administrators to test custom hypervisor plugins implementations in CloudStack and easily integrate it into CloudStack as a new hypervisor type ("Custom"), reducing the implementation to only the hypervisor supported specific storage/networking and the hypervisor resource to communicate with the management server. - CloudStack admin should be able to create a zone for the new custom hypervisor and add clusters, hosts into the zone with normal operations - CloudStack users should be able to execute normal VMs/volumes/network/storage operations on VMs/volumes running on the custom hypervisor hosts (cherry picked from commit 8b5ba13b817e7d53beb0e0012c27d065e96a708c) Signed-off-by: Rohit Yadav --- .../java/com/cloud/hypervisor/Hypervisor.java | 18 +++++++++-- .../com/cloud/hypervisor/HypervisorGuru.java | 5 +++ .../user/template/RegisterTemplateCmd.java | 9 ++++-- .../response/HostForMigrationResponse.java | 5 ++- .../cloudstack/api/response/HostResponse.java | 7 ++-- .../HypervisorCapabilitiesResponse.java | 7 ++-- .../api/response/VMSnapshotResponse.java | 7 ++-- .../cloud/storage/GuestOSHypervisorVO.java | 3 +- .../src/test/resources/component.xml | 5 +++ .../src/main/resources/components-example.xml | 1 + .../main/java/com/cloud/api/ApiDBUtils.java | 3 ++ .../java/com/cloud/api/ApiResponseHelper.java | 13 ++++---- .../query/dao/DomainRouterJoinDaoImpl.java | 2 +- .../cloud/api/query/dao/HostJoinDaoImpl.java | 10 ++++-- .../api/query/dao/StoragePoolJoinDaoImpl.java | 4 +-- .../api/query/dao/TemplateJoinDaoImpl.java | 4 +-- .../api/query/dao/UserVmJoinDaoImpl.java | 2 +- .../api/query/dao/VolumeJoinDaoImpl.java | 12 ++++--- .../ConfigurationManagerImpl.java | 18 +++++++++++ .../cloud/hypervisor/HypervisorGuruBase.java | 5 ++- .../discoverer/CustomServerDiscoverer.java | 32 +++++++++++++++++++ .../cloud/resource/ResourceManagerImpl.java | 5 ++- .../cloud/server/ManagementServerImpl.java | 6 ++-- .../template/HypervisorTemplateAdapter.java | 16 ++++++---- .../cloud/template/TemplateManagerImpl.java | 8 +++-- .../java/com/cloud/vm/UserVmManagerImpl.java | 23 ++++++++++++- .../spring-server-discoverer-context.xml | 5 +++ .../com/cloud/vm/UserVmManagerImplTest.java | 29 ++++++++++++++--- ui/src/config/section/infra/hosts.js | 4 ++- ui/src/store/getters.js | 3 +- ui/src/store/modules/user.js | 27 +++++++++++++++- .../views/image/RegisterOrUploadTemplate.vue | 32 +++++++++++++++++-- .../infra/zone/ZoneWizardAddResources.vue | 7 ++-- 33 files changed, 271 insertions(+), 66 deletions(-) create mode 100644 server/src/main/java/com/cloud/hypervisor/discoverer/CustomServerDiscoverer.java diff --git a/api/src/main/java/com/cloud/hypervisor/Hypervisor.java b/api/src/main/java/com/cloud/hypervisor/Hypervisor.java index 429de5774a4..2f0cc736af3 100644 --- a/api/src/main/java/com/cloud/hypervisor/Hypervisor.java +++ b/api/src/main/java/com/cloud/hypervisor/Hypervisor.java @@ -27,7 +27,7 @@ public class Hypervisor { static Map hypervisorTypeMap; static Map supportedImageFormatMap; - public static enum HypervisorType { + public enum HypervisorType { None, //for storage hosts XenServer, KVM, @@ -40,6 +40,7 @@ public class Hypervisor { Ovm, Ovm3, LXC, + Custom, Any; /*If you don't care about the hypervisor type*/ @@ -57,6 +58,7 @@ public class Hypervisor { hypervisorTypeMap.put("lxc", HypervisorType.LXC); hypervisorTypeMap.put("any", HypervisorType.Any); hypervisorTypeMap.put("ovm3", HypervisorType.Ovm3); + hypervisorTypeMap.put("custom", HypervisorType.Custom); supportedImageFormatMap = new HashMap<>(); supportedImageFormatMap.put(HypervisorType.XenServer, ImageFormat.VHD); @@ -68,7 +70,19 @@ public class Hypervisor { public static HypervisorType getType(String hypervisor) { return hypervisor == null ? HypervisorType.None : - hypervisorTypeMap.getOrDefault(hypervisor.toLowerCase(Locale.ROOT), HypervisorType.None); + (hypervisor.toLowerCase(Locale.ROOT).equalsIgnoreCase( + HypervisorGuru.HypervisorCustomDisplayName.value()) ? Custom : + hypervisorTypeMap.getOrDefault(hypervisor.toLowerCase(Locale.ROOT), HypervisorType.None)); + } + + /** + * Returns the display name of a hypervisor type in case the custom hypervisor is used, + * using the 'hypervisor.custom.display.name' setting. Otherwise, returns hypervisor name + */ + public String getHypervisorDisplayName() { + return !Hypervisor.HypervisorType.Custom.equals(this) ? + this.toString() : + HypervisorGuru.HypervisorCustomDisplayName.value(); } /** diff --git a/api/src/main/java/com/cloud/hypervisor/HypervisorGuru.java b/api/src/main/java/com/cloud/hypervisor/HypervisorGuru.java index c7dc0bba109..2dfa707b57b 100644 --- a/api/src/main/java/com/cloud/hypervisor/HypervisorGuru.java +++ b/api/src/main/java/com/cloud/hypervisor/HypervisorGuru.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.framework.config.ConfigKey; import com.cloud.agent.api.Command; import com.cloud.agent.api.to.NicTO; @@ -35,6 +36,10 @@ import com.cloud.vm.VirtualMachineProfile; public interface HypervisorGuru extends Adapter { + ConfigKey HypervisorCustomDisplayName = new ConfigKey<>(String.class, + "hypervisor.custom.display.name", ConfigKey.CATEGORY_ADVANCED, "Custom", + "Display name for custom hypervisor", true, ConfigKey.Scope.Global, null); + HypervisorType getHypervisorType(); /** diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java index 255b11aaa24..5dfcd41c2e0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import com.cloud.hypervisor.HypervisorGuru; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; @@ -342,9 +343,11 @@ public class RegisterTemplateCmd extends BaseCmd implements UserCmd { throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Parameter zoneids cannot combine all zones (-1) option with other zones"); - if (isDirectDownload() && !getHypervisor().equalsIgnoreCase(Hypervisor.HypervisorType.KVM.toString())) { - throw new ServerApiException(ApiErrorCode.PARAM_ERROR, - "Parameter directdownload is only allowed for KVM templates"); + String customHypervisor = HypervisorGuru.HypervisorCustomDisplayName.value(); + if (isDirectDownload() && !(getHypervisor().equalsIgnoreCase(Hypervisor.HypervisorType.KVM.toString()) + || getHypervisor().equalsIgnoreCase(customHypervisor))) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Parameter directdownload " + + "is only allowed for KVM or %s templates", customHypervisor)); } if (!isDeployAsIs() && osTypeId == null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/response/HostForMigrationResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/HostForMigrationResponse.java index 4ed0cdd8d74..41a0fdc4567 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/HostForMigrationResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/HostForMigrationResponse.java @@ -24,7 +24,6 @@ import org.apache.cloudstack.api.EntityReference; import com.cloud.host.Host; import com.cloud.host.Status; -import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.serializer.Param; import com.google.gson.annotations.SerializedName; @@ -84,7 +83,7 @@ public class HostForMigrationResponse extends BaseResponse { @SerializedName(ApiConstants.HYPERVISOR) @Param(description = "the host hypervisor") - private HypervisorType hypervisor; + private String hypervisor; @SerializedName("cpunumber") @Param(description = "the CPU number of the host") @@ -295,7 +294,7 @@ public class HostForMigrationResponse extends BaseResponse { this.version = version; } - public void setHypervisor(HypervisorType hypervisor) { + public void setHypervisor(String hypervisor) { this.hypervisor = hypervisor; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java index 5d809cf1553..e1f1e5ee241 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java @@ -29,7 +29,6 @@ import org.apache.cloudstack.outofbandmanagement.OutOfBandManagement; import com.cloud.host.Host; import com.cloud.host.Status; -import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.serializer.Param; import com.google.gson.annotations.SerializedName; @@ -89,7 +88,7 @@ public class HostResponse extends BaseResponseWithAnnotations { @SerializedName(ApiConstants.HYPERVISOR) @Param(description = "the host hypervisor") - private HypervisorType hypervisor; + private String hypervisor; @SerializedName("cpusockets") @Param(description = "the number of CPU sockets on the host") @@ -335,7 +334,7 @@ public class HostResponse extends BaseResponseWithAnnotations { this.version = version; } - public void setHypervisor(HypervisorType hypervisor) { + public void setHypervisor(String hypervisor) { this.hypervisor = hypervisor; } @@ -602,7 +601,7 @@ public class HostResponse extends BaseResponseWithAnnotations { return version; } - public HypervisorType getHypervisor() { + public String getHypervisor() { return hypervisor; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/HypervisorCapabilitiesResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/HypervisorCapabilitiesResponse.java index b5e5624bbe5..c19397e0c83 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/HypervisorCapabilitiesResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/HypervisorCapabilitiesResponse.java @@ -20,7 +20,6 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; import org.apache.cloudstack.api.EntityReference; -import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.hypervisor.HypervisorCapabilities; import com.cloud.serializer.Param; import com.google.gson.annotations.SerializedName; @@ -37,7 +36,7 @@ public class HypervisorCapabilitiesResponse extends BaseResponse { @SerializedName(ApiConstants.HYPERVISOR) @Param(description = "the hypervisor type") - private HypervisorType hypervisor; + private String hypervisor; @SerializedName(ApiConstants.MAX_GUESTS_LIMIT) @Param(description = "the maximum number of guest vms recommended for this hypervisor") @@ -83,11 +82,11 @@ public class HypervisorCapabilitiesResponse extends BaseResponse { this.hypervisorVersion = hypervisorVersion; } - public HypervisorType getHypervisor() { + public String getHypervisor() { return hypervisor; } - public void setHypervisor(HypervisorType hypervisor) { + public void setHypervisor(String hypervisor) { this.hypervisor = hypervisor; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VMSnapshotResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VMSnapshotResponse.java index 37670cf6224..9b553ed0744 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/VMSnapshotResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/VMSnapshotResponse.java @@ -25,7 +25,6 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponseWithTagInformation; import org.apache.cloudstack.api.EntityReference; -import com.cloud.hypervisor.Hypervisor; import com.cloud.serializer.Param; import com.cloud.vm.snapshot.VMSnapshot; import com.google.gson.annotations.SerializedName; @@ -111,7 +110,7 @@ public class VMSnapshotResponse extends BaseResponseWithTagInformation implement @SerializedName(ApiConstants.HYPERVISOR) @Param(description = "the type of hypervisor on which snapshot is stored") - private Hypervisor.HypervisorType hypervisor; + private String hypervisor; public VMSnapshotResponse() { tags = new LinkedHashSet(); @@ -266,11 +265,11 @@ public class VMSnapshotResponse extends BaseResponseWithTagInformation implement this.tags = tags; } - public Hypervisor.HypervisorType getHypervisor() { + public String getHypervisor() { return hypervisor; } - public void setHypervisor(Hypervisor.HypervisorType hypervisor) { + public void setHypervisor(String hypervisor) { this.hypervisor = hypervisor; } } diff --git a/engine/schema/src/main/java/com/cloud/storage/GuestOSHypervisorVO.java b/engine/schema/src/main/java/com/cloud/storage/GuestOSHypervisorVO.java index 087649b887a..e900d28a864 100644 --- a/engine/schema/src/main/java/com/cloud/storage/GuestOSHypervisorVO.java +++ b/engine/schema/src/main/java/com/cloud/storage/GuestOSHypervisorVO.java @@ -26,6 +26,7 @@ import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; +import com.cloud.hypervisor.Hypervisor; import com.cloud.utils.db.GenericDao; @Entity @@ -72,7 +73,7 @@ public class GuestOSHypervisorVO implements GuestOSHypervisor { @Override public String getHypervisorType() { - return hypervisorType; + return Hypervisor.HypervisorType.getType(hypervisorType).toString(); } @Override diff --git a/engine/storage/integration-test/src/test/resources/component.xml b/engine/storage/integration-test/src/test/resources/component.xml index aee37145114..d384d546665 100644 --- a/engine/storage/integration-test/src/test/resources/component.xml +++ b/engine/storage/integration-test/src/test/resources/component.xml @@ -121,6 +121,11 @@ + + + + diff --git a/plugins/network-elements/dns-notifier/src/main/resources/components-example.xml b/plugins/network-elements/dns-notifier/src/main/resources/components-example.xml index c53c0b14ef1..76a6cad9b4a 100755 --- a/plugins/network-elements/dns-notifier/src/main/resources/components-example.xml +++ b/plugins/network-elements/dns-notifier/src/main/resources/components-example.xml @@ -111,6 +111,7 @@ under the License. + diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java index c4d7f3cc717..38c7b99150d 100644 --- a/server/src/main/java/com/cloud/api/ApiDBUtils.java +++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java @@ -1283,6 +1283,9 @@ public class ApiDBUtils { // If this check is not passed, the hypervisor type will remain OVM. type = HypervisorType.KVM; break; + } else if (pool.getHypervisor() == HypervisorType.Custom) { + type = HypervisorType.Custom; + break; } } } diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 17c465d40ce..cf2d9be1ad9 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -37,6 +37,7 @@ import java.util.stream.Collectors; import javax.inject.Inject; +import com.cloud.hypervisor.Hypervisor; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.affinity.AffinityGroup; @@ -730,7 +731,7 @@ public class ApiResponseHelper implements ResponseGenerator { if (vm != null) { vmSnapshotResponse.setVirtualMachineId(vm.getUuid()); vmSnapshotResponse.setVirtualMachineName(StringUtils.isEmpty(vm.getDisplayName()) ? vm.getHostName() : vm.getDisplayName()); - vmSnapshotResponse.setHypervisor(vm.getHypervisorType()); + vmSnapshotResponse.setHypervisor(vm.getHypervisorType().getHypervisorDisplayName()); DataCenterVO datacenter = ApiDBUtils.findZoneById(vm.getDataCenterId()); if (datacenter != null) { vmSnapshotResponse.setZoneId(datacenter.getUuid()); @@ -1393,7 +1394,7 @@ public class ApiResponseHelper implements ResponseGenerator { clusterResponse.setZoneId(dc.getUuid()); clusterResponse.setZoneName(dc.getName()); } - clusterResponse.setHypervisorType(cluster.getHypervisorType().toString()); + clusterResponse.setHypervisorType(cluster.getHypervisorType().getHypervisorDisplayName()); clusterResponse.setClusterType(cluster.getClusterType().toString()); clusterResponse.setAllocationState(cluster.getAllocationState().toString()); clusterResponse.setManagedState(cluster.getManagedState().toString()); @@ -1589,7 +1590,7 @@ public class ApiResponseHelper implements ResponseGenerator { vmResponse.setTemplateName(template.getName()); } vmResponse.setCreated(vm.getCreated()); - vmResponse.setHypervisor(vm.getHypervisorType().toString()); + vmResponse.setHypervisor(vm.getHypervisorType().getHypervisorDisplayName()); if (vm.getHostId() != null) { Host host = ApiDBUtils.findHostById(vm.getHostId()); @@ -2752,7 +2753,7 @@ public class ApiResponseHelper implements ResponseGenerator { public HypervisorCapabilitiesResponse createHypervisorCapabilitiesResponse(HypervisorCapabilities hpvCapabilities) { HypervisorCapabilitiesResponse hpvCapabilitiesResponse = new HypervisorCapabilitiesResponse(); hpvCapabilitiesResponse.setId(hpvCapabilities.getUuid()); - hpvCapabilitiesResponse.setHypervisor(hpvCapabilities.getHypervisorType()); + hpvCapabilitiesResponse.setHypervisor(hpvCapabilities.getHypervisorType().getHypervisorDisplayName()); hpvCapabilitiesResponse.setHypervisorVersion(hpvCapabilities.getHypervisorVersion()); hpvCapabilitiesResponse.setIsSecurityGroupEnabled(hpvCapabilities.isSecurityGroupEnabled()); hpvCapabilitiesResponse.setMaxGuestsLimit(hpvCapabilities.getMaxGuestsLimit()); @@ -3660,7 +3661,7 @@ public class ApiResponseHelper implements ResponseGenerator { public GuestOsMappingResponse createGuestOSMappingResponse(GuestOSHypervisor guestOSHypervisor) { GuestOsMappingResponse response = new GuestOsMappingResponse(); response.setId(guestOSHypervisor.getUuid()); - response.setHypervisor(guestOSHypervisor.getHypervisorType()); + response.setHypervisor(Hypervisor.HypervisorType.getType(guestOSHypervisor.getHypervisorType()).getHypervisorDisplayName()); response.setHypervisorVersion(guestOSHypervisor.getHypervisorVersion()); response.setOsNameForHypervisor((guestOSHypervisor.getGuestOsName())); response.setIsUserDefined(Boolean.valueOf(guestOSHypervisor.getIsUserDefined()).toString()); @@ -4888,7 +4889,7 @@ public class ApiResponseHelper implements ResponseGenerator { response.setId(certificate.getUuid()); response.setAlias(certificate.getAlias()); handleCertificateResponse(certificate.getCertificate(), response); - response.setHypervisor(certificate.getHypervisorType().name()); + response.setHypervisor(certificate.getHypervisorType().getHypervisorDisplayName()); response.setObjectName("directdownloadcertificate"); return response; } diff --git a/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDaoImpl.java index 83a89622bd2..e3011bc4d66 100644 --- a/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDaoImpl.java @@ -126,7 +126,7 @@ public class DomainRouterJoinDaoImpl extends GenericDaoBase implements hostResponse.setCpuNumber(host.getCpus()); hostResponse.setZoneId(host.getZoneUuid()); hostResponse.setDisconnectedOn(host.getDisconnectedOn()); - hostResponse.setHypervisor(host.getHypervisorType()); + if (host.getHypervisorType() != null) { + String hypervisorType = host.getHypervisorType().getHypervisorDisplayName(); + hostResponse.setHypervisor(hypervisorType); + } hostResponse.setHostType(host.getType()); hostResponse.setLastPinged(new Date(host.getLastPinged())); Long mshostId = host.getManagementServerId(); @@ -239,7 +242,8 @@ public class HostJoinDaoImpl extends GenericDaoBase implements hostResponse.setUefiCapabilty(new Boolean(false)); } } - if (details.contains(HostDetails.all) && host.getHypervisorType() == Hypervisor.HypervisorType.KVM) { + if (details.contains(HostDetails.all) && (host.getHypervisorType() == Hypervisor.HypervisorType.KVM || + host.getHypervisorType() == Hypervisor.HypervisorType.Custom)) { //only kvm has the requirement to return host details try { hostResponse.setDetails(hostDetails); @@ -303,7 +307,7 @@ public class HostJoinDaoImpl extends GenericDaoBase implements hostResponse.setCpuNumber(host.getCpus()); hostResponse.setZoneId(host.getZoneUuid()); hostResponse.setDisconnectedOn(host.getDisconnectedOn()); - hostResponse.setHypervisor(host.getHypervisorType()); + hostResponse.setHypervisor(host.getHypervisorType().getHypervisorDisplayName()); hostResponse.setHostType(host.getType()); hostResponse.setLastPinged(new Date(host.getLastPinged())); hostResponse.setManagementServerId(host.getManagementServerId()); diff --git a/server/src/main/java/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java index 527cc949ed1..de469d21a11 100644 --- a/server/src/main/java/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/StoragePoolJoinDaoImpl.java @@ -110,7 +110,7 @@ public class StoragePoolJoinDaoImpl extends GenericDaoBase[] getConfigKeys() { - return new ConfigKey[] {VmMinMemoryEqualsMemoryDividedByMemOverprovisioningFactor, VmMinCpuSpeedEqualsCpuSpeedDividedByCpuOverprovisioningFactor }; + return new ConfigKey[] {VmMinMemoryEqualsMemoryDividedByMemOverprovisioningFactor, + VmMinCpuSpeedEqualsCpuSpeedDividedByCpuOverprovisioningFactor, + HypervisorCustomDisplayName + }; } } diff --git a/server/src/main/java/com/cloud/hypervisor/discoverer/CustomServerDiscoverer.java b/server/src/main/java/com/cloud/hypervisor/discoverer/CustomServerDiscoverer.java new file mode 100644 index 00000000000..534947f092e --- /dev/null +++ b/server/src/main/java/com/cloud/hypervisor/discoverer/CustomServerDiscoverer.java @@ -0,0 +1,32 @@ +// 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.hypervisor.discoverer; + +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.kvm.discoverer.LibvirtServerDiscoverer; + +public class CustomServerDiscoverer extends LibvirtServerDiscoverer { + @Override + public Hypervisor.HypervisorType getHypervisorType() { + return Hypervisor.HypervisorType.Custom; + } + + @Override + protected String getPatchPath() { + return "scripts/vm/hypervisor/kvm/"; + } +} diff --git a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java index 09bd1480b11..69b80d10c01 100755 --- a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java +++ b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java @@ -42,6 +42,7 @@ import com.cloud.exception.StorageUnavailableException; import com.cloud.storage.Volume; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.VolumeDao; +import com.cloud.hypervisor.HypervisorGuru; import org.apache.cloudstack.alert.AlertService; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; @@ -652,7 +653,9 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, } } - return discoverHostsFull(dcId, podId, clusterId, clusterName, url, username, password, cmd.getHypervisor(), hostTags, cmd.getFullUrlParams(), false); + String hypervisorType = cmd.getHypervisor().equalsIgnoreCase(HypervisorGuru.HypervisorCustomDisplayName.value()) ? + "Custom" : cmd.getHypervisor(); + return discoverHostsFull(dcId, podId, clusterId, clusterName, url, username, password, hypervisorType, hostTags, cmd.getFullUrlParams(), false); } @Override diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 31a78744153..0d31b9254b7 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -1266,7 +1266,9 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe } if (hypervisorType != null) { - sc.setParameters("hypervisorType", hypervisorType); + String hypervisorStr = (String) hypervisorType; + String hypervisorSearch = HypervisorType.getType(hypervisorStr).toString(); + sc.setParameters("hypervisorType", hypervisorSearch); } if (clusterType != null) { @@ -4376,7 +4378,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe } else { final List clustersForZone = _clusterDao.listByZoneId(zoneId); for (final ClusterVO cluster : clustersForZone) { - result.add(cluster.getHypervisorType().toString()); + result.add(cluster.getHypervisorType().getHypervisorDisplayName()); } } diff --git a/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java b/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java index f5c19ecff1d..3f45505bcc5 100644 --- a/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java +++ b/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java @@ -148,20 +148,21 @@ public class HypervisorTemplateAdapter extends TemplateAdapterBase { } /** - * Validate on random running KVM host that URL is reachable + * Validate on random running host that URL is reachable * @param url url */ - private Long performDirectDownloadUrlValidation(final String format, final String url, final List zoneIds) { + private Long performDirectDownloadUrlValidation(final String format, final Hypervisor.HypervisorType hypervisor, + final String url, final List zoneIds) { HostVO host = null; if (zoneIds != null && !zoneIds.isEmpty()) { for (Long zoneId : zoneIds) { - host = resourceManager.findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, zoneId); + host = resourceManager.findOneRandomRunningHostByHypervisor(hypervisor, zoneId); if (host != null) { break; } } } else { - host = resourceManager.findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, null); + host = resourceManager.findOneRandomRunningHostByHypervisor(hypervisor, null); } if (host == null) { @@ -198,7 +199,8 @@ public class HypervisorTemplateAdapter extends TemplateAdapterBase { zoneIds = new ArrayList<>(); zoneIds.add(cmd.getZoneId()); } - Long templateSize = performDirectDownloadUrlValidation(ImageFormat.ISO.getFileExtension(), url, zoneIds); + Long templateSize = performDirectDownloadUrlValidation(ImageFormat.ISO.getFileExtension(), + Hypervisor.HypervisorType.KVM, url, zoneIds); profile.setSize(templateSize); } profile.setUrl(url); @@ -221,9 +223,11 @@ public class HypervisorTemplateAdapter extends TemplateAdapterBase { TemplateProfile profile = super.prepare(cmd); String url = profile.getUrl(); UriUtils.validateUrl(cmd.getFormat(), url, cmd.isDirectDownload()); + Hypervisor.HypervisorType hypervisor = Hypervisor.HypervisorType.getType(cmd.getHypervisor()); if (cmd.isDirectDownload()) { DigestHelper.validateChecksumString(cmd.getChecksum()); - Long templateSize = performDirectDownloadUrlValidation(cmd.getFormat(), url, cmd.getZoneIds()); + Long templateSize = performDirectDownloadUrlValidation(cmd.getFormat(), + hypervisor, url, cmd.getZoneIds()); profile.setSize(templateSize); } profile.setUrl(url); diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java index d1fd204c024..bb8affc1870 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -309,8 +309,12 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, if (type == HypervisorType.BareMetal) { adapter = AdapterBase.getAdapterByName(_adapters, TemplateAdapterType.BareMetal.getName()); } else { - // see HypervisorTemplateAdapter - adapter = AdapterBase.getAdapterByName(_adapters, TemplateAdapterType.Hypervisor.getName()); + // Get template adapter according to hypervisor + adapter = AdapterBase.getAdapterByName(_adapters, type.name()); + // Otherwise, default to generic hypervisor template adapter + if (adapter == null) { + adapter = AdapterBase.getAdapterByName(_adapters, TemplateAdapterType.Hypervisor.getName()); + } } if (adapter == null) { diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index b43ba087a10..bf77424b617 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -648,6 +648,14 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir HypervisorType.Simulator )); + protected static final List ROOT_DISK_SIZE_OVERRIDE_SUPPORTING_HYPERVISORS = Arrays.asList( + HypervisorType.KVM, + HypervisorType.XenServer, + HypervisorType.VMware, + HypervisorType.Simulator, + HypervisorType.Custom + ); + private static final List HYPERVISORS_THAT_CAN_DO_STORAGE_MIGRATION_ON_NON_USER_VMS = Arrays.asList(HypervisorType.KVM, HypervisorType.VMware); @Override @@ -4360,7 +4368,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir * @throws InvalidParameterValueException if the hypervisor does not support rootdisksize override */ protected void verifyIfHypervisorSupportsRootdiskSizeOverride(HypervisorType hypervisorType) { - if (!(hypervisorType == HypervisorType.KVM || hypervisorType == HypervisorType.XenServer || hypervisorType == HypervisorType.VMware || hypervisorType == HypervisorType.Simulator)) { + if (!ROOT_DISK_SIZE_OVERRIDE_SUPPORTING_HYPERVISORS.contains(hypervisorType)) { throw new InvalidParameterValueException("Hypervisor " + hypervisorType + " does not support rootdisksize override"); } } @@ -5076,6 +5084,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir Answer startAnswer = cmds.getAnswer(StartAnswer.class); String returnedIp = null; String originalIp = null; + String originalVncPassword = profile.getVirtualMachine().getVncPassword(); + String returnedVncPassword = null; if (startAnswer != null) { StartAnswer startAns = (StartAnswer)startAnswer; VirtualMachineTO vmTO = startAns.getVirtualMachine(); @@ -5084,6 +5094,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir returnedIp = nicTO.getIp(); } } + returnedVncPassword = vmTO.getVncPassword(); } List nics = _nicDao.listByVmId(vm.getId()); @@ -5135,6 +5146,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } } + updateVncPasswordIfItHasChanged(originalVncPassword, returnedVncPassword, profile); + // get system ip and create static nat rule for the vm try { _rulesMgr.getSystemIpAndEnableStaticNatForVm(profile.getVirtualMachine(), false); @@ -5169,6 +5182,14 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir return true; } + protected void updateVncPasswordIfItHasChanged(String originalVncPassword, String returnedVncPassword, VirtualMachineProfile profile) { + if (returnedVncPassword != null && !originalVncPassword.equals(returnedVncPassword)) { + UserVmVO userVm = _vmDao.findById(profile.getId()); + userVm.setVncPassword(returnedVncPassword); + _vmDao.update(userVm.getId(), userVm); + } + } + @Override public void finalizeExpunge(VirtualMachine vm) { } diff --git a/server/src/main/resources/META-INF/cloudstack/server-discoverer/spring-server-discoverer-context.xml b/server/src/main/resources/META-INF/cloudstack/server-discoverer/spring-server-discoverer-context.xml index 3a7e0ffb04a..98abb08ae11 100644 --- a/server/src/main/resources/META-INF/cloudstack/server-discoverer/spring-server-discoverer-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/server-discoverer/spring-server-discoverer-context.xml @@ -38,6 +38,11 @@ + + + + diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java index 22a9bed7764..daefb1b4e39 100644 --- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java +++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java @@ -193,6 +193,9 @@ public class UserVmManagerImplTest { @Mock private ServiceOfferingVO serviceOffering; + @Mock + VirtualMachineProfile virtualMachineProfile; + private static final long vmId = 1l; private static final long zoneId = 2L; private static final long accountId = 3L; @@ -220,6 +223,8 @@ public class UserVmManagerImplTest { customParameters.put(VmDetailConstants.ROOT_DISK_SIZE, "123"); lenient().doNothing().when(resourceLimitMgr).incrementResourceCount(anyLong(), any(Resource.ResourceType.class)); lenient().doNothing().when(resourceLimitMgr).decrementResourceCount(anyLong(), any(Resource.ResourceType.class), anyLong()); + + Mockito.when(virtualMachineProfile.getId()).thenReturn(vmId); } @After @@ -548,13 +553,10 @@ public class UserVmManagerImplTest { public void verifyIfHypervisorSupportRootdiskSizeOverrideTest() { Hypervisor.HypervisorType[] hypervisorTypeArray = Hypervisor.HypervisorType.values(); int exceptionCounter = 0; - int expectedExceptionCounter = hypervisorTypeArray.length - 4; + int expectedExceptionCounter = hypervisorTypeArray.length - 5; for(int i = 0; i < hypervisorTypeArray.length; i++) { - if (Hypervisor.HypervisorType.KVM == hypervisorTypeArray[i] - || Hypervisor.HypervisorType.XenServer == hypervisorTypeArray[i] - || Hypervisor.HypervisorType.VMware == hypervisorTypeArray[i] - || Hypervisor.HypervisorType.Simulator == hypervisorTypeArray[i]) { + if (UserVmManagerImpl.ROOT_DISK_SIZE_OVERRIDE_SUPPORTING_HYPERVISORS.contains(hypervisorTypeArray[i])) { userVmManagerImpl.verifyIfHypervisorSupportsRootdiskSizeOverride(hypervisorTypeArray[i]); } else { try { @@ -927,4 +929,21 @@ public class UserVmManagerImplTest { userVmManagerImpl.createVirtualMachine(deployVMCmd); } + + @Test + public void testUpdateVncPasswordIfItHasChanged() { + String vncPassword = "12345678"; + userVmManagerImpl.updateVncPasswordIfItHasChanged(vncPassword, vncPassword, virtualMachineProfile); + Mockito.verify(userVmDao, Mockito.never()).update(vmId, userVmVoMock); + } + + @Test + public void testUpdateVncPasswordIfItHasChangedNewPassword() { + String vncPassword = "12345678"; + String newPassword = "87654321"; + Mockito.when(userVmVoMock.getId()).thenReturn(vmId); + userVmManagerImpl.updateVncPasswordIfItHasChanged(vncPassword, newPassword, virtualMachineProfile); + Mockito.verify(userVmDao).findById(vmId); + Mockito.verify(userVmDao).update(vmId, userVmVoMock); + } } diff --git a/ui/src/config/section/infra/hosts.js b/ui/src/config/section/infra/hosts.js index dce830a2a3f..376bd973a84 100644 --- a/ui/src/config/section/infra/hosts.js +++ b/ui/src/config/section/infra/hosts.js @@ -80,7 +80,9 @@ export default { label: 'label.action.secure.host', message: 'message.action.secure.host', dataView: true, - show: (record) => { return record.hypervisor === 'KVM' }, + show: (record) => { + return record.hypervisor === 'KVM' || record.hypervisor === store.getters.customHypervisorName + }, args: ['hostid'], mapping: { hostid: { diff --git a/ui/src/store/getters.js b/ui/src/store/getters.js index 0273fd02b19..5b01bead597 100644 --- a/ui/src/store/getters.js +++ b/ui/src/store/getters.js @@ -48,7 +48,8 @@ const getters = { twoFaProvider: state => state.user.twoFaProvider, twoFaIssuer: state => state.user.twoFaIssuer, loginFlag: state => state.user.loginFlag, - allProjects: (state) => state.app.allProjects + allProjects: (state) => state.app.allProjects, + customHypervisorName: state => state.user.customHypervisorName } export default getters diff --git a/ui/src/store/modules/user.js b/ui/src/store/modules/user.js index 3994a3fc29d..6a509676f06 100644 --- a/ui/src/store/modules/user.js +++ b/ui/src/store/modules/user.js @@ -63,7 +63,8 @@ const user = { customColumns: {}, twoFaEnabled: false, twoFaProvider: '', - twoFaIssuer: '' + twoFaIssuer: '', + customHypervisorName: 'Custom' }, mutations: { @@ -147,6 +148,9 @@ const user = { }, SET_LOGIN_FLAG: (state, flag) => { state.loginFlag = flag + }, + SET_CUSTOM_HYPERVISOR_NAME (state, name) { + state.customHypervisorName = name } }, @@ -294,6 +298,15 @@ const user = { commit('SET_CLOUDIAN', cloudian) }).catch(ignored => { }) + + api('listConfigurations', { name: 'hypervisor.custom.display.name' }).then(json => { + if (json.listconfigurationsresponse.configuration !== null) { + const config = json.listconfigurationsresponse.configuration[0] + commit('SET_CUSTOM_HYPERVISOR_NAME', config.value) + } + }).catch(error => { + reject(error) + }) }) }, @@ -391,6 +404,15 @@ const user = { }).catch(error => { reject(error) }) + + api('listConfigurations', { name: 'hypervisor.custom.display.name' }).then(json => { + if (json.listconfigurationsresponse.configuration !== null) { + const config = json.listconfigurationsresponse.configuration[0] + commit('SET_CUSTOM_HYPERVISOR_NAME', config.value) + } + }).catch(error => { + reject(error) + }) }) }, UpdateConfiguration ({ commit }) { @@ -411,6 +433,9 @@ const user = { }, SetLoginFlag ({ commit }, loggedIn) { commit('SET_LOGIN_FLAG', loggedIn) + }, + SetCustomHypervisorName ({ commit }, name) { + commit('SET_CUSTOM_HYPERVISOR_NAME', name) } } } diff --git a/ui/src/views/image/RegisterOrUploadTemplate.vue b/ui/src/views/image/RegisterOrUploadTemplate.vue index c8e7ad3510e..b5214e80baa 100644 --- a/ui/src/views/image/RegisterOrUploadTemplate.vue +++ b/ui/src/views/image/RegisterOrUploadTemplate.vue @@ -173,7 +173,7 @@ - +