From 9cc88b8dcccbaace9a831ab1721f295944eba432 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Mon, 29 Sep 2025 09:10:56 +0200 Subject: [PATCH 001/311] CKS: fix control plane endpoint IP (#11720) --- .../cluster/actionworkers/KubernetesClusterStartWorker.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java index 9ffee220a10..8cd539b78e4 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java @@ -198,16 +198,16 @@ public class KubernetesClusterStartWorker extends KubernetesClusterResourceModif String initArgs = ""; if (haSupported) { initArgs = String.format("--control-plane-endpoint %s:%d --upload-certs --certificate-key %s ", - controlNodeIp, + serverIp, CLUSTER_API_PORT, KubernetesClusterUtil.generateClusterHACertificateKey(kubernetesCluster)); } - initArgs += String.format("--apiserver-cert-extra-sans=%s", controlNodeIp); + initArgs += String.format("--apiserver-cert-extra-sans=%s", serverIp); initArgs += String.format(" --kubernetes-version=%s", getKubernetesClusterVersion().getSemanticVersion()); k8sControlNodeConfig = k8sControlNodeConfig.replace(clusterInitArgsKey, initArgs); k8sControlNodeConfig = k8sControlNodeConfig.replace(ejectIsoKey, String.valueOf(ejectIso)); k8sControlNodeConfig = k8sControlNodeConfig.replace(etcdEndpointList, endpointList); - k8sControlNodeConfig = k8sControlNodeConfig.replace(k8sServerIp, controlNodeIp); + k8sControlNodeConfig = k8sControlNodeConfig.replace(k8sServerIp, serverIp); k8sControlNodeConfig = k8sControlNodeConfig.replace(k8sApiPort, String.valueOf(CLUSTER_API_PORT)); k8sControlNodeConfig = k8sControlNodeConfig.replace(certSans, String.format("- %s", serverIp)); k8sControlNodeConfig = k8sControlNodeConfig.replace(k8sCertificate, KubernetesClusterUtil.generateClusterHACertificateKey(kubernetesCluster)); From 3159fa7d84e2e6b0f791689484811dccfdcda77d Mon Sep 17 00:00:00 2001 From: Vishesh <8760112+vishesh92@users.noreply.github.com> Date: Mon, 29 Sep 2025 19:48:42 +0530 Subject: [PATCH 002/311] noVNC: make show dot configurable (#11741) --- .../java/com/cloud/consoleproxy/ConsoleProxyManager.java | 4 ++++ .../java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java | 2 +- .../cloudstack/consoleproxy/ConsoleAccessManagerImpl.java | 5 ++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java index 59819934e3a..7b5fc123fb0 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java @@ -56,6 +56,10 @@ public interface ConsoleProxyManager extends Manager, ConsoleProxyService { ConfigKey NoVncConsoleSourceIpCheckEnabled = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Boolean.class, "novnc.console.sourceip.check.enabled", "false", "If true, The source IP to access novnc console must be same as the IP in request to management server for console URL. Needs to reconnect CPVM to management server when this changes (via restart CPVM, or management server, or cloud service in CPVM)", false); + ConfigKey NoVncConsoleShowDot = new ConfigKey<>(Boolean.class, "novnc.console.show.dot", ConfigKey.CATEGORY_ADVANCED, "true", + "If true, in noVNC console a dot cursor will be shown when the remote server provides no local cursor, or provides a fully-transparent (invisible) cursor.", + true, ConfigKey.Scope.Zone, null); + ConfigKey ConsoleProxyServiceOffering = new ConfigKey<>(String.class, "consoleproxy.service.offering", "Console Proxy", null, "Uuid of the service offering used by console proxy; if NULL - system offering will be used", true, ConfigKey.Scope.Zone, null); diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index 98fa8a4dcfd..c273cf40e2f 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -1572,7 +1572,7 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy public ConfigKey[] getConfigKeys() { return new ConfigKey[] { ConsoleProxySslEnabled, NoVncConsoleDefault, NoVncConsoleSourceIpCheckEnabled, ConsoleProxyServiceOffering, ConsoleProxyCapacityStandby, ConsoleProxyCapacityScanInterval, ConsoleProxyCmdPort, ConsoleProxyRestart, ConsoleProxyUrlDomain, ConsoleProxySessionMax, ConsoleProxySessionTimeout, ConsoleProxyDisableRpFilter, ConsoleProxyLaunchMax, - ConsoleProxyManagementLastState, ConsoleProxyServiceManagementState }; + ConsoleProxyManagementLastState, ConsoleProxyServiceManagementState, NoVncConsoleShowDot }; } protected ConsoleProxyStatus parseJsonToConsoleProxyStatus(String json) throws JsonParseException { diff --git a/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java b/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java index fde937764e7..237135b5176 100644 --- a/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java @@ -89,6 +89,8 @@ import com.cloud.vm.dao.VMInstanceDetailsDao; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import static com.cloud.consoleproxy.ConsoleProxyManager.NoVncConsoleShowDot; + public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAccessManager { @Inject @@ -565,8 +567,9 @@ public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAcce if (param.getHypervHost() != null || !ConsoleProxyManager.NoVncConsoleDefault.value()) { sb.append("/ajax?token=").append(token); } else { + String showDot = NoVncConsoleShowDot.valueIn(vm.getDataCenterId()) ? "true" : "false"; sb.append("/resource/noVNC/vnc.html") - .append("?autoconnect=true&show_dot=true") + .append("?autoconnect=true&show_dot=").append(showDot) .append("&port=").append(vncPort) .append("&token=").append(token); if (requiresVncOverWebSocketConnection(vm, hostVo) && StringUtils.isNotBlank(locale)) { From 30cb8c7a82005e334f939956d8db6546fa02b5c0 Mon Sep 17 00:00:00 2001 From: Nicolas Vazquez Date: Tue, 30 Sep 2025 04:01:07 -0300 Subject: [PATCH 003/311] Fix importing unmanaged instances due to incorrect internal name (#11753) --- .../java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java index 30cf4ad76a7..e5eb29a798d 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -1128,7 +1128,7 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager { } String internalCSName = unmanagedInstance.getInternalCSName(); - if (StringUtils.isEmpty(instanceNameInternal)) { + if (StringUtils.isEmpty(internalCSName)) { internalCSName = instanceNameInternal; } Map allDetails = new HashMap<>(details); From 70af55e84897ab23b8f37f7ade04a520d99e1d8a Mon Sep 17 00:00:00 2001 From: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> Date: Tue, 30 Sep 2025 12:50:44 +0530 Subject: [PATCH 004/311] UI support for extraconfig in deploy and update instance (#11719) --- .../apache/cloudstack/api/ApiConstants.java | 1 + .../user/config/ListCapabilitiesCmd.java | 1 + .../api/response/CapabilitiesResponse.java | 8 ++++++ .../cloud/server/ManagementServerImpl.java | 2 ++ .../main/java/com/cloud/vm/UserVmManager.java | 9 +++++++ .../java/com/cloud/vm/UserVmManagerImpl.java | 9 +++---- ui/public/locales/en.json | 2 ++ ui/src/views/compute/DeployVM.vue | 12 +++++++++ ui/src/views/compute/EditVM.vue | 25 ++++++++++++++++++- 9 files changed, 62 insertions(+), 7 deletions(-) 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 89c9a194e3f..4abc0d13d74 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -26,6 +26,7 @@ public class ApiConstants { public static final String ACTIVATION_RULE = "activationrule"; public static final String ACTIVITY = "activity"; public static final String ADAPTER_TYPE = "adaptertype"; + public static final String ADDITONAL_CONFIG_ENABLED = "additionalconfigenabled"; public static final String ADDRESS = "address"; public static final String ALGORITHM = "algorithm"; public static final String ALIAS = "alias"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java index bd3f39a09aa..7553ccffa7d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java @@ -73,6 +73,7 @@ public class ListCapabilitiesCmd extends BaseCmd { response.setSharedFsVmMinCpuCount((Integer)capabilities.get(ApiConstants.SHAREDFSVM_MIN_CPU_COUNT)); response.setSharedFsVmMinRamSize((Integer)capabilities.get(ApiConstants.SHAREDFSVM_MIN_RAM_SIZE)); response.setDynamicScalingEnabled((Boolean) capabilities.get(ApiConstants.DYNAMIC_SCALING_ENABLED)); + response.setAdditionalConfigEnabled((Boolean) capabilities.get(ApiConstants.ADDITONAL_CONFIG_ENABLED)); response.setObjectName("capability"); response.setResponseName(getCommandName()); this.setResponseObject(response); diff --git a/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java index ff2e33b1389..affa130d4b0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java @@ -140,6 +140,10 @@ public class CapabilitiesResponse extends BaseResponse { @Param(description = "true if dynamically scaling for instances is enabled", since = "4.21.0") private Boolean dynamicScalingEnabled; + @SerializedName(ApiConstants.ADDITONAL_CONFIG_ENABLED) + @Param(description = "true if additional configurations or extraconfig can be passed to Instances", since = "4.20.2") + private Boolean additionalConfigEnabled; + public void setSecurityGroupsEnabled(boolean securityGroupsEnabled) { this.securityGroupsEnabled = securityGroupsEnabled; } @@ -255,4 +259,8 @@ public class CapabilitiesResponse extends BaseResponse { public void setDynamicScalingEnabled(Boolean dynamicScalingEnabled) { this.dynamicScalingEnabled = dynamicScalingEnabled; } + + public void setAdditionalConfigEnabled(Boolean additionalConfigEnabled) { + this.additionalConfigEnabled = additionalConfigEnabled; + } } diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 271372bf656..56e8a56f2e2 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -4535,6 +4535,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe } capabilities.put(ApiConstants.SHAREDFSVM_MIN_CPU_COUNT, fsVmMinCpu); capabilities.put(ApiConstants.SHAREDFSVM_MIN_RAM_SIZE, fsVmMinRam); + capabilities.put(ApiConstants.ADDITONAL_CONFIG_ENABLED, UserVmManager.EnableAdditionalVmConfig.valueIn(caller.getId())); + return capabilities; } diff --git a/server/src/main/java/com/cloud/vm/UserVmManager.java b/server/src/main/java/com/cloud/vm/UserVmManager.java index f2a8a672d42..21ac6e3eb36 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManager.java +++ b/server/src/main/java/com/cloud/vm/UserVmManager.java @@ -83,6 +83,15 @@ public interface UserVmManager extends UserVmService { "If set to true, tags specified in `resource.limit.host.tags` are also included in vm.strict.host.tags.", true); + ConfigKey EnableAdditionalVmConfig = new ConfigKey<>( + "Advanced", + Boolean.class, + "enable.additional.vm.configuration", + "false", + "allow additional arbitrary configuration to vm", + true, + ConfigKey.Scope.Account); + static final int MAX_USER_DATA_LENGTH_BYTES = 2048; public static final String CKS_NODE = "cksnode"; diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 5b3284c2c1e..a67484b6dd6 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -670,9 +670,6 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir private static final ConfigKey AllowDeployVmIfGivenHostFails = new ConfigKey("Advanced", Boolean.class, "allow.deploy.vm.if.deploy.on.given.host.fails", "false", "allow vm to deploy on different host if vm fails to deploy on the given host ", true); - private static final ConfigKey EnableAdditionalVmConfig = new ConfigKey<>("Advanced", Boolean.class, - "enable.additional.vm.configuration", "false", "allow additional arbitrary configuration to vm", true, ConfigKey.Scope.Account); - private static final ConfigKey KvmAdditionalConfigAllowList = new ConfigKey<>(String.class, "allow.additional.vm.configuration.list.kvm", "Advanced", "", "Comma separated list of allowed additional configuration options.", true, ConfigKey.Scope.Account, null, null, EnableAdditionalVmConfig.key(), null, null, ConfigKey.Kind.CSV, null); @@ -6280,7 +6277,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir protected void persistExtraConfigVmware(String decodedUrl, UserVm vm) { boolean isValidConfig = isValidKeyValuePair(decodedUrl); if (isValidConfig) { - String[] extraConfigs = decodedUrl.split("\\r?\\n"); + String[] extraConfigs = decodedUrl.split("\\r?\\n+"); for (String cfg : extraConfigs) { // Validate cfg against unsupported operations set by admin here String[] allowedKeyList = VmwareAdditionalConfigAllowList.value().split(","); @@ -6308,7 +6305,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir protected void persistExtraConfigXenServer(String decodedUrl, UserVm vm) { boolean isValidConfig = isValidKeyValuePair(decodedUrl); if (isValidConfig) { - String[] extraConfigs = decodedUrl.split("\\r?\\n"); + String[] extraConfigs = decodedUrl.split("\\r?\\n+"); int i = 1; String extraConfigKey = ApiConstants.EXTRA_CONFIG + "-"; for (String cfg : extraConfigs) { @@ -6388,8 +6385,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir // validate config against denied cfg commands validateKvmExtraConfig(decodedUrl, vm.getAccountId()); String[] extraConfigs = decodedUrl.split("\n\n"); + int i = 1; for (String cfg : extraConfigs) { - int i = 1; String[] cfgParts = cfg.split("\n"); String extraConfigKey = ApiConstants.EXTRA_CONFIG; String extraConfigValue; diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 79ea9bb9d8f..d18c0945d3b 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -975,6 +975,8 @@ "label.externalid": "External Id", "label.externalloadbalanceripaddress": "External load balancer IP address.", "label.extra": "Extra arguments", +"label.extraconfig": "Additional Configuration", +"label.extraconfig.tooltip": "Additional configuration parameters (extraconfig) to pass to the instance in plain text", "label.f5": "F5", "label.f5.ip.loadbalancer": "F5 BIG-IP load balancer.", "label.failed": "Failed", diff --git a/ui/src/views/compute/DeployVM.vue b/ui/src/views/compute/DeployVM.vue index a604fe68fe4..82a54ed8314 100644 --- a/ui/src/views/compute/DeployVM.vue +++ b/ui/src/views/compute/DeployVM.vue @@ -724,6 +724,12 @@ + + + + 0) { deployVmData.userdata = this.$toBase64AndURIEncoded(values.userdata) } + if (values.extraconfig && values.extraconfig.length > 0) { + deployVmData.extraconfig = encodeURIComponent(values.extraconfig) + } // step 2: select template/iso if (this.tabKey === 'templateid') { deployVmData.templateid = values.templateid diff --git a/ui/src/views/compute/EditVM.vue b/ui/src/views/compute/EditVM.vue index d5e75fcc658..9e60175f2a9 100644 --- a/ui/src/views/compute/EditVM.vue +++ b/ui/src/views/compute/EditVM.vue @@ -91,6 +91,12 @@ + + + + key.startsWith('extraconfig-')) + .map(key => this.resource.details[key] || '') + .filter(val => val.trim()) + return configs.join('\n\n') + } + }, beforeCreate () { this.apiParams = this.$getApiParams('updateVirtualMachine') }, @@ -185,7 +204,8 @@ export default { deleteprotection: this.resource.deleteprotection, group: this.resource.group, userdata: '', - haenable: this.resource.haenable + haenable: this.resource.haenable, + extraconfig: this.combinedExtraConfig }) this.rules = reactive({}) }, @@ -342,6 +362,9 @@ export default { if (values.userdata && values.userdata.length > 0) { params.userdata = this.$toBase64AndURIEncoded(values.userdata) } + if (values.extraconfig && values.extraconfig.length > 0) { + params.extraconfig = encodeURIComponent(values.extraconfig) + } this.loading = true api('updateVirtualMachine', {}, 'POST', params).then(json => { From d60f455b00e9c1ff6448fa9b8c1b8ad6e5061ac2 Mon Sep 17 00:00:00 2001 From: dk-blackfuel Date: Tue, 30 Sep 2025 15:04:58 +0200 Subject: [PATCH 005/311] Fix detection of Mi3xx GPUs (#11715) --- scripts/vm/hypervisor/kvm/gpudiscovery.sh | 37 +++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/scripts/vm/hypervisor/kvm/gpudiscovery.sh b/scripts/vm/hypervisor/kvm/gpudiscovery.sh index d27f6daf8c5..bf6892c898d 100755 --- a/scripts/vm/hypervisor/kvm/gpudiscovery.sh +++ b/scripts/vm/hypervisor/kvm/gpudiscovery.sh @@ -324,7 +324,40 @@ # "used_by_vm": null # } # ] -# } +# }, +# { +# "pci_address":"05:00.0", +# "vendor_id":"1002", +# "device_id":"74a5", +# "vendor":"Advanced Micro Devices, Inc. [AMD/ATI]", +# "device":"Aqua Vanjaram [Instinct MI325X]", +# "driver":"amdgpu", +# "pci_class":"Processing accelerators [1200]", +# "iommu_group":"null", +# "pci_root":"0000:05:00.0", +# "numa_node":-1, +# "sriov_totalvfs":0, +# "sriov_numvfs":0, +# "max_instances":null, +# "video_ram":null, +# "max_heads":null, +# "max_resolution_x":null, +# "max_resolution_y":null, +# +# "full_passthrough": { +# "enabled":1, +# "libvirt_address": { +# "domain":"0x0000", +# "bus":"0x05", +# "slot":"0x00", +# "function":"0x0" +# }, +# "used_by_vm":null +# }, + +# "vgpu_instances":[], +# "vf_instances":[] +# } # ] # } # @@ -716,7 +749,7 @@ for LINE in "${LINES[@]}"; do fi # Only process GPU classes (3D controller) - if [[ ! "$PCI_CLASS" =~ (3D\ controller) ]]; then + if [[ ! "$PCI_CLASS" =~ (3D\ controller|Processing\ accelerators) ]]; then continue fi From c631d6a480dfc89475c14bead84ae309c78d3d81 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Wed, 1 Oct 2025 08:47:58 +0200 Subject: [PATCH 006/311] CKS: generate a random UUID as password of CKS user in project (#11639) --- .../kubernetes/cluster/KubernetesClusterManagerImpl.java | 2 +- server/src/main/java/com/cloud/user/AccountManagerImpl.java | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java index 9b3e487680d..5a171296826 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java @@ -1551,7 +1551,7 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne try { Role role = getProjectKubernetesAccountRole(); UserAccount userAccount = accountService.createUserAccount(accountName, - UuidUtils.first(UUID.randomUUID().toString()), PROJECT_KUBERNETES_ACCOUNT_FIRST_NAME, + UUID.randomUUID().toString(), PROJECT_KUBERNETES_ACCOUNT_FIRST_NAME, PROJECT_KUBERNETES_ACCOUNT_LAST_NAME, null, null, accountName, Account.Type.NORMAL, role.getId(), project.getDomainId(), null, null, null, null, User.Source.NATIVE); projectManager.assignAccountToProject(project, userAccount.getAccountId(), ProjectAccount.Role.Regular, diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 04a64fbfc8c..2f6392ffaad 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -2747,7 +2747,10 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M logger.debug("Creating user: " + userName + ", accountId: " + accountId + " timezone:" + timezone); } - passwordPolicy.verifyIfPasswordCompliesWithPasswordPolicies(password, userName, getAccount(accountId).getDomainId()); + Account callingAccount = getCurrentCallingAccount(); + if (callingAccount.getId() != Account.ACCOUNT_ID_SYSTEM) { + passwordPolicy.verifyIfPasswordCompliesWithPasswordPolicies(password, userName, getAccount(accountId).getDomainId()); + } String encodedPassword = null; for (UserAuthenticator authenticator : _userPasswordEncoders) { From 2a802a314356d53d14c5886c5afcfad986b31a74 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Wed, 1 Oct 2025 08:49:58 +0200 Subject: [PATCH 007/311] Extensions: use home directory of cloud user instead of /var/lib/cloudstack/management/ (#11732) --- .../external/provisioner/ExternalPathPayloadProvisioner.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisioner.java b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisioner.java index 6cec5181de6..046d0e2aa42 100644 --- a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisioner.java +++ b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisioner.java @@ -103,10 +103,11 @@ public class ExternalPathPayloadProvisioner extends ManagerBase implements Exter BASE_EXTERNAL_PROVISIONER_SCRIPTS_DIR + "/provisioner.sh"; private static final String PROPERTIES_FILE = "server.properties"; + private static final String EXTENSIONS = "extensions"; private static final String EXTENSIONS_DEPLOYMENT_MODE_NAME = "extensions.deployment.mode"; private static final String EXTENSIONS_DIRECTORY_PROD = "/usr/share/cloudstack-management/extensions"; - private static final String EXTENSIONS_DATA_DIRECTORY_PROD = "/var/lib/cloudstack/management/extensions"; - private static final String EXTENSIONS_DIRECTORY_DEV = "extensions"; + private static final String EXTENSIONS_DATA_DIRECTORY_PROD = System.getProperty("user.home") + File.separator + EXTENSIONS; + private static final String EXTENSIONS_DIRECTORY_DEV = EXTENSIONS; private static final String EXTENSIONS_DATA_DIRECTORY_DEV = "client/target/extensions-data"; @Inject From 7dd0d6e9377b5589f9a59f6d726dd28ad258c14b Mon Sep 17 00:00:00 2001 From: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> Date: Wed, 1 Oct 2025 18:00:44 +0530 Subject: [PATCH 008/311] add ConfigDrive to datasource_list in SharedfsVM (#11726) --- systemvm/debian/opt/cloud/bin/setup/sharedfsvm.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/systemvm/debian/opt/cloud/bin/setup/sharedfsvm.sh b/systemvm/debian/opt/cloud/bin/setup/sharedfsvm.sh index 4908f8c1eec..1b1bb273753 100755 --- a/systemvm/debian/opt/cloud/bin/setup/sharedfsvm.sh +++ b/systemvm/debian/opt/cloud/bin/setup/sharedfsvm.sh @@ -49,6 +49,12 @@ setup_sharedfsvm() { rm -f /etc/logrotate.d/cloud + # Enable cloud-init without any aid from ds-identify + echo "policy: enabled" > /etc/cloud/ds-identify.cfg + + # Add ConfigDrive to datasource_list + sed -i "s/datasource_list: .*/datasource_list: ['ConfigDrive', 'CloudStack']/g" /etc/cloud/cloud.cfg.d/cloudstack.cfg + log_it "Starting cloud-init services" if [ -f /home/cloud/success ]; then systemctl stop cloud-init cloud-config cloud-final From cd12fa584825889692d88d909132d684b6c6c778 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Wed, 1 Oct 2025 08:43:22 -0400 Subject: [PATCH 009/311] Add UUID field for LDAP configuration (#11462) * Add UUID field for LDAP configuration * move db changes to the lastest schema file * Add ID param to list ldapConf API & delete ldapConf API * fix ui test * fix 1 ui test * fix test * fix api description --------- Co-authored-by: dahn --- .../META-INF/db/schema-42100to42200.sql | 6 ++++++ .../command/LdapDeleteConfigurationCmd.java | 8 +++++++- .../api/command/LdapListConfigurationCmd.java | 7 +++++++ .../response/LdapConfigurationResponse.java | 19 +++++++++++++++--- .../cloudstack/ldap/LdapConfigurationVO.java | 20 +++++++++++++++---- .../cloudstack/ldap/LdapManagerImpl.java | 19 ++++++++++++++++-- .../ldap/dao/LdapConfigurationDao.java | 2 +- .../ldap/dao/LdapConfigurationDaoImpl.java | 19 +++++++++++------- ui/src/config/section/config.js | 2 +- ui/src/views/AutogenView.vue | 5 ----- ui/tests/unit/views/AutogenView.spec.js | 12 +++++------ 11 files changed, 89 insertions(+), 30 deletions(-) diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql b/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql index 4fcb2b75de5..62ae10b7cc9 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql @@ -26,5 +26,11 @@ CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('router_health_check', 'check_result', ' -- Increase length of scripts_version column to 128 due to md5sum to sha512sum change CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('cloud.domain_router', 'scripts_version', 'scripts_version', 'VARCHAR(128)'); +-- Add uuid column to ldap_configuration table +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.ldap_configuration', 'uuid', 'VARCHAR(40) NOT NULL'); + +-- Populate uuid for existing rows where uuid is NULL or empty +UPDATE `cloud`.`ldap_configuration` SET uuid = UUID() WHERE uuid IS NULL OR uuid = ''; + -- Add the column cross_zone_instance_creation to cloud.backup_repository. if enabled it means that new Instance can be created on all Zones from Backups on this Repository. CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backup_repository', 'cross_zone_instance_creation', 'TINYINT(1) DEFAULT NULL COMMENT ''Backup Repository can be used for disaster recovery on another zone'''); diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapDeleteConfigurationCmd.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapDeleteConfigurationCmd.java index 15e6c836d0d..0ce7daff432 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapDeleteConfigurationCmd.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapDeleteConfigurationCmd.java @@ -40,8 +40,10 @@ public class LdapDeleteConfigurationCmd extends BaseCmd { @Inject private LdapManager _ldapManager; + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, required = false, entityType = LdapConfigurationResponse.class, description = "ID of the LDAP configuration") + private Long id; - @Parameter(name = ApiConstants.HOST_NAME, type = CommandType.STRING, required = true, description = "Hostname") + @Parameter(name = ApiConstants.HOST_NAME, type = CommandType.STRING, description = "Hostname") private String hostname; @Parameter(name = ApiConstants.PORT, type = CommandType.INTEGER, required = false, description = "port") @@ -71,6 +73,10 @@ public class LdapDeleteConfigurationCmd extends BaseCmd { return domainId; } + public Long getId() { + return id; + } + @Override public void execute() throws ServerApiException { try { diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapListConfigurationCmd.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapListConfigurationCmd.java index c34d026f89b..c950554b0d1 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapListConfigurationCmd.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapListConfigurationCmd.java @@ -53,6 +53,9 @@ public class LdapListConfigurationCmd extends BaseListCmd { @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = false, entityType = DomainResponse.class, description = "linked domain") private Long domainId; + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = LdapConfigurationResponse.class, description = "list ldap configuration by ID; when passed, all other parameters are ignored") + private Long id; + @Parameter(name = ApiConstants.LIST_ALL, type = CommandType.BOOLEAN, description = "If set to true, " + " and no domainid specified, list all LDAP configurations irrespective of the linked domain", since = "4.13.2") private Boolean listAll; @@ -120,6 +123,10 @@ public class LdapListConfigurationCmd extends BaseListCmd { this.domainId = domainId; } + public Long getId() { + return id; + } + public boolean listAll() { return listAll != null && listAll; } diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/response/LdapConfigurationResponse.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/response/LdapConfigurationResponse.java index 744c73d8e77..4c24b366bb0 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/response/LdapConfigurationResponse.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/response/LdapConfigurationResponse.java @@ -23,10 +23,14 @@ import org.apache.cloudstack.api.BaseResponse; import com.cloud.serializer.Param; import org.apache.cloudstack.api.EntityReference; -import org.apache.cloudstack.ldap.LdapConfiguration; +import org.apache.cloudstack.ldap.LdapConfigurationVO; -@EntityReference(value = LdapConfiguration.class) +@EntityReference(value = LdapConfigurationVO.class) public class LdapConfigurationResponse extends BaseResponse { + @SerializedName("id") + @Param(description = "the ID of the LDAP configuration") + private String id; + @SerializedName(ApiConstants.HOST_NAME) @Param(description = "name of the host running the ldap server") private String hostname; @@ -53,9 +57,18 @@ public class LdapConfigurationResponse extends BaseResponse { setPort(port); } - public LdapConfigurationResponse(final String hostname, final int port, final String domainId) { + public LdapConfigurationResponse(final String hostname, final int port, final String domainId, final String id) { this(hostname, port); setDomainId(domainId); + setId(id); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; } public String getHostname() { diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapConfigurationVO.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapConfigurationVO.java index ee9f0930c47..7e51fe352d9 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapConfigurationVO.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapConfigurationVO.java @@ -23,19 +23,25 @@ import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; +import org.apache.cloudstack.api.Identity; import org.apache.cloudstack.api.InternalIdentity; +import java.util.UUID; + @Entity @Table(name = "ldap_configuration") -public class LdapConfigurationVO implements InternalIdentity { - @Column(name = "hostname") - private String hostname; - +public class LdapConfigurationVO implements Identity, InternalIdentity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; + @Column(name = "hostname") + private String hostname; + + @Column(name = "uuid") + private String uuid; + @Column(name = "port") private int port; @@ -43,12 +49,14 @@ public class LdapConfigurationVO implements InternalIdentity { private Long domainId; public LdapConfigurationVO() { + this.uuid = UUID.randomUUID().toString(); } public LdapConfigurationVO(final String hostname, final int port, final Long domainId) { this.hostname = hostname; this.port = port; this.domainId = domainId; + this.uuid = UUID.randomUUID().toString(); } public String getHostname() { @@ -60,6 +68,10 @@ public class LdapConfigurationVO implements InternalIdentity { return id; } + public String getUuid() { + return uuid; + } + public int getPort() { return port; } diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapManagerImpl.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapManagerImpl.java index 05b8578bb42..abf47d4094e 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapManagerImpl.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapManagerImpl.java @@ -52,6 +52,7 @@ import org.apache.cloudstack.framework.messagebus.MessageSubscriber; import org.apache.cloudstack.ldap.dao.LdapConfigurationDao; import org.apache.cloudstack.ldap.dao.LdapTrustMapDao; import org.apache.commons.lang.Validate; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import com.cloud.domain.DomainVO; @@ -240,7 +241,7 @@ public class LdapManagerImpl extends ComponentLifecycleBase implements LdapManag domainUuid = domain.getUuid(); } } - return new LdapConfigurationResponse(configuration.getHostname(), configuration.getPort(), domainUuid); + return new LdapConfigurationResponse(configuration.getHostname(), configuration.getPort(), domainUuid, configuration.getUuid()); } @Override @@ -257,6 +258,19 @@ public class LdapManagerImpl extends ComponentLifecycleBase implements LdapManag @Override public LdapConfigurationResponse deleteConfiguration(final LdapDeleteConfigurationCmd cmd) throws InvalidParameterValueException { + Long id = cmd.getId(); + String hostname = cmd.getHostname(); + if (id == null && StringUtils.isEmpty(hostname)) { + throw new InvalidParameterValueException("Either id or hostname must be specified"); + } + if (id != null) { + final LdapConfigurationVO config = _ldapConfigurationDao.findById(cmd.getId()); + if (config != null) { + _ldapConfigurationDao.remove(config.getId()); + return createLdapConfigurationResponse(config); + } + throw new InvalidParameterValueException("Cannot find configuration with id " + id); + } return deleteConfigurationInternal(cmd.getHostname(), cmd.getPort(), cmd.getDomainId()); } @@ -377,7 +391,8 @@ public class LdapManagerImpl extends ComponentLifecycleBase implements LdapManag final int port = cmd.getPort(); final Long domainId = cmd.getDomainId(); final boolean listAll = cmd.listAll(); - final Pair, Integer> result = _ldapConfigurationDao.searchConfigurations(hostname, port, domainId, listAll); + final Long id = cmd.getId(); + final Pair, Integer> result = _ldapConfigurationDao.searchConfigurations(id, hostname, port, domainId, listAll); return new Pair, Integer>(result.first(), result.second()); } diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/dao/LdapConfigurationDao.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/dao/LdapConfigurationDao.java index af774b685ed..889efa3ef90 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/dao/LdapConfigurationDao.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/dao/LdapConfigurationDao.java @@ -41,5 +41,5 @@ public interface LdapConfigurationDao extends GenericDao, Integer> searchConfigurations(String hostname, int port, Long domainId); - Pair, Integer> searchConfigurations(String hostname, int port, Long domainId, boolean listAll); + Pair, Integer> searchConfigurations(Long id, String hostname, int port, Long domainId, boolean listAll); } diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/dao/LdapConfigurationDaoImpl.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/dao/LdapConfigurationDaoImpl.java index c053e87b6bf..67d09feed90 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/dao/LdapConfigurationDaoImpl.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/dao/LdapConfigurationDaoImpl.java @@ -48,6 +48,7 @@ public class LdapConfigurationDaoImpl extends GenericDaoBase sc = getSearchCriteria(hostname, port, domainId, false); + SearchCriteria sc = getSearchCriteria(null, hostname, port, domainId, false); return findOneBy(sc); } @Override public LdapConfigurationVO find(String hostname, int port, Long domainId, boolean listAll) { - SearchCriteria sc = getSearchCriteria(hostname, port, domainId, listAll); + SearchCriteria sc = getSearchCriteria(null, hostname, port, domainId, listAll); return findOneBy(sc); } @Override public Pair, Integer> searchConfigurations(final String hostname, final int port, final Long domainId) { - SearchCriteria sc = getSearchCriteria(hostname, port, domainId, false); + SearchCriteria sc = getSearchCriteria(null, hostname, port, domainId, false); return searchAndCount(sc, null); } @Override - public Pair, Integer> searchConfigurations(final String hostname, final int port, final Long domainId, final boolean listAll) { - SearchCriteria sc = getSearchCriteria(hostname, port, domainId, listAll); + public Pair, Integer> searchConfigurations(final Long id, final String hostname, final int port, final Long domainId, final boolean listAll) { + SearchCriteria sc = getSearchCriteria(id, hostname, port, domainId, listAll); return searchAndCount(sc, null); } - private SearchCriteria getSearchCriteria(String hostname, int port, Long domainId,boolean listAll) { + private SearchCriteria getSearchCriteria(Long id, String hostname, int port, Long domainId,boolean listAll) { SearchCriteria sc; - if (domainId != null) { + if (id != null) { + // If id is present, ignore all other parameters + sc = listDomainConfigurationsSearch.create(); + sc.setParameters("id", id); + } else if (domainId != null) { // If domainid is present, ignore listall sc = listDomainConfigurationsSearch.create(); sc.setParameters("domain_id", domainId); diff --git a/ui/src/config/section/config.js b/ui/src/config/section/config.js index e69f6d1e6c4..1961186e0bb 100644 --- a/ui/src/config/section/config.js +++ b/ui/src/config/section/config.js @@ -41,7 +41,7 @@ export default { permission: ['listLdapConfigurations'], searchFilters: ['domainid', 'hostname', 'port'], columns: ['hostname', 'port', 'domainid'], - details: ['hostname', 'port', 'domainid'], + details: ['id', 'hostname', 'port', 'domainid'], actions: [ { api: 'addLdapConfiguration', diff --git a/ui/src/views/AutogenView.vue b/ui/src/views/AutogenView.vue index f55767ded2a..765515033ef 100644 --- a/ui/src/views/AutogenView.vue +++ b/ui/src/views/AutogenView.vue @@ -1078,8 +1078,6 @@ export default { } if (this.$route.path.startsWith('/vmsnapshot/')) { params.vmsnapshotid = this.$route.params.id - } else if (this.$route.path.startsWith('/ldapsetting/')) { - params.hostname = this.$route.params.id } if (this.$route.path.startsWith('/tungstenpolicy/')) { params.policyuuid = this.$route.params.id @@ -1192,9 +1190,6 @@ export default { this.items[idx][key] = func(this.items[idx]) } } - if (this.$route.path.startsWith('/ldapsetting')) { - this.items[idx].id = this.items[idx].hostname - } } if (this.items.length > 0) { if (!this.showAction || this.dataView) { diff --git a/ui/tests/unit/views/AutogenView.spec.js b/ui/tests/unit/views/AutogenView.spec.js index 6b1085637b3..16e8ae219e3 100644 --- a/ui/tests/unit/views/AutogenView.spec.js +++ b/ui/tests/unit/views/AutogenView.spec.js @@ -652,12 +652,12 @@ describe('Views > AutogenView.vue', () => { testapinamecase1response: { count: 0, testapinamecase1: [{ - id: 'test-id-1', + id: 'uuid1', name: 'test-name-1' }] } }) - await router.push({ name: 'testRouter13', params: { id: 'test-id' } }) + await router.push({ name: 'testRouter13', params: { id: 'uuid1' } }) await flushPromises() expect(mockAxios).toHaveBeenCalled() @@ -668,8 +668,7 @@ describe('Views > AutogenView.vue', () => { command: 'testApiNameCase1', response: 'json', listall: true, - id: 'test-id', - hostname: 'test-id', + id: 'uuid1', page: 1, pagesize: 20 }) @@ -777,6 +776,7 @@ describe('Views > AutogenView.vue', () => { testapinamecase1response: { count: 1, testapinamecase1: [{ + id: 'uuid1', name: 'test-name-value', hostname: 'test-hostname-value' }] @@ -786,13 +786,13 @@ describe('Views > AutogenView.vue', () => { await flushPromises() expect(wrapper.vm.items).toEqual([{ - id: 'test-hostname-value', + id: 'uuid1', name: 'test-name-value', hostname: 'test-hostname-value', key: 0 }]) expect(wrapper.vm.resource).toEqual({ - id: 'test-hostname-value', + id: 'uuid1', name: 'test-name-value', hostname: 'test-hostname-value', key: 0 From ca7138b3bdf73dafce147d0a730d41db621a7e9b Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Thu, 2 Oct 2025 11:43:48 +0530 Subject: [PATCH 010/311] server: Consider Instance in Starting state as well for allocation algorithm (#11751) * Consider Instance in Starting state as well for allocation algorithm * use IN instead of OR statement --- .../src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java index ef10af63bae..e6405fd34db 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java @@ -116,17 +116,17 @@ public class VMInstanceDaoImpl extends GenericDaoBase implem protected Attribute _updateTimeAttr; - private static final String ORDER_CLUSTERS_NUMBER_OF_VMS_FOR_ACCOUNT_PART1 = "SELECT host.cluster_id, SUM(IF(vm.state='Running' AND vm.account_id = ?, 1, 0)) " + + private static final String ORDER_CLUSTERS_NUMBER_OF_VMS_FOR_ACCOUNT_PART1 = "SELECT host.cluster_id, SUM(IF(vm.state IN ('Running', 'Starting') AND vm.account_id = ?, 1, 0)) " + "FROM `cloud`.`host` host LEFT JOIN `cloud`.`vm_instance` vm ON host.id = vm.host_id WHERE "; private static final String ORDER_CLUSTERS_NUMBER_OF_VMS_FOR_ACCOUNT_PART2 = " AND host.type = 'Routing' AND host.removed is null GROUP BY host.cluster_id " + "ORDER BY 2 ASC "; - private static final String ORDER_PODS_NUMBER_OF_VMS_FOR_ACCOUNT = "SELECT pod.id, SUM(IF(vm.state='Running' AND vm.account_id = ?, 1, 0)) FROM `cloud`.`" + + private static final String ORDER_PODS_NUMBER_OF_VMS_FOR_ACCOUNT = "SELECT pod.id, SUM(IF(vm.state IN ('Running', 'Starting') AND vm.account_id = ?, 1, 0)) FROM `cloud`.`" + "host_pod_ref` pod LEFT JOIN `cloud`.`vm_instance` vm ON pod.id = vm.pod_id WHERE pod.data_center_id = ? AND pod.removed is null " + " GROUP BY pod.id ORDER BY 2 ASC "; private static final String ORDER_HOSTS_NUMBER_OF_VMS_FOR_ACCOUNT = - "SELECT host.id, SUM(IF(vm.state='Running' AND vm.account_id = ?, 1, 0)) FROM `cloud`.`host` host LEFT JOIN `cloud`.`vm_instance` vm ON host.id = vm.host_id " + + "SELECT host.id, SUM(IF(vm.state IN ('Running', 'Starting') AND vm.account_id = ?, 1, 0)) FROM `cloud`.`host` host LEFT JOIN `cloud`.`vm_instance` vm ON host.id = vm.host_id " + "WHERE host.data_center_id = ? AND host.type = 'Routing' AND host.removed is null "; private static final String ORDER_HOSTS_NUMBER_OF_VMS_FOR_ACCOUNT_PART2 = " GROUP BY host.id ORDER BY 2 ASC "; From 1efa46cb4d1d9571b2e3138be0565ea25a70787d Mon Sep 17 00:00:00 2001 From: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:15:54 +0530 Subject: [PATCH 011/311] fix removeUsage for backups (#11522) --- .../main/java/com/cloud/usage/dao/UsageBackupDaoImpl.java | 2 +- .../org/apache/cloudstack/backup/BackupManagerImpl.java | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/usage/dao/UsageBackupDaoImpl.java b/engine/schema/src/main/java/com/cloud/usage/dao/UsageBackupDaoImpl.java index e5b46b02a59..3f852b0cfb5 100644 --- a/engine/schema/src/main/java/com/cloud/usage/dao/UsageBackupDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/usage/dao/UsageBackupDaoImpl.java @@ -68,7 +68,7 @@ public class UsageBackupDaoImpl extends GenericDaoBase impl pstmt.setString(1, DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), eventDate)); pstmt.setLong(2, accountId); pstmt.setLong(3, vmId); - pstmt.setLong(3, backupOfferingId); + pstmt.setLong(4, backupOfferingId); pstmt.executeUpdate(); } } catch (SQLException e) { diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index bb3e3f2409f..aff838a3cdf 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -1927,7 +1927,11 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager { for (final VirtualMachine vm : vms) { Map> backupOfferingToSizeMap = new HashMap<>(); - for (final Backup backup: backupDao.listByVmId(null, vm.getId())) { + List backups = backupDao.listByVmId(null, vm.getId()); + if (backups.isEmpty() && vm.getBackupOfferingId() != null) { + backupOfferingToSizeMap.put(vm.getBackupOfferingId(), new Pair<>(0L, 0L)); + } + for (final Backup backup: backups) { Long backupSize = 0L; Long backupProtectedSize = 0L; if (Objects.nonNull(backup.getSize())) { From b09f3e8ff7e57868da9654998708ee7fc08a8502 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 3 Oct 2025 11:20:22 +0530 Subject: [PATCH 012/311] ui: fix overflow for value in DetailInput (#11771) In DetailInput component when a long value is used, it overflows on some browsers. Signed-off-by: Abhishek Kumar --- ui/src/components/widgets/DetailsInput.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/components/widgets/DetailsInput.vue b/ui/src/components/widgets/DetailsInput.vue index eeab0e3d528..a8d39fce02b 100644 --- a/ui/src/components/widgets/DetailsInput.vue +++ b/ui/src/components/widgets/DetailsInput.vue @@ -87,8 +87,8 @@ export default { data () { return { columns: [ - { title: this.$t('label.key'), dataIndex: 'key', key: 'key', width: '40%' }, - { title: this.$t('label.value'), dataIndex: 'value', key: 'value', width: '40%' }, + { title: this.$t('label.key'), dataIndex: 'key', key: 'key', width: '40%', ellipsis: true }, + { title: this.$t('label.value'), dataIndex: 'value', key: 'value', width: '40%', ellipsis: true }, { title: this.$t('label.actions'), key: 'actions', width: '20%' } ], newKey: '', From e12813de497e8e94907d602c30842b70841f3bb0 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Fri, 3 Oct 2025 11:05:43 +0200 Subject: [PATCH 013/311] CKS: fix CKS creation on an existing Shared and Routed network (#11735) --- .../cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java index 5a171296826..3b6052fb1b9 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java @@ -381,6 +381,9 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne logger.warn("Unable to find the network with ID: {} passed for the Kubernetes cluster", networkId); return false; } + if (isDirectAccess(network)) { + return true; + } networkOffering = networkOfferingDao.findById(network.getNetworkOfferingId()); if (networkOffering == null) { logger.warn("Unable to find the network offering of the network: {} ({}) to be used for provisioning Kubernetes cluster", network.getName(), network.getUuid()); From 5a8a1e27e100f3b807b4dcd0691959d3f8700c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20B=C3=B6ck?= <89930804+erikbocks@users.noreply.github.com> Date: Fri, 3 Oct 2025 07:42:36 -0300 Subject: [PATCH 014/311] Fixed and enhanced vlan field validation in the UI (#10983) --- .../infra/zone/AdvancedGuestTrafficForm.vue | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/ui/src/views/infra/zone/AdvancedGuestTrafficForm.vue b/ui/src/views/infra/zone/AdvancedGuestTrafficForm.vue index 4c0d2c137e6..caf690efcc9 100644 --- a/ui/src/views/infra/zone/AdvancedGuestTrafficForm.vue +++ b/ui/src/views/infra/zone/AdvancedGuestTrafficForm.vue @@ -39,6 +39,7 @@ { const values = toRaw(this.form) - if (!values.vlanRangeStart || (values.vlanRangeEnd && !this.checkFromTo(values.vlanRangeStart, values.vlanRangeEnd))) { + if (!this.checkFromTo(values.vlanRangeStart, values.vlanRangeEnd)) { this.validStatus = 'error' this.validMessage = this.$t('message.error.vlan.range') return @@ -185,19 +207,30 @@ export default { toVal = value fromVal = this.form[rule.compare] } - if (fromVal && toVal && !this.checkFromTo(fromVal, toVal)) { + if (!this.checkFromTo(fromVal, toVal)) { this.validStatus = 'error' this.validMessage = this.$t('message.error.vlan.range') } return Promise.resolve() }, checkFromTo (fromVal, toVal) { - if (!fromVal) fromVal = 0 - if (!toVal) toVal = 0 - if (fromVal > toVal) { - return false + const vlanRange = this.rangeLimits[this.isolationMethod] ? this.rangeLimits[this.isolationMethod] : this.rangeLimits.VLAN + switch (true) { + case ((fromVal === null) || (toVal === null)): + case fromVal === toVal: + case fromVal > toVal: + case toVal > vlanRange.max: + case fromVal < vlanRange.min: + return false + default: + this.validStatus = 'success' + this.validMessage = '' + return true } - return true + }, + getIsolationMethod () { + const phyNetworks = this.getPrefilled('physicalNetworks') + this.isolationMethod = phyNetworks[phyNetworks.length - 1].isolationMethod } } } From 8e4dc0a66d23e1d5d857cd7341003ac111e00a3a Mon Sep 17 00:00:00 2001 From: Alexandru Bagu Date: Sat, 4 Oct 2025 12:49:26 +0300 Subject: [PATCH 015/311] VMware: match nic mac for ip address fetch (#10641) --- .../vmware/resource/VmwareResource.java | 17 ++++++++++---- .../java/com/cloud/vm/UserVmManagerImpl.java | 22 ++++++++++--------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java index 481dbba922e..d8e7d170369 100644 --- a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java +++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java @@ -5837,11 +5837,20 @@ public class VmwareResource extends ServerResourceBase implements StoragePoolRes if (toolsStatus == VirtualMachineToolsStatus.TOOLS_NOT_INSTALLED) { details += "Vmware tools not installed."; } else { - ip = guestInfo.getIpAddress(); - if (ip != null) { - result = true; + var normalizedMac = cmd.getMacAddress().replaceAll("-", ":"); + for(var guestInfoNic : guestInfo.getNet()) { + var normalizedNicMac = guestInfoNic.getMacAddress().replaceAll("-", ":"); + if (!result && normalizedNicMac.equalsIgnoreCase(normalizedMac)) { + result = true; + details = null; + for (var ipAddr : guestInfoNic.getIpAddress()) { + if (NetUtils.isValidIp4(ipAddr) && (cmd.getVmNetworkCidr() == null || NetUtils.isIpWithInCidrRange(ipAddr, cmd.getVmNetworkCidr()))) { + details = ipAddr; + } + } + break; + } } - details = ip; } } else { details += "VM " + vmName + " no longer exists on vSphere host: " + hyperHost.getHyperHostName(); diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index a67484b6dd6..7cf722db797 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -753,7 +753,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir String networkCidr; String macAddress; - public VmIpAddrFetchThread(long vmId, long nicId, String instanceName, boolean windows, Long hostId, String networkCidr, String macAddress) { + public VmIpAddrFetchThread(long vmId, String vmUuid, long nicId, String instanceName, boolean windows, Long hostId, String networkCidr, String macAddress) { this.vmId = vmId; this.vmUuid = vmUuid; this.nicId = nicId; @@ -775,8 +775,13 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir Answer answer = _agentMgr.send(hostId, cmd); if (answer.getResult()) { String vmIp = answer.getDetails(); - - if (NetUtils.isValidIp4(vmIp)) { + if (vmIp == null) { + // we got a valid response and the NIC does not have an IP assigned, as such we will update the database with null + if (nic.getIPv4Address() != null) { + nic.setIPv4Address(null); + _nicDao.update(nicId, nic); + } + } else if (NetUtils.isValidIp4(vmIp)) { // set this vm ip addr in vm nic. if (nic != null) { nic.setIPv4Address(vmIp); @@ -791,12 +796,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } } } else { - //previously vm has ip and nic table has ip address. After vm restart or stop/start - //if vm doesnot get the ip then set the ip in nic table to null - if (nic.getIPv4Address() != null) { - nic.setIPv4Address(null); - _nicDao.update(nicId, nic); - } + // since no changes are being done, we should not decrement IP usage + decrementCount = false; if (answer.getDetails() != null) { logger.debug("Failed to get vm ip for Vm [id: {}, uuid: {}, name: {}], details: {}", vmId, vmUuid, vmName, answer.getDetails()); @@ -2693,7 +2694,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir VirtualMachineProfile vmProfile = new VirtualMachineProfileImpl(userVm); VirtualMachine vm = vmProfile.getVirtualMachine(); boolean isWindows = _guestOSCategoryDao.findById(_guestOSDao.findById(vm.getGuestOSId()).getCategoryId()).getName().equalsIgnoreCase("Windows"); - _vmIpFetchThreadExecutor.execute(new VmIpAddrFetchThread(vmId, nicId, vmInstance.getInstanceName(), + + _vmIpFetchThreadExecutor.execute(new VmIpAddrFetchThread(vmId, vmInstance.getUuid(), nicId, vmInstance.getInstanceName(), isWindows, vm.getHostId(), network.getCidr(), nicVo.getMacAddress())); } From a208db54ea84a8e3eda406990671c49efc90ef42 Mon Sep 17 00:00:00 2001 From: Rene Peinthor Date: Mon, 6 Oct 2025 09:10:53 +0200 Subject: [PATCH 016/311] linstor: use sparse/discard qemu-img convert on thin devices (#11787) --- plugins/storage/volume/linstor/CHANGELOG.md | 6 +++ ...torRevertBackupSnapshotCommandWrapper.java | 21 ++++++++-- .../kvm/storage/LinstorStorageAdaptor.java | 38 +------------------ .../storage/datastore/util/LinstorUtil.java | 34 +++++++++++++++++ 4 files changed, 59 insertions(+), 40 deletions(-) diff --git a/plugins/storage/volume/linstor/CHANGELOG.md b/plugins/storage/volume/linstor/CHANGELOG.md index c0991a9aa2b..7da3516955d 100644 --- a/plugins/storage/volume/linstor/CHANGELOG.md +++ b/plugins/storage/volume/linstor/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Linstor CloudStack plugin will be documented in this file The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2025-10-03] + +### Changed + +- Revert qcow2 snapshot now use sparse/discard options to convert on thin devices. + ## [2025-08-05] ### Fixed diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java index 511b5a40ca8..98b8bf0bb78 100644 --- a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java +++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java @@ -26,13 +26,16 @@ import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; import com.cloud.storage.Storage; +import com.cloud.utils.script.Script; import org.apache.cloudstack.storage.command.CopyCmdAnswer; +import org.apache.cloudstack.storage.datastore.util.LinstorUtil; import org.apache.cloudstack.storage.to.SnapshotObjectTO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImgException; import org.apache.cloudstack.utils.qemu.QemuImgFile; import org.apache.log4j.Logger; +import org.joda.time.Duration; import org.libvirt.LibvirtException; @ResourceWrapper(handles = LinstorRevertBackupSnapshotCommand.class) @@ -41,12 +44,23 @@ public final class LinstorRevertBackupSnapshotCommandWrapper { private static final Logger s_logger = Logger.getLogger(LinstorRevertBackupSnapshotCommandWrapper.class); - private void convertQCow2ToRAW(final String srcPath, final String dstPath, int waitMilliSeconds) + private void convertQCow2ToRAW( + KVMStoragePool pool, final String srcPath, final String dstUuid, int waitMilliSeconds) throws LibvirtException, QemuImgException { + final String dstPath = pool.getPhysicalDisk(dstUuid).getPath(); final QemuImgFile srcQemuFile = new QemuImgFile( srcPath, QemuImg.PhysicalDiskFormat.QCOW2); - final QemuImg qemu = new QemuImg(waitMilliSeconds); + boolean zeroedDevice = LinstorUtil.resourceSupportZeroBlocks(pool, LinstorUtil.RSC_PREFIX + dstUuid); + if (zeroedDevice) + { + // blockdiscard the device to ensure the device is filled with zeroes + Script blkDiscardScript = new Script("blkdiscard", Duration.millis(waitMilliSeconds)); + blkDiscardScript.add("-f"); + blkDiscardScript.add(dstPath); + blkDiscardScript.execute(); + } + final QemuImg qemu = new QemuImg(waitMilliSeconds, zeroedDevice, true); final QemuImgFile dstFile = new QemuImgFile(dstPath, QemuImg.PhysicalDiskFormat.RAW); qemu.convert(srcQemuFile, dstFile); } @@ -73,8 +87,9 @@ public final class LinstorRevertBackupSnapshotCommandWrapper srcDataStore.getUrl() + File.separator + srcFile.getParent()); convertQCow2ToRAW( + linstorPool, secondaryPool.getLocalPath() + File.separator + srcFile.getName(), - linstorPool.getPhysicalDisk(dst.getPath()).getPath(), + dst.getPath(), cmd.getWaitInMillSeconds()); final VolumeObjectTO dstVolume = new VolumeObjectTO(); diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java index 4210008f1c0..c269878c808 100644 --- a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java +++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java @@ -30,7 +30,6 @@ import javax.annotation.Nonnull; import com.cloud.storage.Storage; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.script.Script; - import org.apache.cloudstack.storage.datastore.util.LinstorUtil; import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImgException; @@ -56,7 +55,6 @@ import com.linbit.linstor.api.model.ResourceGroupSpawn; import com.linbit.linstor.api.model.ResourceMakeAvailable; import com.linbit.linstor.api.model.ResourceWithVolumes; import com.linbit.linstor.api.model.StoragePool; -import com.linbit.linstor.api.model.Volume; import com.linbit.linstor.api.model.VolumeDefinition; import java.io.File; @@ -570,40 +568,6 @@ public class LinstorStorageAdaptor implements StorageAdaptor { return copyPhysicalDisk(disk, name, destPool, timeout, null, null, null); } - /** - * Checks if all diskful resource are on a zeroed block device. - * @param destPool Linstor pool to use - * @param resName Linstor resource name - * @return true if all resources are on a provider with zeroed blocks. - */ - private boolean resourceSupportZeroBlocks(KVMStoragePool destPool, String resName) { - final DevelopersApi api = getLinstorAPI(destPool); - - try { - List resWithVols = api.viewResources( - Collections.emptyList(), - Collections.singletonList(resName), - Collections.emptyList(), - Collections.emptyList(), - null, - null); - - if (resWithVols != null) { - return resWithVols.stream() - .allMatch(res -> { - Volume vol0 = res.getVolumes().get(0); - return vol0 != null && (vol0.getProviderKind() == ProviderKind.LVM_THIN || - vol0.getProviderKind() == ProviderKind.ZFS || - vol0.getProviderKind() == ProviderKind.ZFS_THIN || - vol0.getProviderKind() == ProviderKind.DISKLESS); - } ); - } - } catch (ApiException apiExc) { - s_logger.error(apiExc.getMessage()); - } - return false; - } - /** * Checks if the given disk is the SystemVM template, by checking its properties file in the same directory. * The initial systemvm template resource isn't created on the management server, but @@ -674,7 +638,7 @@ public class LinstorStorageAdaptor implements StorageAdaptor { destFile.setFormat(dstDisk.getFormat()); destFile.setSize(disk.getVirtualSize()); - boolean zeroedDevice = resourceSupportZeroBlocks(destPools, getLinstorRscName(name)); + boolean zeroedDevice = LinstorUtil.resourceSupportZeroBlocks(destPools, getLinstorRscName(name)); try { final QemuImg qemu = new QemuImg(timeout, zeroedDevice, true); qemu.convert(srcFile, destFile); diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java index 60d06590006..9a6151efafc 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java @@ -42,6 +42,7 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; import org.apache.log4j.Logger; @@ -430,4 +431,37 @@ public class LinstorUtil { public static boolean isRscDiskless(ResourceWithVolumes rsc) { return rsc.getFlags() != null && rsc.getFlags().contains(ApiConsts.FLAG_DISKLESS); } + + /** + * Checks if all diskful resource are on a zeroed block device. + * @param pool Linstor pool to use + * @param resName Linstor resource name + * @return true if all resources are on a provider with zeroed blocks. + */ + public static boolean resourceSupportZeroBlocks(KVMStoragePool pool, String resName) { + final DevelopersApi api = getLinstorAPI(pool.getSourceHost()); + try { + List resWithVols = api.viewResources( + Collections.emptyList(), + Collections.singletonList(resName), + Collections.emptyList(), + Collections.emptyList(), + null, + null); + + if (resWithVols != null) { + return resWithVols.stream() + .allMatch(res -> { + Volume vol0 = res.getVolumes().get(0); + return vol0 != null && (vol0.getProviderKind() == ProviderKind.LVM_THIN || + vol0.getProviderKind() == ProviderKind.ZFS || + vol0.getProviderKind() == ProviderKind.ZFS_THIN || + vol0.getProviderKind() == ProviderKind.DISKLESS); + } ); + } + } catch (ApiException apiExc) { + s_logger.error(apiExc.getMessage()); + } + return false; + } } From b7a11cb203aeb9458460e3ce06090172b3a814d1 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Mon, 6 Oct 2025 12:43:28 +0530 Subject: [PATCH 017/311] NAS backup provider: Support restore from backup to volumes on Ceph storage pool(s), and take backup for stopped instances with volumes on Ceph storage pool(s) (#11684) Co-authored-by: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> --- .../cloudstack/backup/BackupManager.java | 2 +- .../backup/RestoreBackupCommand.java | 10 ++ .../cloudstack/backup/TakeBackupCommand.java | 10 ++ .../cloudstack/backup/NASBackupProvider.java | 63 +++++--- .../LibvirtRestoreBackupCommandWrapper.java | 142 +++++++++++++++--- .../LibvirtTakeBackupCommandWrapper.java | 26 +++- .../apache/cloudstack/utils/qemu/QemuImg.java | 7 + ...ibvirtRestoreBackupCommandWrapperTest.java | 27 ++++ scripts/vm/hypervisor/kvm/nasbackup.sh | 9 +- 9 files changed, 255 insertions(+), 41 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java index 37d21613c3d..f1f0c3c31ee 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java @@ -55,7 +55,7 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer ConfigKey BackupProviderPlugin = new ConfigKey<>("Advanced", String.class, "backup.framework.provider.plugin", "dummy", - "The backup and recovery provider plugin.", true, ConfigKey.Scope.Zone, BackupFrameworkEnabled.key()); + "The backup and recovery provider plugin. Valid plugin values: dummy, veeam, networker and nas", true, ConfigKey.Scope.Zone, BackupFrameworkEnabled.key()); ConfigKey BackupSyncPollingInterval = new ConfigKey<>("Advanced", Long.class, "backup.framework.sync.interval", diff --git a/core/src/main/java/org/apache/cloudstack/backup/RestoreBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/RestoreBackupCommand.java index 453b236df6b..9cbb87da19a 100644 --- a/core/src/main/java/org/apache/cloudstack/backup/RestoreBackupCommand.java +++ b/core/src/main/java/org/apache/cloudstack/backup/RestoreBackupCommand.java @@ -22,6 +22,7 @@ package org.apache.cloudstack.backup; import com.cloud.agent.api.Command; import com.cloud.agent.api.LogLevel; import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import java.util.List; @@ -31,6 +32,7 @@ public class RestoreBackupCommand extends Command { private String backupRepoType; private String backupRepoAddress; private List backupVolumesUUIDs; + private List restoreVolumePools; private List restoreVolumePaths; private String diskType; private Boolean vmExists; @@ -74,6 +76,14 @@ public class RestoreBackupCommand extends Command { this.backupRepoAddress = backupRepoAddress; } + public List getRestoreVolumePools() { + return restoreVolumePools; + } + + public void setRestoreVolumePools(List restoreVolumePools) { + this.restoreVolumePools = restoreVolumePools; + } + public List getRestoreVolumePaths() { return restoreVolumePaths; } diff --git a/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java index ecebd57a178..5402b6b2476 100644 --- a/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java +++ b/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java @@ -21,6 +21,7 @@ package org.apache.cloudstack.backup; import com.cloud.agent.api.Command; import com.cloud.agent.api.LogLevel; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import java.util.List; @@ -29,6 +30,7 @@ public class TakeBackupCommand extends Command { private String backupPath; private String backupRepoType; private String backupRepoAddress; + private List volumePools; private List volumePaths; private Boolean quiesce; @LogLevel(LogLevel.Log4jLevel.Off) @@ -80,6 +82,14 @@ public class TakeBackupCommand extends Command { this.mountOptions = mountOptions; } + public List getVolumePools() { + return volumePools; + } + + public void setVolumePools(List volumePools) { + this.volumePools = volumePools; + } + public List getVolumePaths() { return volumePaths; } diff --git a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java index 9cd2f20e386..3813cac0a33 100644 --- a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java +++ b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java @@ -26,9 +26,9 @@ import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.Hypervisor; import com.cloud.offering.DiskOffering; import com.cloud.resource.ResourceManager; +import com.cloud.storage.DataStoreRole; import com.cloud.storage.ScopeType; import com.cloud.storage.Storage; -import com.cloud.storage.StoragePoolHostVO; import com.cloud.storage.Volume; import com.cloud.storage.VolumeApiServiceImpl; import com.cloud.storage.VolumeVO; @@ -49,10 +49,13 @@ import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.backup.dao.BackupRepositoryDao; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; @@ -106,6 +109,9 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co @Inject private PrimaryDataStoreDao primaryDataStoreDao; + @Inject + DataStoreManager dataStoreMgr; + @Inject private AgentManager agentManager; @@ -203,8 +209,9 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co if (VirtualMachine.State.Stopped.equals(vm.getState())) { List vmVolumes = volumeDao.findByInstance(vm.getId()); vmVolumes.sort(Comparator.comparing(Volume::getDeviceId)); - List volumePaths = getVolumePaths(vmVolumes); - command.setVolumePaths(volumePaths); + Pair, List> volumePoolsAndPaths = getVolumePoolsAndPaths(vmVolumes); + command.setVolumePools(volumePoolsAndPaths.first()); + command.setVolumePaths(volumePoolsAndPaths.second()); } BackupAnswer answer; @@ -303,7 +310,9 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co restoreCommand.setMountOptions(backupRepository.getMountOptions()); restoreCommand.setVmName(vm.getName()); restoreCommand.setBackupVolumesUUIDs(backedVolumesUUIDs); - restoreCommand.setRestoreVolumePaths(getVolumePaths(restoreVolumes)); + Pair, List> volumePoolsAndPaths = getVolumePoolsAndPaths(restoreVolumes); + restoreCommand.setRestoreVolumePools(volumePoolsAndPaths.first()); + restoreCommand.setRestoreVolumePaths(volumePoolsAndPaths.second()); restoreCommand.setVmExists(vm.getRemoved() == null); restoreCommand.setVmState(vm.getState()); restoreCommand.setMountTimeout(NASBackupRestoreMountTimeout.value()); @@ -319,31 +328,42 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co return new Pair<>(answer.getResult(), answer.getDetails()); } - private List getVolumePaths(List volumes) { + private Pair, List> getVolumePoolsAndPaths(List volumes) { + List volumePools = new ArrayList<>(); List volumePaths = new ArrayList<>(); for (VolumeVO volume : volumes) { StoragePoolVO storagePool = primaryDataStoreDao.findById(volume.getPoolId()); if (Objects.isNull(storagePool)) { throw new CloudRuntimeException("Unable to find storage pool associated to the volume"); } - String volumePathPrefix; - if (ScopeType.HOST.equals(storagePool.getScope())) { - volumePathPrefix = storagePool.getPath(); - } else if (Storage.StoragePoolType.SharedMountPoint.equals(storagePool.getPoolType())) { - volumePathPrefix = storagePool.getPath(); - } else { - volumePathPrefix = String.format("/mnt/%s", storagePool.getUuid()); - } + + DataStore dataStore = dataStoreMgr.getDataStore(storagePool.getId(), DataStoreRole.Primary); + volumePools.add(dataStore != null ? (PrimaryDataStoreTO)dataStore.getTO() : null); + + String volumePathPrefix = getVolumePathPrefix(storagePool); volumePaths.add(String.format("%s/%s", volumePathPrefix, volume.getPath())); } - return volumePaths; + return new Pair<>(volumePools, volumePaths); + } + + private String getVolumePathPrefix(StoragePoolVO storagePool) { + String volumePathPrefix; + if (ScopeType.HOST.equals(storagePool.getScope()) || + Storage.StoragePoolType.SharedMountPoint.equals(storagePool.getPoolType()) || + Storage.StoragePoolType.RBD.equals(storagePool.getPoolType())) { + volumePathPrefix = storagePool.getPath(); + } else { + // Should be Storage.StoragePoolType.NetworkFilesystem + volumePathPrefix = String.format("/mnt/%s", storagePool.getUuid()); + } + return volumePathPrefix; } @Override public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, Pair vmNameAndState) { final VolumeVO volume = volumeDao.findByUuid(backupVolumeInfo.getUuid()); final DiskOffering diskOffering = diskOfferingDao.findByUuid(backupVolumeInfo.getDiskOfferingId()); - final StoragePoolHostVO dataStore = storagePoolHostDao.findByUuid(dataStoreUuid); + final StoragePoolVO pool = primaryDataStoreDao.findByUuid(dataStoreUuid); final HostVO hostVO = hostDao.findByIp(hostIp); LOG.debug("Restoring vm volume {} from backup {} on the NAS Backup Provider", backupVolumeInfo, backup); @@ -360,19 +380,26 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co restoredVolume.setUuid(volumeUUID); restoredVolume.setRemoved(null); restoredVolume.setDisplayVolume(true); - restoredVolume.setPoolId(dataStore.getPoolId()); + restoredVolume.setPoolId(pool.getId()); + restoredVolume.setPoolType(pool.getPoolType()); restoredVolume.setPath(restoredVolume.getUuid()); restoredVolume.setState(Volume.State.Copying); - restoredVolume.setFormat(Storage.ImageFormat.QCOW2); restoredVolume.setSize(backupVolumeInfo.getSize()); restoredVolume.setDiskOfferingId(diskOffering.getId()); + if (pool.getPoolType() != Storage.StoragePoolType.RBD) { + restoredVolume.setFormat(Storage.ImageFormat.QCOW2); + } else { + restoredVolume.setFormat(Storage.ImageFormat.RAW); + } RestoreBackupCommand restoreCommand = new RestoreBackupCommand(); restoreCommand.setBackupPath(backup.getExternalId()); restoreCommand.setBackupRepoType(backupRepository.getType()); restoreCommand.setBackupRepoAddress(backupRepository.getAddress()); restoreCommand.setVmName(vmNameAndState.first()); - restoreCommand.setRestoreVolumePaths(Collections.singletonList(String.format("%s/%s", dataStore.getLocalPath(), volumeUUID))); + restoreCommand.setRestoreVolumePaths(Collections.singletonList(String.format("%s/%s", getVolumePathPrefix(pool), volumeUUID))); + DataStore dataStore = dataStoreMgr.getDataStore(pool.getId(), DataStoreRole.Primary); + restoreCommand.setRestoreVolumePools(Collections.singletonList(dataStore != null ? (PrimaryDataStoreTO)dataStore.getTO() : null)); restoreCommand.setDiskType(backupVolumeInfo.getType().name().toLowerCase(Locale.ROOT)); restoreCommand.setMountOptions(backupRepository.getMountOptions()); restoreCommand.setVmExists(null); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java index 243cf2efa03..fd94013dd50 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java @@ -21,15 +21,25 @@ package com.cloud.hypervisor.kvm.resource.wrapper; import com.cloud.agent.api.Answer; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.script.Script; import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.backup.BackupAnswer; import org.apache.cloudstack.backup.RestoreBackupCommand; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.libvirt.LibvirtException; import java.io.File; import java.io.IOException; @@ -45,7 +55,8 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper backedVolumeUUIDs = command.getBackupVolumesUUIDs(); + List restoreVolumePools = command.getRestoreVolumePools(); List restoreVolumePaths = command.getRestoreVolumePaths(); String restoreVolumeUuid = command.getRestoreVolumeUUID(); Integer mountTimeout = command.getMountTimeout() * 1000; + int timeout = command.getWait(); + KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr(); String newVolumeId = null; try { String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions, mountTimeout); if (Objects.isNull(vmExists)) { + PrimaryDataStoreTO volumePool = restoreVolumePools.get(0); String volumePath = restoreVolumePaths.get(0); int lastIndex = volumePath.lastIndexOf("/"); newVolumeId = volumePath.substring(lastIndex + 1); - restoreVolume(backupPath, volumePath, diskType, restoreVolumeUuid, - new Pair<>(vmName, command.getVmState()), mountDirectory); + restoreVolume(storagePoolMgr, backupPath, volumePool, volumePath, diskType, restoreVolumeUuid, + new Pair<>(vmName, command.getVmState()), mountDirectory, timeout); } else if (Boolean.TRUE.equals(vmExists)) { - restoreVolumesOfExistingVM(restoreVolumePaths, backedVolumeUUIDs, backupPath, mountDirectory); + restoreVolumesOfExistingVM(storagePoolMgr, restoreVolumePools, restoreVolumePaths, backedVolumeUUIDs, backupPath, mountDirectory, timeout); } else { - restoreVolumesOfDestroyedVMs(restoreVolumePaths, vmName, backupPath, mountDirectory); + restoreVolumesOfDestroyedVMs(storagePoolMgr, restoreVolumePools, restoreVolumePaths, vmName, backupPath, mountDirectory, timeout); } } catch (CloudRuntimeException e) { String errorMessage = e.getMessage() != null ? e.getMessage() : ""; @@ -94,17 +109,18 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper restoreVolumePaths, List backedVolumesUUIDs, - String backupPath, String mountDirectory) { + private void restoreVolumesOfExistingVM(KVMStoragePoolManager storagePoolMgr, List restoreVolumePools, List restoreVolumePaths, List backedVolumesUUIDs, + String backupPath, String mountDirectory, int timeout) { String diskType = "root"; try { for (int idx = 0; idx < restoreVolumePaths.size(); idx++) { + PrimaryDataStoreTO restoreVolumePool = restoreVolumePools.get(idx); String restoreVolumePath = restoreVolumePaths.get(idx); String backupVolumeUuid = backedVolumesUUIDs.get(idx); Pair bkpPathAndVolUuid = getBackupPath(mountDirectory, null, backupPath, diskType, backupVolumeUuid); diskType = "datadisk"; verifyBackupFile(bkpPathAndVolUuid.first(), bkpPathAndVolUuid.second()); - if (!replaceVolumeWithBackup(restoreVolumePath, bkpPathAndVolUuid.first())) { + if (!replaceVolumeWithBackup(storagePoolMgr, restoreVolumePool, restoreVolumePath, bkpPathAndVolUuid.first(), timeout)) { throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second())); } } @@ -114,15 +130,16 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper volumePaths, String vmName, String backupPath, String mountDirectory) { + private void restoreVolumesOfDestroyedVMs(KVMStoragePoolManager storagePoolMgr, List volumePools, List volumePaths, String vmName, String backupPath, String mountDirectory, int timeout) { String diskType = "root"; try { for (int i = 0; i < volumePaths.size(); i++) { + PrimaryDataStoreTO volumePool = volumePools.get(i); String volumePath = volumePaths.get(i); Pair bkpPathAndVolUuid = getBackupPath(mountDirectory, volumePath, backupPath, diskType, null); diskType = "datadisk"; verifyBackupFile(bkpPathAndVolUuid.first(), bkpPathAndVolUuid.second()); - if (!replaceVolumeWithBackup(volumePath, bkpPathAndVolUuid.first())) { + if (!replaceVolumeWithBackup(storagePoolMgr, volumePool, volumePath, bkpPathAndVolUuid.first(), timeout)) { throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second())); } } @@ -132,17 +149,17 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper vmNameAndState, String mountDirectory) { + private void restoreVolume(KVMStoragePoolManager storagePoolMgr, String backupPath, PrimaryDataStoreTO volumePool, String volumePath, String diskType, String volumeUUID, + Pair vmNameAndState, String mountDirectory, int timeout) { Pair bkpPathAndVolUuid; try { bkpPathAndVolUuid = getBackupPath(mountDirectory, volumePath, backupPath, diskType, volumeUUID); verifyBackupFile(bkpPathAndVolUuid.first(), bkpPathAndVolUuid.second()); - if (!replaceVolumeWithBackup(volumePath, bkpPathAndVolUuid.first())) { + if (!replaceVolumeWithBackup(storagePoolMgr, volumePool, volumePath, bkpPathAndVolUuid.first(), timeout, true)) { throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second())); } if (VirtualMachine.State.Running.equals(vmNameAndState.second())) { - if (!attachVolumeToVm(vmNameAndState.first(), volumePath)) { + if (!attachVolumeToVm(storagePoolMgr, vmNameAndState.first(), volumePool, volumePath)) { throw new CloudRuntimeException(String.format("Failed to attach volume to VM: %s", vmNameAndState.first())); } } @@ -220,14 +237,63 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper\n"); + + diskBuilder.append("\n"); + for (String sourceHost : volumePool.getHost().split(",")) { + diskBuilder.append("\n"); + } + diskBuilder.append("\n"); + String authUserName = null; + final KVMStoragePool primaryPool = storagePoolMgr.getStoragePool(volumePool.getPoolType(), volumePool.getUuid()); + if (primaryPool != null) { + authUserName = primaryPool.getAuthUserName(); + } + if (StringUtils.isNotBlank(authUserName)) { + diskBuilder.append("\n"); + diskBuilder.append("\n"); + diskBuilder.append("\n"); + } + diskBuilder.append("\n"); + diskBuilder.append("\n"); + return diskBuilder.toString(); + } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java index c7a67080fbf..11fa605908a 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java @@ -22,12 +22,17 @@ package com.cloud.hypervisor.kvm.resource.wrapper; import com.amazonaws.util.CollectionUtils; import com.cloud.agent.api.Answer; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage; import com.cloud.utils.Pair; import com.cloud.utils.script.Script; import org.apache.cloudstack.backup.BackupAnswer; import org.apache.cloudstack.backup.TakeBackupCommand; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import java.util.ArrayList; import java.util.Arrays; @@ -44,7 +49,24 @@ public class LibvirtTakeBackupCommandWrapper extends CommandWrapper diskPaths = command.getVolumePaths(); + List volumePools = command.getVolumePools(); + final List volumePaths = command.getVolumePaths(); + KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); + + List diskPaths = new ArrayList<>(); + if (Objects.nonNull(volumePaths)) { + for (int idx = 0; idx < volumePaths.size(); idx++) { + PrimaryDataStoreTO volumePool = volumePools.get(idx); + String volumePath = volumePaths.get(idx); + if (volumePool.getPoolType() != Storage.StoragePoolType.RBD) { + diskPaths.add(volumePath); + } else { + KVMStoragePool volumeStoragePool = storagePoolMgr.getStoragePool(volumePool.getPoolType(), volumePool.getUuid()); + String rbdDestVolumeFile = KVMPhysicalDisk.RBDStringBuilder(volumeStoragePool, volumePath); + diskPaths.add(rbdDestVolumeFile); + } + } + } List commands = new ArrayList<>(); commands.add(new String[]{ @@ -56,7 +78,7 @@ public class LibvirtTakeBackupCommandWrapper extends CommandWrapper result = Script.executePipedCommands(commands, libvirtComputingResource.getCmdsTimeout()); diff --git a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java index beba0856241..0a8ea27cd49 100644 --- a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java +++ b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java @@ -61,6 +61,7 @@ public class QemuImg { private String cloudQemuImgPath = "cloud-qemu-img"; private int timeout; private boolean skipZero = false; + private boolean skipTargetVolumeCreation = false; private boolean noCache = false; private long version; @@ -435,6 +436,8 @@ public class QemuImg { // with target-is-zero we skip zeros in 1M chunks for compatibility script.add("-S"); script.add("1M"); + } else if (skipTargetVolumeCreation) { + script.add("-n"); } script.add("-O"); @@ -881,6 +884,10 @@ public class QemuImg { this.skipZero = skipZero; } + public void setSkipTargetVolumeCreation(boolean skipTargetVolumeCreation) { + this.skipTargetVolumeCreation = skipTargetVolumeCreation; + } + public boolean supportsImageFormat(QemuImg.PhysicalDiskFormat format) { final Script s = new Script(_qemuImgPath, timeout); s.add("--help"); diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java index d120abd0a1b..7bcd0bf18e6 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java @@ -22,6 +22,7 @@ import com.cloud.utils.script.Script; import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.backup.BackupAnswer; import org.apache.cloudstack.backup.RestoreBackupCommand; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -65,6 +66,8 @@ public class LibvirtRestoreBackupCommandWrapperTest { when(command.getMountOptions()).thenReturn("rw"); when(command.isVmExists()).thenReturn(null); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); @@ -105,6 +108,8 @@ public class LibvirtRestoreBackupCommandWrapperTest { when(command.getMountOptions()).thenReturn("rw"); when(command.isVmExists()).thenReturn(true); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); when(command.getBackupVolumesUUIDs()).thenReturn(Arrays.asList("volume-123")); when(command.getMountTimeout()).thenReturn(30); @@ -141,6 +146,8 @@ public class LibvirtRestoreBackupCommandWrapperTest { when(command.getMountOptions()).thenReturn("rw"); when(command.isVmExists()).thenReturn(false); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); when(command.getMountTimeout()).thenReturn(30); @@ -176,6 +183,8 @@ public class LibvirtRestoreBackupCommandWrapperTest { when(command.getMountOptions()).thenReturn("username=user,password=pass"); when(command.isVmExists()).thenReturn(null); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); @@ -215,6 +224,8 @@ public class LibvirtRestoreBackupCommandWrapperTest { lenient().when(command.getMountOptions()).thenReturn("rw"); lenient().when(command.isVmExists()).thenReturn(null); lenient().when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); lenient().when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); lenient().when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); lenient().when(command.getVmState()).thenReturn(VirtualMachine.State.Running); @@ -249,6 +260,8 @@ public class LibvirtRestoreBackupCommandWrapperTest { when(command.getMountOptions()).thenReturn("rw"); when(command.isVmExists()).thenReturn(null); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); @@ -293,6 +306,8 @@ public class LibvirtRestoreBackupCommandWrapperTest { when(command.getMountOptions()).thenReturn("rw"); when(command.isVmExists()).thenReturn(null); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); @@ -339,6 +354,8 @@ public class LibvirtRestoreBackupCommandWrapperTest { when(command.getMountOptions()).thenReturn("rw"); when(command.isVmExists()).thenReturn(null); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); @@ -387,6 +404,8 @@ public class LibvirtRestoreBackupCommandWrapperTest { when(command.getMountOptions()).thenReturn("rw"); when(command.isVmExists()).thenReturn(null); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); @@ -439,6 +458,8 @@ public class LibvirtRestoreBackupCommandWrapperTest { lenient().when(command.getMountOptions()).thenReturn("rw"); lenient().when(command.isVmExists()).thenReturn(null); lenient().when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); lenient().when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); lenient().when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); lenient().when(command.getVmState()).thenReturn(VirtualMachine.State.Running); @@ -467,6 +488,12 @@ public class LibvirtRestoreBackupCommandWrapperTest { when(command.getMountOptions()).thenReturn("rw"); when(command.isVmExists()).thenReturn(true); when(command.getDiskType()).thenReturn("root"); + PrimaryDataStoreTO primaryDataStore1 = Mockito.mock(PrimaryDataStoreTO.class); + PrimaryDataStoreTO primaryDataStore2 = Mockito.mock(PrimaryDataStoreTO.class); + when(command.getRestoreVolumePools()).thenReturn(Arrays.asList( + primaryDataStore1, + primaryDataStore2 + )); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList( "/var/lib/libvirt/images/volume-123", "/var/lib/libvirt/images/volume-456" diff --git a/scripts/vm/hypervisor/kvm/nasbackup.sh b/scripts/vm/hypervisor/kvm/nasbackup.sh index 588c3791769..e298006f7a8 100755 --- a/scripts/vm/hypervisor/kvm/nasbackup.sh +++ b/scripts/vm/hypervisor/kvm/nasbackup.sh @@ -165,7 +165,14 @@ backup_stopped_vm() { name="root" for disk in $DISK_PATHS; do - volUuid="${disk##*/}" + if [[ "$disk" == rbd:* ]]; then + # disk for rbd => rbd:/:mon_host=... + # sample: rbd:cloudstack/53d5c355-d726-4d3e-9422-046a503a0b12:mon_host=10.0.1.2... + beforeUuid="${disk#*/}" # Remove up to first slash after rbd: + volUuid="${beforeUuid%%:*}" # Remove everything after colon to get the uuid + else + volUuid="${disk##*/}" + fi output="$dest/$name.$volUuid.qcow2" if ! qemu-img convert -O qcow2 "$disk" "$output" > "$logFile" 2> >(cat >&2); then echo "qemu-img convert failed for $disk $output" From 963a67b81677fa85ef06dc7c6c2aaa165c85d9df Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Tue, 7 Oct 2025 06:49:57 +0200 Subject: [PATCH 018/311] server: add user.password.reset.smtp.useStartTLS and enabledSecurityProtocols for password reset (#11228) --- .../cloudstack/user/UserPasswordResetManager.java | 11 +++++++++++ .../cloudstack/user/UserPasswordResetManagerImpl.java | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/server/src/main/java/org/apache/cloudstack/user/UserPasswordResetManager.java b/server/src/main/java/org/apache/cloudstack/user/UserPasswordResetManager.java index a42faf2835a..929f11013b0 100644 --- a/server/src/main/java/org/apache/cloudstack/user/UserPasswordResetManager.java +++ b/server/src/main/java/org/apache/cloudstack/user/UserPasswordResetManager.java @@ -55,6 +55,17 @@ public interface UserPasswordResetManager { "Use auth in the SMTP server for sending emails for resetting password for ACS users", false, ConfigKey.Scope.Global); + ConfigKey UserPasswordResetSMTPUseStartTLS = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, + Boolean.class, "user.password.reset.smtp.useStartTLS", "false", + "If set to true and if we enable security via user.password.reset.smtp.useAuth, this will enable StartTLS to secure the connection.", + true, + ConfigKey.Scope.Global); + + ConfigKey UserPasswordResetSMTPEnabledSecurityProtocols = new ConfigKey(ConfigKey.CATEGORY_ADVANCED, + String.class, "user.password.reset.smtp.enabledSecurityProtocols", "", + "White-space separated security protocols; ex: \"TLSv1 TLSv1.1\". Supported protocols: SSLv2Hello, SSLv3, TLSv1, TLSv1.1 and TLSv1.2", + true, ConfigKey.Kind.WhitespaceSeparatedListWithOptions, "SSLv2Hello,SSLv3,TLSv1,TLSv1.1,TLSv1.2"); + ConfigKey UserPasswordResetSMTPUsername = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, String.class, "user.password.reset.smtp.username", null, "Username for SMTP server for sending emails for resetting password for ACS users", diff --git a/server/src/main/java/org/apache/cloudstack/user/UserPasswordResetManagerImpl.java b/server/src/main/java/org/apache/cloudstack/user/UserPasswordResetManagerImpl.java index 6574489c827..798b6287e7e 100644 --- a/server/src/main/java/org/apache/cloudstack/user/UserPasswordResetManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/user/UserPasswordResetManagerImpl.java @@ -93,6 +93,8 @@ public class UserPasswordResetManagerImpl extends ManagerBase implements UserPas UserPasswordResetSMTPHost, UserPasswordResetSMTPPort, UserPasswordResetSMTPUseAuth, + UserPasswordResetSMTPUseStartTLS, + UserPasswordResetSMTPEnabledSecurityProtocols, UserPasswordResetSMTPUsername, UserPasswordResetSMTPPassword, PasswordResetMailTemplate @@ -106,6 +108,8 @@ public class UserPasswordResetManagerImpl extends ManagerBase implements UserPas Boolean useAuth = UserPasswordResetSMTPUseAuth.value(); String username = UserPasswordResetSMTPUsername.value(); String password = UserPasswordResetSMTPPassword.value(); + Boolean useStartTLS = UserPasswordResetSMTPUseStartTLS.value(); + String enabledSecurityProtocols = UserPasswordResetSMTPEnabledSecurityProtocols.value(); if (!StringUtils.isEmpty(smtpHost) && smtpPort != null && smtpPort > 0) { String namespace = "password.reset.smtp"; @@ -117,6 +121,8 @@ public class UserPasswordResetManagerImpl extends ManagerBase implements UserPas configs.put(getKey(namespace, SMTPMailSender.CONFIG_USE_AUTH), useAuth.toString()); configs.put(getKey(namespace, SMTPMailSender.CONFIG_USERNAME), username); configs.put(getKey(namespace, SMTPMailSender.CONFIG_PASSWORD), password); + configs.put(getKey(namespace, SMTPMailSender.CONFIG_USE_STARTTLS), useStartTLS.toString()); + configs.put(getKey(namespace, SMTPMailSender.CONFIG_ENABLED_SECURITY_PROTOCOLS), enabledSecurityProtocols); mailSender = new SMTPMailSender(configs, namespace); } From 9bcd98876d6737a9139b2e33535f479e7300723f Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Tue, 7 Oct 2025 10:32:33 +0530 Subject: [PATCH 019/311] Make kvm domain persistent when unmanaged from CS (#11541) CS creates transient KVM domain.xml. When instance is unmanaged from CS, explicit dump of domain has to be taken to manage is outside of CS. With this PR domainXML gets backed up and becomes persistent for further management of Instance. Stopped instance also can be unmanaged, last host for instance is considered for defining domain hostid param is supported in unmanageVirtualMachine API for KVM hypervisor and for stopped Instances hostid field in response of unmanageVirtualMachine, representing host used for unmanage operation Disable unmanaging instance with config drive, can unmanage from API using forced=true param for KVM --- .../main/java/com/cloud/vm/UserVmService.java | 6 +- .../admin/vm/UnmanageVMInstanceCmd.java | 37 +- .../response/UnmanageVMInstanceResponse.java | 12 + .../cloudstack/vm/UnmanageVMService.java | 7 +- .../agent/api/UnmanageInstanceAnswer.java | 27 + .../agent/api/UnmanageInstanceCommand.java | 61 +++ .../com/cloud/vm/VirtualMachineManager.java | 2 +- .../cloud/vm/VirtualMachineManagerImpl.java | 121 ++++- .../vm/VirtualMachineManagerImplTest.java | 317 +++++++++++- ...LibvirtUnmanageInstanceCommandWrapper.java | 174 +++++++ ...irtUnmanageInstanceCommandWrapperTest.java | 357 +++++++++++++ .../java/com/cloud/vm/UserVmManagerImpl.java | 26 +- .../vm/UnmanagedVMsManagerImpl.java | 19 +- .../com/cloud/vm/UserVmManagerImplTest.java | 184 ++++++- .../vm/UnmanagedVMsManagerImplTest.java | 91 +++- .../test_vm_lifecycle_unmanage_kvm_import.py | 473 ++++++++++++++++++ tools/marvin/marvin/lib/base.py | 5 +- ui/public/locales/en.json | 2 +- 18 files changed, 1850 insertions(+), 71 deletions(-) create mode 100644 core/src/main/java/com/cloud/agent/api/UnmanageInstanceAnswer.java create mode 100644 core/src/main/java/com/cloud/agent/api/UnmanageInstanceCommand.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUnmanageInstanceCommandWrapper.java create mode 100644 plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUnmanageInstanceCommandWrapperTest.java create mode 100644 test/integration/smoke/test_vm_lifecycle_unmanage_kvm_import.py diff --git a/api/src/main/java/com/cloud/vm/UserVmService.java b/api/src/main/java/com/cloud/vm/UserVmService.java index 0747e193ab8..01f11b73cd4 100644 --- a/api/src/main/java/com/cloud/vm/UserVmService.java +++ b/api/src/main/java/com/cloud/vm/UserVmService.java @@ -64,6 +64,7 @@ import com.cloud.storage.StoragePool; import com.cloud.template.VirtualMachineTemplate; import com.cloud.user.Account; import com.cloud.uservm.UserVm; +import com.cloud.utils.Pair; import com.cloud.utils.exception.ExecutionException; public interface UserVmService { @@ -538,9 +539,10 @@ public interface UserVmService { /** * Unmanage a guest VM from CloudStack - * @return true if the VM is successfully unmanaged, false if not. + * + * @return (true if successful, false if not, hostUuid) if the VM is successfully unmanaged. */ - boolean unmanageUserVM(Long vmId); + Pair unmanageUserVM(Long vmId, Long targetHostId); UserVm allocateVMFromBackup(CreateVMFromBackupCmd cmd) throws InsufficientCapacityException, ResourceAllocationException, ResourceUnavailableException; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/UnmanageVMInstanceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/UnmanageVMInstanceCmd.java index bbcb8840f66..2c60b574126 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/UnmanageVMInstanceCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/UnmanageVMInstanceCmd.java @@ -27,6 +27,7 @@ import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.user.Account; import com.cloud.uservm.UserVm; +import com.cloud.utils.Pair; import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; @@ -36,10 +37,12 @@ import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseAsyncCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.HostResponse; import org.apache.cloudstack.api.response.UnmanageVMInstanceResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.vm.UnmanagedVMsManager; +import org.apache.commons.lang3.BooleanUtils; import javax.inject.Inject; @@ -65,6 +68,20 @@ public class UnmanageVMInstanceCmd extends BaseAsyncCmd { description = "The ID of the virtual machine to unmanage") private Long vmId; + @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, + entityType = HostResponse.class, required = false, + description = "ID of the host which will be used for unmanaging the Instance. " + + "Applicable only for KVM hypervisor and stopped Instances. Domain XML will be stored on this host.", + since = "4.22.0") + private Long hostId; + + @Parameter(name = ApiConstants.FORCED, + type = CommandType.BOOLEAN, + required = false, + description = "Force unmanaging Instance with config drive. Applicable only for KVM Hypervisor.", + since = "4.22.0") + private Boolean forced; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -83,6 +100,18 @@ public class UnmanageVMInstanceCmd extends BaseAsyncCmd { return "unmanaging VM. VM ID = " + vmId; } + public Long getHostId() { + return hostId; + } + + public void setHostId(Long hostId) { + this.hostId = hostId; + } + + public Boolean isForced() { + return BooleanUtils.isTrue(forced); + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -93,9 +122,10 @@ public class UnmanageVMInstanceCmd extends BaseAsyncCmd { UnmanageVMInstanceResponse response = new UnmanageVMInstanceResponse(); try { CallContext.current().setEventDetails("VM ID = " + vmId); - boolean result = unmanagedVMsManager.unmanageVMInstance(vmId); - response.setSuccess(result); - if (result) { + Pair result = unmanagedVMsManager.unmanageVMInstance(vmId, hostId, isForced()); + if (result.first()) { + response.setSuccess(true); + response.setHostId(result.second()); response.setDetails("VM unmanaged successfully"); } } catch (Exception e) { @@ -124,5 +154,4 @@ public class UnmanageVMInstanceCmd extends BaseAsyncCmd { public Long getApiResourceId() { return vmId; } - } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UnmanageVMInstanceResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UnmanageVMInstanceResponse.java index e9d45cb506a..bd95ecee5ae 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UnmanageVMInstanceResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UnmanageVMInstanceResponse.java @@ -32,6 +32,10 @@ public class UnmanageVMInstanceResponse extends BaseResponse { @Param(description = "details of the unmanage VM operation") private String details; + @SerializedName(ApiConstants.HOST_ID) + @Param(description = "The ID of the host used for unmanaged Instance") + private String hostId; + public UnmanageVMInstanceResponse() { } @@ -55,4 +59,12 @@ public class UnmanageVMInstanceResponse extends BaseResponse { public void setDetails(String details) { this.details = details; } + + public String getHostId() { + return hostId; + } + + public void setHostId(String hostId) { + this.hostId = hostId; + } } diff --git a/api/src/main/java/org/apache/cloudstack/vm/UnmanageVMService.java b/api/src/main/java/org/apache/cloudstack/vm/UnmanageVMService.java index 2315e5f2e95..70d8795f305 100644 --- a/api/src/main/java/org/apache/cloudstack/vm/UnmanageVMService.java +++ b/api/src/main/java/org/apache/cloudstack/vm/UnmanageVMService.java @@ -17,11 +17,14 @@ package org.apache.cloudstack.vm; +import com.cloud.utils.Pair; + public interface UnmanageVMService { /** * Unmanage a guest VM from CloudStack - * @return true if the VM is successfully unmanaged, false if not. + * + * @return (true if successful, false if not, hostUuid) if the VM is successfully unmanaged. */ - boolean unmanageVMInstance(long vmId); + Pair unmanageVMInstance(long vmId, Long paramHostId, boolean isForced); } diff --git a/core/src/main/java/com/cloud/agent/api/UnmanageInstanceAnswer.java b/core/src/main/java/com/cloud/agent/api/UnmanageInstanceAnswer.java new file mode 100644 index 00000000000..39a35d49990 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/UnmanageInstanceAnswer.java @@ -0,0 +1,27 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package com.cloud.agent.api; + +public class UnmanageInstanceAnswer extends Answer { + + public UnmanageInstanceAnswer(UnmanageInstanceCommand cmd, boolean success, String details) { + super(cmd, success, details); + } +} diff --git a/core/src/main/java/com/cloud/agent/api/UnmanageInstanceCommand.java b/core/src/main/java/com/cloud/agent/api/UnmanageInstanceCommand.java new file mode 100644 index 00000000000..dd504b9ea26 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/UnmanageInstanceCommand.java @@ -0,0 +1,61 @@ +// +// 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.agent.api; + +import com.cloud.agent.api.to.VirtualMachineTO; + +/** + */ +public class UnmanageInstanceCommand extends Command { + String instanceName; + boolean executeInSequence = false; + VirtualMachineTO vm; + boolean isConfigDriveAttached; + + @Override + public boolean executeInSequence() { + return executeInSequence; + } + + public UnmanageInstanceCommand(VirtualMachineTO vm) { + this.vm = vm; + this.instanceName = vm.getName(); + } + + public UnmanageInstanceCommand(String instanceName) { + this.instanceName = instanceName; + } + + public String getInstanceName() { + return instanceName; + } + + public VirtualMachineTO getVm() { + return vm; + } + + public boolean isConfigDriveAttached() { + return isConfigDriveAttached; + } + + public void setConfigDriveAttached(boolean configDriveAttached) { + isConfigDriveAttached = configDriveAttached; + } +} 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 c05c29add55..cffba3d9a13 100644 --- a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java +++ b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java @@ -274,7 +274,7 @@ public interface VirtualMachineManager extends Manager { * - Remove the references of the VM and its volumes, nics, IPs from database * - Keep the VM as it is on the hypervisor */ - boolean unmanage(String vmUuid); + Pair unmanage(String vmUuid, Long paramHostId); UserVm restoreVirtualMachine(long vmId, Long newTemplateId, Long rootDiskOfferingId, boolean expunge, Map details) throws ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException; 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 b5597280364..7c5d43fea97 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -71,6 +71,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProviderManag import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver; import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.framework.ca.Certificate; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; @@ -150,11 +151,13 @@ import com.cloud.agent.api.StopAnswer; import com.cloud.agent.api.StopCommand; import com.cloud.agent.api.UnPlugNicAnswer; import com.cloud.agent.api.UnPlugNicCommand; +import com.cloud.agent.api.UnmanageInstanceCommand; import com.cloud.agent.api.UnregisterVMCommand; import com.cloud.agent.api.VmDiskStatsEntry; import com.cloud.agent.api.VmNetworkStatsEntry; import com.cloud.agent.api.VmStatsEntry; import com.cloud.agent.api.routing.NetworkElementCommand; +import com.cloud.agent.api.to.DataTO; import com.cloud.agent.api.to.DiskTO; import com.cloud.agent.api.to.DpdkTO; import com.cloud.agent.api.to.GPUDeviceTO; @@ -2016,7 +2019,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } @Override - public boolean unmanage(String vmUuid) { + public Pair unmanage(String vmUuid, Long paramHostId) { VMInstanceVO vm = _vmDao.findByUuid(vmUuid); if (vm == null || vm.getRemoved() != null) { throw new CloudRuntimeException("Could not find VM with id = " + vmUuid); @@ -2029,6 +2032,10 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac throw new ConcurrentOperationException(msg); } + Long agentHostId = vm.getHostId(); + if (HypervisorType.KVM.equals(vm.getHypervisorType())) { + agentHostId = persistDomainForKVM(vm, paramHostId); + } Boolean result = Transaction.execute(new TransactionCallback() { @Override public Boolean doInTransaction(TransactionStatus status) { @@ -2052,21 +2059,66 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac return true; } }); + HostVO host = ApiDBUtils.findHostById(agentHostId); + if (host == null) { + return new Pair<>(result, null); + } + logger.debug("Selected host UUID: {} to unmanage Instance: {}.", host.getUuid(), vm.getName()); + ActionEventUtils.onActionEvent(User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM, Domain.ROOT_DOMAIN, EventTypes.EVENT_VM_UNMANAGE, + String.format("Successfully unmanaged Instance: %s (ID: %s) on host ID: %s", vm.getName(), vm.getUuid(), host.getUuid()), + vm.getId(), ApiCommandResourceType.VirtualMachine.toString()); + return new Pair<>(result, host.getUuid()); + } - return BooleanUtils.isTrue(result); + Long persistDomainForKVM(VMInstanceVO vm, Long paramHostId) { + Long agentHostId = vm.getHostId(); + String vmName = vm.getName(); + UnmanageInstanceCommand unmanageInstanceCommand; + if (State.Stopped.equals(vm.getState())) { + if (paramHostId == null) { + Pair clusterAndHostId = findClusterAndHostIdForVm(vm, false); + agentHostId = clusterAndHostId.second(); + if (agentHostId == null) { + String errorMsg = "No available host to persist domain XML for Instance: " + vmName; + logger.debug(errorMsg); + throw new CloudRuntimeException(errorMsg); + } + } else { + agentHostId = paramHostId; + } + unmanageInstanceCommand = new UnmanageInstanceCommand(prepVmSpecForUnmanageCmd(vm.getId(), agentHostId)); // reconstruct vmSpec for stopped instance + } else { + unmanageInstanceCommand = new UnmanageInstanceCommand(vmName); + unmanageInstanceCommand.setConfigDriveAttached(vmInstanceDetailsDao.findDetail(vm.getId(), VmDetailConstants.CONFIG_DRIVE_LOCATION) != null); + } + + logger.debug("Selected host ID: {} to persist domain XML for Instance: {}.", agentHostId, vmName); + try { + Answer answer = _agentMgr.send(agentHostId, unmanageInstanceCommand); + if (!answer.getResult()) { + String errorMsg = "Failed to persist domain XML for Instance: " + vmName + " on host ID: " + agentHostId; + logger.debug(errorMsg); + throw new CloudRuntimeException(errorMsg); + } + } catch (AgentUnavailableException | OperationTimedoutException e) { + String errorMsg = "Failed to send command to persist domain XML for Instance: " + vmName + " on host ID: " + agentHostId; + logger.error(errorMsg, e); + throw new CloudRuntimeException(errorMsg); + } + return agentHostId; } /** * Clean up VM snapshots (if any) from DB */ - private void unmanageVMSnapshots(VMInstanceVO vm) { + void unmanageVMSnapshots(VMInstanceVO vm) { _vmSnapshotMgr.deleteVMSnapshotsFromDB(vm.getId(), true); } /** * Clean up volumes for a VM to be unmanaged from CloudStack */ - private void unmanageVMVolumes(VMInstanceVO vm) { + void unmanageVMVolumes(VMInstanceVO vm) { final Long hostId = vm.getHostId() != null ? vm.getHostId() : vm.getLastHostId(); if (hostId != null) { volumeMgr.revokeAccess(vm.getId(), hostId); @@ -2084,7 +2136,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac * - If 'unmanage.vm.preserve.nics' = true: then the NICs are not removed but still Allocated, to preserve MAC addresses * - If 'unmanage.vm.preserve.nics' = false: then the NICs are removed while unmanaging */ - private void unmanageVMNics(VirtualMachineProfile profile, VMInstanceVO vm) { + void unmanageVMNics(VirtualMachineProfile profile, VMInstanceVO vm) { logger.debug("Cleaning up NICs of {}.", vm.toString()); Boolean preserveNics = UnmanagedVMsManager.UnmanageVMPreserveNic.valueIn(vm.getDataCenterId()); if (BooleanUtils.isTrue(preserveNics)) { @@ -4019,6 +4071,62 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac vmTo.setEnterHardwareSetup(enterSetup == null ? false : enterSetup); } + /** + * This method helps constructing vmSpec for Unmanage operation for Stopped Instance + * @param vmId + * @param hostId + * @return VirtualMachineTO + */ + protected VirtualMachineTO prepVmSpecForUnmanageCmd(Long vmId, Long hostId) { + final VMInstanceVO vm = _vmDao.findById(vmId); + final Account owner = _entityMgr.findById(Account.class, vm.getAccountId()); + final ServiceOfferingVO offering = _offeringDao.findById(vm.getId(), vm.getServiceOfferingId()); + final VirtualMachineTemplate template = _entityMgr.findByIdIncludingRemoved(VirtualMachineTemplate.class, vm.getTemplateId()); + Host host = _hostDao.findById(hostId); + VirtualMachineProfileImpl vmProfile = new VirtualMachineProfileImpl(vm, template, offering, owner, null); + updateOverCommitRatioForVmProfile(vmProfile, host.getClusterId()); + final List nics = _nicsDao.listByVmId(vmProfile.getId()); + Collections.sort(nics, (nic1, nic2) -> { + Long nicId1 = Long.valueOf(nic1.getDeviceId()); + Long nicId2 = Long.valueOf(nic2.getDeviceId()); + return nicId1.compareTo(nicId2); + }); + + for (final NicVO nic : nics) { + final Network network = _networkModel.getNetwork(nic.getNetworkId()); + final NicProfile nicProfile = + new NicProfile(nic, network, nic.getBroadcastUri(), nic.getIsolationUri(), null, _networkModel.isSecurityGroupSupportedInNetwork(network), + _networkModel.getNetworkTag(vmProfile.getHypervisorType(), network)); + vmProfile.addNic(nicProfile); + } + + List volumes = _volsDao.findUsableVolumesForInstance(vmId); + for (VolumeVO vol: volumes) { + VolumeInfo volumeInfo = volumeDataFactory.getVolume(vol.getId()); + DataTO dataTO = volumeInfo.getTO(); + DiskTO disk = storageMgr.getDiskWithThrottling(dataTO, vol.getVolumeType(), vol.getDeviceId(), vol.getPath(), vm.getServiceOfferingId(), vol.getDiskOfferingId()); + vmProfile.addDisk(disk); + } + + Map details = vmInstanceDetailsDao.listDetailsKeyPairs(vmId, + List.of(VirtualMachineProfile.Param.BootType.getName(), VirtualMachineProfile.Param.BootMode.getName(), + VirtualMachineProfile.Param.UefiFlag.getName())); + + if (details.containsKey(VirtualMachineProfile.Param.BootType.getName())) { + vmProfile.getParameters().put(VirtualMachineProfile.Param.BootType, details.get(VirtualMachineProfile.Param.BootType.getName())); + } + + if (details.containsKey(VirtualMachineProfile.Param.BootMode.getName())) { + vmProfile.getParameters().put(VirtualMachineProfile.Param.BootMode, details.get(VirtualMachineProfile.Param.BootMode.getName())); + } + + if (details.containsKey(VirtualMachineProfile.Param.UefiFlag.getName())) { + vmProfile.getParameters().put(VirtualMachineProfile.Param.UefiFlag, details.get(VirtualMachineProfile.Param.UefiFlag.getName())); + } + + return toVmTO(vmProfile); + } + protected VirtualMachineTO getVmTO(Long vmId) { final VMInstanceVO vm = _vmDao.findById(vmId); final VirtualMachineProfile profile = new VirtualMachineProfileImpl(vm); @@ -6185,8 +6293,9 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac host = host == null ? _hostDao.findById(hostId) : host; if (host != null) { clusterId = host.getClusterId(); + return new Pair<>(clusterId, hostId); } - return new Pair<>(clusterId, hostId); + return findClusterAndHostIdForVmFromVolumes(vm.getId()); } private Pair findClusterAndHostIdForVm(VirtualMachine vm) { diff --git a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java index 4f6329f81cb..a07870d09af 100644 --- a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java +++ b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java @@ -19,14 +19,18 @@ package com.cloud.vm; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -36,6 +40,8 @@ import static org.mockito.Mockito.when; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -43,15 +49,29 @@ import java.util.Random; import java.util.UUID; import java.util.stream.Collectors; +import com.cloud.agent.api.UnmanageInstanceAnswer; +import com.cloud.agent.api.UnmanageInstanceCommand; +import com.cloud.agent.api.to.DataTO; +import com.cloud.agent.api.to.DiskTO; +import com.cloud.api.ApiDBUtils; +import com.cloud.event.ActionEventUtils; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.ha.HighAvailabilityManager; +import com.cloud.network.Network; +import com.cloud.network.NetworkModel; import com.cloud.resource.ResourceManager; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.impl.ConfigDepotImpl; import org.apache.cloudstack.framework.extensions.dao.ExtensionDetailsDao; import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; import org.apache.cloudstack.framework.extensions.vo.ExtensionDetailsVO; +import org.apache.cloudstack.framework.jobs.dao.VmWorkJobDao; +import org.apache.cloudstack.framework.jobs.impl.VmWorkJobVO; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; @@ -61,6 +81,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -167,6 +188,9 @@ public class VirtualMachineManagerImplTest { private PrimaryDataStoreDao storagePoolDaoMock; @Mock private VMInstanceVO vmInstanceMock; + @Mock + private VmWorkJobDao _workJobDao; + private long vmInstanceVoMockId = 1L; @Mock @@ -181,6 +205,9 @@ public class VirtualMachineManagerImplTest { private long hostMockId = 1L; private long clusterMockId = 2L; private long zoneMockId = 3L; + private final String vmMockUuid = UUID.randomUUID().toString(); + private final String hostUuid = UUID.randomUUID().toString(); + @Mock private HostVO hostMock; @Mock @@ -192,6 +219,7 @@ public class VirtualMachineManagerImplTest { private StoragePoolVO storagePoolVoMock; private long storagePoolVoMockId = 11L; private long storagePoolVoMockClusterId = 234L; + private String vmName = "vm1"; @Mock private VolumeVO volumeVoMock; @@ -254,6 +282,16 @@ public class VirtualMachineManagerImplTest { NicDao _nicsDao; @Mock NetworkService networkService; + @Mock + NetworkModel networkModel; + @Mock + VolumeDataFactory volumeDataFactoryMock; + @Mock + StorageManager storageManager; + @Mock + private HighAvailabilityManager _haMgr; + @Mock + VirtualMachineGuru guru; private ConfigDepotImpl configDepotImpl; private boolean updatedConfigKeyDepot = false; @@ -263,6 +301,7 @@ public class VirtualMachineManagerImplTest { ReflectionTestUtils.getField(VirtualMachineManager.VmMetadataManufacturer, "s_depot"); virtualMachineManagerImpl.setHostAllocators(new ArrayList<>()); + when(vmInstanceMock.getName()).thenReturn(vmName); when(vmInstanceMock.getId()).thenReturn(vmInstanceVoMockId); when(vmInstanceMock.getServiceOfferingId()).thenReturn(2L); when(hostMock.getId()).thenReturn(hostMockId); @@ -1361,7 +1400,7 @@ public class VirtualMachineManagerImplTest { Mockito.doReturn(HypervisorType.KVM).when(vmInstanceMock).getHypervisorType(); Mockito.doReturn(List.of(new VolumeObjectTO())).when(virtualMachineManagerImpl).getVmVolumesWithCheckpointsToRecreate(Mockito.any()); - Mockito.doThrow(new AgentUnavailableException(0)).when(agentManagerMock).send(Mockito.anyLong(), (Command) any()); + doThrow(new AgentUnavailableException(0)).when(agentManagerMock).send(Mockito.anyLong(), (Command) any()); Mockito.doNothing().when(snapshotManagerMock).endSnapshotChainForVolume(Mockito.anyLong(), Mockito.any()); virtualMachineManagerImpl.recreateCheckpointsKvmOnVmAfterMigration(vmInstanceMock, 0); @@ -1374,7 +1413,7 @@ public class VirtualMachineManagerImplTest { Mockito.doReturn(HypervisorType.KVM).when(vmInstanceMock).getHypervisorType(); Mockito.doReturn(List.of(new VolumeObjectTO())).when(virtualMachineManagerImpl).getVmVolumesWithCheckpointsToRecreate(Mockito.any()); - Mockito.doThrow(new OperationTimedoutException(null, 0, 0, 0, false)).when(agentManagerMock).send(Mockito.anyLong(), (Command) any()); + doThrow(new OperationTimedoutException(null, 0, 0, 0, false)).when(agentManagerMock).send(Mockito.anyLong(), (Command) any()); Mockito.doNothing().when(snapshotManagerMock).endSnapshotChainForVolume(Mockito.anyLong(), Mockito.any()); virtualMachineManagerImpl.recreateCheckpointsKvmOnVmAfterMigration(vmInstanceMock, 0); @@ -1641,4 +1680,278 @@ public class VirtualMachineManagerImplTest { virtualMachineManagerImpl.processPrepareExternalProvisioning(true, host, vmProfile, mock(DataCenter.class)); verify(agentManagerMock).send(anyLong(), any(Command.class)); } + + @Test + public void testPrepVMSpecForUnmanageInstance() { + // Arrange + final Long accountId = 1L; + final Long offeringId = 1L; + final Long templateId = 1L; + + // Mock vm + VMInstanceVO vm = Mockito.mock(VMInstanceVO.class); + when(vm.getId()).thenReturn(vmInstanceVoMockId); + when(vm.getAccountId()).thenReturn(accountId); + when(vm.getServiceOfferingId()).thenReturn(offeringId); + when(vm.getTemplateId()).thenReturn(templateId); + when(vm.getHypervisorType()).thenReturn(HypervisorType.KVM); + when(vmInstanceDaoMock.findById(vmInstanceVoMockId)).thenReturn(vm); + + // Mock owner + AccountVO owner = Mockito.mock(AccountVO.class); + when(_entityMgr.findById(Account.class, accountId)).thenReturn(owner); + + ServiceOfferingVO offering = Mockito.mock(ServiceOfferingVO.class); + when(serviceOfferingDaoMock.findById(vmInstanceVoMockId, offeringId)).thenReturn(offering); + + VMTemplateVO template = Mockito.mock(VMTemplateVO.class); + when(_entityMgr.findByIdIncludingRemoved(VirtualMachineTemplate.class, templateId)).thenReturn(template); + + when(hostMock.getClusterId()).thenReturn(clusterMockId); + + // Mock cpuOvercommitRatio and ramOvercommitRatio + ClusterDetailsVO cpuOvercommitRatio = Mockito.mock(ClusterDetailsVO.class); + when(cpuOvercommitRatio.getValue()).thenReturn("1.0"); + when(_clusterDetailsDao.findDetail(clusterMockId, VmDetailConstants.CPU_OVER_COMMIT_RATIO)).thenReturn(cpuOvercommitRatio); + ClusterDetailsVO ramOvercommitRatio = Mockito.mock(ClusterDetailsVO.class); + when(ramOvercommitRatio.getValue()).thenReturn("1.0"); + when(_clusterDetailsDao.findDetail(clusterMockId, VmDetailConstants.MEMORY_OVER_COMMIT_RATIO)).thenReturn(ramOvercommitRatio); + + // Mock NICs + List nics = new ArrayList<>(); + NicVO nic1 = Mockito.mock(NicVO.class); + when(nic1.getDeviceId()).thenReturn(1); + nics.add(nic1); + NicVO nic2 = Mockito.mock(NicVO.class); + when(nic2.getDeviceId()).thenReturn(0); + nics.add(nic2); + when(_nicsDao.listByVmId(vmInstanceVoMockId)).thenReturn(nics); + + Network networkMock = Mockito.mock(Network.class); + when(networkModel.getNetwork(anyLong())).thenReturn(networkMock); + + when(volumeVoMock.getVolumeType()).thenReturn(Volume.Type.ROOT); + when(volumeVoMock.getDeviceId()).thenReturn(0L); + when(volumeVoMock.getPath()).thenReturn("/"); + when(volumeVoMock.getDiskOfferingId()).thenReturn(1L); + when(volumeDaoMock.findUsableVolumesForInstance(vmInstanceVoMockId)).thenReturn(List.of(volumeVoMock)); + + VolumeInfo volumeInfo = mock(VolumeInfo.class); + DataTO dataTO = mock(DataTO.class); + when(volumeInfo.getTO()).thenReturn(dataTO); + when(volumeDataFactoryMock.getVolume(anyLong())).thenReturn(volumeInfo); + when(storageManager.getDiskWithThrottling(any(), any(), anyLong(), anyString(), anyLong(), anyLong())).thenReturn(Mockito.mock(DiskTO.class)); + + Map details = new HashMap<>(); + details.put(VirtualMachineProfile.Param.BootType.getName(), "BIOS"); + details.put(VirtualMachineProfile.Param.BootMode.getName(), "LEGACY"); + details.put(VirtualMachineProfile.Param.UefiFlag.getName(), "Yes"); + when(vmInstanceDetailsDao.listDetailsKeyPairs(anyLong(), anyList())).thenReturn(details); + + com.cloud.hypervisor.HypervisorGuru guru = Mockito.mock(com.cloud.hypervisor.HypervisorGuru.class); + when(_hvGuruMgr.getGuru(HypervisorType.KVM)).thenReturn(guru); + VirtualMachineTO vmTO = new VirtualMachineTO() {}; + when(guru.implement(any(VirtualMachineProfile.class))).thenAnswer((Answer) invocation -> { + VirtualMachineProfile profile = invocation.getArgument(0); + assertEquals("BIOS", profile.getParameter(VirtualMachineProfile.Param.BootType)); + return vmTO; + }); + + // Act + VirtualMachineTO result = virtualMachineManagerImpl.prepVmSpecForUnmanageCmd(vmInstanceVoMockId, hostMockId); + + // Assert + assertNotNull(result); + assertEquals(vmTO, result); + verify(_clusterDetailsDao, times(2)).findDetail(eq(clusterMockId), anyString()); + verify(vmInstanceDetailsDao).listDetailsKeyPairs(anyLong(), anyList()); + } + + @Test + public void testPersistDomainForKvmForRunningVmSuccess() throws AgentUnavailableException, OperationTimedoutException { + when(vmInstanceMock.getState()).thenReturn(VirtualMachine.State.Running); + when(vmInstanceMock.getHostId()).thenReturn(hostMockId); + UnmanageInstanceAnswer successAnswer = new UnmanageInstanceAnswer(null, true, "success"); + when(agentManagerMock.send(anyLong(), any(Command.class))).thenReturn(successAnswer); + virtualMachineManagerImpl.persistDomainForKVM(vmInstanceMock, null); + ArgumentCaptor hostIdCaptor = ArgumentCaptor.forClass(Long.class); + ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(UnmanageInstanceCommand.class); + verify(agentManagerMock).send(hostIdCaptor.capture(), commandCaptor.capture()); + assertEquals(hostMockId, hostIdCaptor.getValue().longValue()); + } + + @Test + public void testPersistDomainForKvmForStoppedVmSuccess() throws AgentUnavailableException, OperationTimedoutException { + when(vmInstanceMock.getState()).thenReturn(VirtualMachine.State.Stopped); + VirtualMachineTO vmTO = new VirtualMachineTO() {}; + vmTO.setName(vmName); + doReturn(vmTO).when(virtualMachineManagerImpl).prepVmSpecForUnmanageCmd(vmInstanceVoMockId, 1L); + UnmanageInstanceAnswer successAnswer = new UnmanageInstanceAnswer(null, true, "success"); + when(agentManagerMock.send(anyLong(), any(UnmanageInstanceCommand.class))).thenReturn(successAnswer); + when(virtualMachineManagerImpl.findClusterAndHostIdForVm(vmInstanceMock, false)).thenReturn(new Pair<>(clusterMockId, hostMockId)); + virtualMachineManagerImpl.persistDomainForKVM(vmInstanceMock, null); + ArgumentCaptor hostIdCaptor = ArgumentCaptor.forClass(Long.class); + ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(UnmanageInstanceCommand.class); + verify(agentManagerMock).send(hostIdCaptor.capture(), commandCaptor.capture()); + assertEquals(1L, hostIdCaptor.getValue().longValue()); + UnmanageInstanceCommand sentCommand = commandCaptor.getValue(); + assertNotNull(sentCommand.getVm()); + assertEquals(vmTO, sentCommand.getVm()); + assertEquals(vmName, sentCommand.getInstanceName()); + verify(virtualMachineManagerImpl).prepVmSpecForUnmanageCmd(vmInstanceVoMockId, 1L); + } + + + @Test + public void testPersistDomainForKvmForStoppedVmNoHost() { + when(vmInstanceMock.getState()).thenReturn(VirtualMachine.State.Stopped); + VirtualMachineTO vmTO = new VirtualMachineTO() {}; + vmTO.setName(vmName); + when(virtualMachineManagerImpl.findClusterAndHostIdForVm(vmInstanceMock, false)).thenReturn(new Pair<>(clusterMockId, null)); + CloudRuntimeException exception = assertThrows(CloudRuntimeException.class, () -> virtualMachineManagerImpl.persistDomainForKVM(vmInstanceMock, null)); + assertEquals("No available host to persist domain XML for Instance: " + vmName, exception.getMessage()); + } + + @Test + public void testPersistDomainForKvmForRunningVmAgentFailure() throws AgentUnavailableException, OperationTimedoutException { + when(vmInstanceMock.getState()).thenReturn(VirtualMachine.State.Running); + when(vmInstanceMock.getHostId()).thenReturn(hostMockId); + UnmanageInstanceAnswer failureAnswer = new UnmanageInstanceAnswer(null, false, "failure"); + when(agentManagerMock.send(anyLong(), any(UnmanageInstanceCommand.class))).thenReturn(failureAnswer); + CloudRuntimeException exception = assertThrows(CloudRuntimeException.class, () -> virtualMachineManagerImpl.persistDomainForKVM(vmInstanceMock, null)); + assertEquals("Failed to persist domain XML for Instance: " + vmName + " on host ID: " + hostMockId, exception.getMessage()); + } + + @Test + public void testPersistDomainForKvmAgentUnavailable() throws AgentUnavailableException, OperationTimedoutException { + when(vmInstanceMock.getState()).thenReturn(VirtualMachine.State.Running); + when(vmInstanceMock.getHostId()).thenReturn(hostMockId); + doThrow(new AgentUnavailableException("Agent down", hostMockId)).when(agentManagerMock).send(anyLong(), any(UnmanageInstanceCommand.class)); + CloudRuntimeException exception = assertThrows(CloudRuntimeException.class, () -> virtualMachineManagerImpl.persistDomainForKVM(vmInstanceMock, null)); + assertEquals("Failed to send command to persist domain XML for Instance: " + vmName + " on host ID: " + hostMockId, exception.getMessage()); + } + + @Test(expected = ConcurrentOperationException.class) + public void testUnmanagePendingHaWork() { + when(vmInstanceDaoMock.findByUuid(vmMockUuid)).thenReturn(vmInstanceMock); + when(_workJobDao.listPendingWorkJobs(VirtualMachine.Type.Instance, vmInstanceVoMockId)).thenReturn(Collections.emptyList()); + when(_haMgr.hasPendingHaWork(vmInstanceVoMockId)).thenReturn(true); + virtualMachineManagerImpl.unmanage(vmMockUuid, null); + } + + @Test + public void testPersistDomainForKvmOperationTimedOut() throws AgentUnavailableException, OperationTimedoutException { + when(vmInstanceMock.getState()).thenReturn(VirtualMachine.State.Running); + when(vmInstanceMock.getHostId()).thenReturn(hostMockId); + doThrow(new OperationTimedoutException(null, hostMockId, 123L, 60, false)).when(agentManagerMock).send(anyLong(), any(UnmanageInstanceCommand.class)); + CloudRuntimeException exception = assertThrows(CloudRuntimeException.class, () -> virtualMachineManagerImpl.persistDomainForKVM(vmInstanceMock, null)); + assertEquals("Failed to send command to persist domain XML for Instance: " + vmName + " on host ID: " + hostMockId, exception.getMessage()); + } + + @Test(expected = CloudRuntimeException.class) + public void testUnmanageVmRemoved() { + when(vmInstanceMock.getRemoved()).thenReturn(new Date()); + when(vmInstanceDaoMock.findByUuid(vmMockUuid)).thenReturn(vmInstanceMock); + virtualMachineManagerImpl.unmanage(vmMockUuid, null); + } + + @Test(expected = ConcurrentOperationException.class) + public void testUnmanagePendingWorkJobs() { + when(vmInstanceDaoMock.findByUuid(vmMockUuid)).thenReturn(vmInstanceMock); + List pendingJobs = new ArrayList<>(); + VmWorkJobVO vmWorkJobVO = mock(VmWorkJobVO.class); + pendingJobs.add(vmWorkJobVO); + when(_workJobDao.listPendingWorkJobs(VirtualMachine.Type.Instance, vmInstanceVoMockId)).thenReturn(pendingJobs); + virtualMachineManagerImpl.unmanage(vmMockUuid, null); + } + + @Test + public void testUnmanageHostNotFoundAfterTransaction() { + when(vmInstanceMock.getHostId()).thenReturn(hostMockId); + when(vmInstanceDaoMock.findByUuid(vmMockUuid)).thenReturn(vmInstanceMock); + when(_workJobDao.listPendingWorkJobs(any(), anyLong())).thenReturn(Collections.emptyList()); + when(_haMgr.hasPendingHaWork(anyLong())).thenReturn(false); + doReturn(guru).when(virtualMachineManagerImpl).getVmGuru(vmInstanceMock); + doNothing().when(virtualMachineManagerImpl).unmanageVMSnapshots(vmInstanceMock); + doNothing().when(virtualMachineManagerImpl).unmanageVMNics(any(VirtualMachineProfile.class), any(VMInstanceVO.class)); + doNothing().when(virtualMachineManagerImpl).unmanageVMVolumes(vmInstanceMock); + doNothing().when(guru).finalizeUnmanage(vmInstanceMock); + try (MockedStatic ignored = Mockito.mockStatic(ApiDBUtils.class)) { + when(ApiDBUtils.findHostById(hostMockId)).thenReturn(null); + Pair result = virtualMachineManagerImpl.unmanage(vmMockUuid, null); + assertNull(result.second()); + } + } + + @Test + public void testUnmanageSuccessNonKvm() { + when(vmInstanceMock.getHostId()).thenReturn(hostMockId); + when(hostMock.getUuid()).thenReturn(hostUuid); + when(vmInstanceDaoMock.findByUuid(vmMockUuid)).thenReturn(vmInstanceMock); + when(_workJobDao.listPendingWorkJobs(any(), anyLong())).thenReturn(Collections.emptyList()); + when(_haMgr.hasPendingHaWork(anyLong())).thenReturn(false); + doReturn(guru).when(virtualMachineManagerImpl).getVmGuru(vmInstanceMock); + doNothing().when(virtualMachineManagerImpl).unmanageVMSnapshots(vmInstanceMock); + doNothing().when(virtualMachineManagerImpl).unmanageVMNics(any(VirtualMachineProfile.class), any(VMInstanceVO.class)); + doNothing().when(virtualMachineManagerImpl).unmanageVMVolumes(vmInstanceMock); + doNothing().when(guru).finalizeUnmanage(vmInstanceMock); + try (MockedStatic ignored = Mockito.mockStatic(ApiDBUtils.class)) { + when(ApiDBUtils.findHostById(hostMockId)).thenReturn(hostMock); + try (MockedStatic actionUtil = Mockito.mockStatic(ActionEventUtils.class)) { + actionUtil.when(() -> ActionEventUtils.onActionEvent( + anyLong(), anyLong(), anyLong(), + anyString(), anyString(), + anyLong(), anyString() + )).thenReturn(1L); + + Pair result = virtualMachineManagerImpl.unmanage(vmMockUuid, null); + assertNotNull(result); + assertTrue(result.first()); + assertEquals(hostUuid, result.second()); + + verify(virtualMachineManagerImpl, never()).persistDomainForKVM(any(VMInstanceVO.class), anyLong()); + verify(virtualMachineManagerImpl, times(1)).unmanageVMSnapshots(vmInstanceMock); + verify(virtualMachineManagerImpl, times(1)).unmanageVMNics(any(VirtualMachineProfile.class), any(VMInstanceVO.class)); + verify(virtualMachineManagerImpl, times(1)).unmanageVMVolumes(vmInstanceMock); + verify(guru, times(1)).finalizeUnmanage(vmInstanceMock); + } + } + } + + @Test + public void testUnmanageSuccessKvm() throws Exception { + when(vmInstanceMock.getHostId()).thenReturn(hostMockId); + when(hostMock.getUuid()).thenReturn(hostUuid); + when(vmInstanceMock.getHypervisorType()).thenReturn(HypervisorType.KVM); + when(vmInstanceDaoMock.findByUuid(vmMockUuid)).thenReturn(vmInstanceMock); + when(_workJobDao.listPendingWorkJobs(any(), anyLong())).thenReturn(Collections.emptyList()); + when(_haMgr.hasPendingHaWork(anyLong())).thenReturn(false); + doReturn(guru).when(virtualMachineManagerImpl).getVmGuru(vmInstanceMock); + doNothing().when(virtualMachineManagerImpl).unmanageVMSnapshots(vmInstanceMock); + doNothing().when(virtualMachineManagerImpl).unmanageVMNics(any(VirtualMachineProfile.class), any(VMInstanceVO.class)); + doNothing().when(virtualMachineManagerImpl).unmanageVMVolumes(vmInstanceMock); + doNothing().when(guru).finalizeUnmanage(vmInstanceMock); + try (MockedStatic ignored = Mockito.mockStatic(ApiDBUtils.class)) { + when(ApiDBUtils.findHostById(hostMockId)).thenReturn(hostMock); + try (MockedStatic actionUtil = Mockito.mockStatic(ActionEventUtils.class)) { + actionUtil.when(() -> ActionEventUtils.onActionEvent( + anyLong(), anyLong(), anyLong(), + anyString(), anyString(), + anyLong(), anyString() + )).thenReturn(1L); + UnmanageInstanceAnswer successAnswer = new UnmanageInstanceAnswer(null, true, "success"); + when(agentManagerMock.send(anyLong(), any(UnmanageInstanceCommand.class))).thenReturn(successAnswer); + Pair result = virtualMachineManagerImpl.unmanage(vmMockUuid, null); + assertNotNull(result); + assertTrue(result.first()); + assertEquals(hostUuid, result.second()); + verify(virtualMachineManagerImpl, times(1)).persistDomainForKVM(vmInstanceMock, null); + verify(virtualMachineManagerImpl, times(1)).unmanageVMSnapshots(vmInstanceMock); + verify(virtualMachineManagerImpl, times(1)).unmanageVMNics(any(VirtualMachineProfile.class), any(VMInstanceVO.class)); + verify(virtualMachineManagerImpl, times(1)).unmanageVMVolumes(vmInstanceMock); + verify(guru, times(1)).finalizeUnmanage(vmInstanceMock); + } + } + } + } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUnmanageInstanceCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUnmanageInstanceCommandWrapper.java new file mode 100644 index 00000000000..33ad274b8d1 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUnmanageInstanceCommandWrapper.java @@ -0,0 +1,174 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.apache.cloudstack.utils.security.ParserUtils; +import org.apache.commons.io.IOUtils; +import org.libvirt.Connect; +import org.libvirt.Domain; +import org.libvirt.LibvirtException; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.UnmanageInstanceAnswer; +import com.cloud.agent.api.UnmanageInstanceCommand; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.exception.InternalErrorException; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtKvmAgentHook; +import com.cloud.hypervisor.kvm.resource.LibvirtVMDef; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; + +@ResourceWrapper(handles = UnmanageInstanceCommand.class) +public final class LibvirtUnmanageInstanceCommandWrapper extends CommandWrapper { + + + @Override + public Answer execute(final UnmanageInstanceCommand command, final LibvirtComputingResource libvirtComputingResource) { + String instanceName = command.getInstanceName(); + VirtualMachineTO vmSpec = command.getVm(); + final LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper(); + logger.debug("Attempting to unmanage KVM instance: {}", instanceName); + Domain dom = null; + Connect conn = null; + String vmFinalSpecification; + try { + if (vmSpec == null) { + conn = libvirtUtilitiesHelper.getConnectionByVmName(instanceName); + dom = conn.domainLookupByName(instanceName); + vmFinalSpecification = dom.getXMLDesc(1); + if (command.isConfigDriveAttached()) { + vmFinalSpecification = cleanupConfigDrive(vmFinalSpecification, instanceName); + } + } else { + // define domain using reconstructed vmSpec + logger.debug("Unmanaging Stopped KVM instance: {}", instanceName); + LibvirtVMDef vm = libvirtComputingResource.createVMFromSpec(vmSpec); + libvirtComputingResource.createVbd(conn, vmSpec, instanceName, vm); + conn = libvirtUtilitiesHelper.getConnectionByType(vm.getHvsType()); + String vmInitialSpecification = vm.toString(); + vmFinalSpecification = performXmlTransformHook(vmInitialSpecification, libvirtComputingResource); + } + conn.domainDefineXML(vmFinalSpecification).free(); + logger.debug("Successfully unmanaged KVM instance: {} with domain XML: {}", instanceName, vmFinalSpecification); + return new UnmanageInstanceAnswer(command, true, "Successfully unmanaged"); + } catch (final LibvirtException e) { + logger.error("LibvirtException occurred during unmanaging instance: {} ", instanceName, e); + return new UnmanageInstanceAnswer(command, false, e.getMessage()); + } catch (final IOException + | ParserConfigurationException + | SAXException + | TransformerException + | XPathExpressionException + | InternalErrorException + | URISyntaxException e) { + + logger.error("Failed to unmanage Instance: {}.", instanceName, e); + return new UnmanageInstanceAnswer(command, false, e.getMessage()); + } finally { + if (dom != null) { + try { + dom.free(); + } catch (LibvirtException e) { + logger.error("Ignore libvirt error on free.", e); + } + } + } + } + + String cleanupConfigDrive(String domainXML, String instanceName) throws ParserConfigurationException, IOException, SAXException, XPathExpressionException, TransformerException { + String isoName = "/" + instanceName + ".iso"; + DocumentBuilderFactory docFactory = ParserUtils.getSaferDocumentBuilderFactory(); + DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); + Document document; + try (InputStream inputStream = IOUtils.toInputStream(domainXML, StandardCharsets.UTF_8)) { + document = docBuilder.parse(inputStream); + } + XPathFactory xPathFactory = XPathFactory.newInstance(); + XPath xpath = xPathFactory.newXPath(); + + // Find all elements with source file containing instanceName.iso + String expression = String.format("//disk[@device='cdrom'][source/@file[contains(., '%s')]]", isoName); + NodeList cdromDisks = (NodeList) xpath.evaluate(expression, document, XPathConstants.NODESET); + + // If nothing matched, return original XML + if (cdromDisks == null || cdromDisks.getLength() == 0) { + logger.debug("No config drive found in domain XML for Instance: {}", instanceName); + return domainXML; + } + + // Remove all matched config drive disks + for (int i = 0; i < cdromDisks.getLength(); i++) { + Node diskNode = cdromDisks.item(i); + if (diskNode != null && diskNode.getParentNode() != null) { + diskNode.getParentNode().removeChild(diskNode); + } + } + logger.debug("Removed {} config drive ISO CD-ROM entries for instance: {}", cdromDisks.getLength(), instanceName); + + TransformerFactory transformerFactory = ParserUtils.getSaferTransformerFactory(); + Transformer transformer = transformerFactory.newTransformer(); + DOMSource source = new DOMSource(document); + StringWriter output = new StringWriter(); + StreamResult result = new StreamResult(output); + transformer.transform(source, result); + return output.toString(); + } + + private String performXmlTransformHook(String vmInitialSpecification, final LibvirtComputingResource libvirtComputingResource) { + String vmFinalSpecification; + try { + // if transformer fails, everything must go as it's just skipped. + LibvirtKvmAgentHook t = libvirtComputingResource.getTransformer(); + vmFinalSpecification = (String) t.handle(vmInitialSpecification); + if (null == vmFinalSpecification) { + logger.warn("Libvirt XML transformer returned NULL, will use XML specification unchanged."); + vmFinalSpecification = vmInitialSpecification; + } + } catch(Exception e) { + logger.warn("Exception occurred when handling LibVirt XML transformer hook: {}", e); + vmFinalSpecification = vmInitialSpecification; + } + return vmFinalSpecification; + } +} diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUnmanageInstanceCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUnmanageInstanceCommandWrapperTest.java new file mode 100644 index 00000000000..13ddc26610c --- /dev/null +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUnmanageInstanceCommandWrapperTest.java @@ -0,0 +1,357 @@ +// +// 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.kvm.resource.wrapper; + +import java.io.IOException; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.xpath.XPathExpressionException; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.xml.sax.SAXException; + + +@RunWith(MockitoJUnitRunner.class) +public class LibvirtUnmanageInstanceCommandWrapperTest { + @Spy + LibvirtUnmanageInstanceCommandWrapper unmanageInstanceCommandWrapper = new LibvirtUnmanageInstanceCommandWrapper(); + + @Test + public void testCleanupConfigDriveFromDomain() throws XPathExpressionException, ParserConfigurationException, IOException, TransformerException, SAXException { + String domainXML = "\n" + + " i-2-6-VM\n" + + " 071628d0-84f1-421e-a9cf-d18bca2283bc\n" + + " CentOS 5.5 (64-bit)\n" + + " 524288\n" + + " 524288\n" + + " 1\n" + + " \n" + + " 250\n" + + " \n" + + " \n" + + " /machine\n" + + " \n" + + " \n" + + " \n" + + " Apache Software Foundation\n" + + " CloudStack KVM Hypervisor\n" + + " 071628d0-84f1-421e-a9cf-d18bca2283bc\n" + + " 071628d0-84f1-421e-a9cf-d18bca2283bc\n" + + " \n" + + " \n" + + " \n" + + " hvm\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " qemu64\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " destroy\n" + + " restart\n" + + " destroy\n" + + " \n" + + " /usr/libexec/qemu-kvm\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 9329e4fa9db546c78b1a\n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "