From 30cb8c7a82005e334f939956d8db6546fa02b5c0 Mon Sep 17 00:00:00 2001 From: Nicolas Vazquez Date: Tue, 30 Sep 2025 04:01:07 -0300 Subject: [PATCH 001/156] 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 002/156] 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 c631d6a480dfc89475c14bead84ae309c78d3d81 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Wed, 1 Oct 2025 08:47:58 +0200 Subject: [PATCH 003/156] 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 ca7138b3bdf73dafce147d0a730d41db621a7e9b Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Thu, 2 Oct 2025 11:43:48 +0530 Subject: [PATCH 004/156] 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 005/156] 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 006/156] 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 007/156] 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 008/156] 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 009/156] 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 010/156] 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 011/156] 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 012/156] 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 013/156] 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" + + "