From bf464585785b6c0848fb4b07953e12d1039af4ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernardo=20De=20Marco=20Gon=C3=A7alves?= Date: Thu, 17 Jul 2025 04:01:49 -0300 Subject: [PATCH 1/7] List templates and ISOs by domain (#11179) --- .../com/cloud/api/query/QueryManagerImpl.java | 28 +++++++++++++------ ui/src/components/view/InfoCard.vue | 3 ++ ui/src/config/section/domain.js | 5 ++++ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index d0f6fc0b16d..1d8e8687051 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -29,6 +29,7 @@ import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -4443,6 +4444,8 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q boolean showRemovedTmpl = cmd.getShowRemoved(); Account caller = CallContext.current().getCallingAccount(); Long parentTemplateId = cmd.getParentTemplateId(); + Long domainId = cmd.getDomainId(); + boolean isRecursive = cmd.isRecursive(); boolean listAll = false; if (templateFilter != null && templateFilter == TemplateFilter.all) { @@ -4453,7 +4456,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q } List permittedAccountIds = new ArrayList(); - Ternary domainIdRecursiveListProject = new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null); + Ternary domainIdRecursiveListProject = new Ternary<>(domainId, isRecursive, null); accountMgr.buildACLSearchParameters( caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccountIds, domainIdRecursiveListProject, listAll, false @@ -4481,7 +4484,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q null, cmd.getPageSizeVal(), cmd.getStartIndex(), cmd.getZoneId(), cmd.getStoragePoolId(), cmd.getImageStoreId(), hypervisorType, showDomr, cmd.listInReadyState(), permittedAccounts, caller, listProjectResourcesCriteria, tags, showRemovedTmpl, cmd.getIds(), parentTemplateId, cmd.getShowUnique(), - templateType, isVnf); + templateType, isVnf, domainId, isRecursive); } private Pair, Integer> searchForTemplatesInternal(Long templateId, String name, String keyword, @@ -4490,7 +4493,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q boolean showDomr, boolean onlyReady, List permittedAccounts, Account caller, ListProjectResourcesCriteria listProjectResourcesCriteria, Map tags, boolean showRemovedTmpl, List ids, Long parentTemplateId, Boolean showUnique, String templateType, - Boolean isVnf) { + Boolean isVnf, Long domainId, boolean isRecursive) { // check if zone is configured, if not, just return empty list List hypers = null; @@ -4572,7 +4575,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q if (!permittedAccounts.isEmpty()) { domain = _domainDao.findById(permittedAccounts.get(0).getDomainId()); } else { - domain = _domainDao.findById(caller.getDomainId()); + domain = _domainDao.findById(Objects.requireNonNullElse(domainId, caller.getDomainId())); } setIdsListToSearchCriteria(sc, ids); @@ -4584,10 +4587,14 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q sc.addAnd("accountType", SearchCriteria.Op.EQ, Account.Type.PROJECT); } - // add criteria for domain path in case of domain admin + // add criteria for domain path in case of admins if ((templateFilter == TemplateFilter.self || templateFilter == TemplateFilter.selfexecutable) - && (caller.getType() == Account.Type.DOMAIN_ADMIN || caller.getType() == Account.Type.RESOURCE_DOMAIN_ADMIN)) { - sc.addAnd("domainPath", SearchCriteria.Op.LIKE, domain.getPath() + "%"); + && (accountMgr.isAdmin(caller.getAccountId()))) { + if (isRecursive) { + sc.addAnd("domainPath", SearchCriteria.Op.LIKE, domain.getPath() + "%"); + } else { + sc.addAnd("domainPath", SearchCriteria.Op.EQ, domain.getPath()); + } } List relatedDomainIds = new ArrayList(); @@ -4893,6 +4900,8 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q Map tags = cmd.getTags(); boolean showRemovedISO = cmd.getShowRemoved(); Account caller = CallContext.current().getCallingAccount(); + Long domainId = cmd.getDomainId(); + boolean isRecursive = cmd.isRecursive(); boolean listAll = false; if (isoFilter != null && isoFilter == TemplateFilter.all) { @@ -4904,7 +4913,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q List permittedAccountIds = new ArrayList<>(); - Ternary domainIdRecursiveListProject = new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null); + Ternary domainIdRecursiveListProject = new Ternary<>(domainId, isRecursive, null); accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccountIds, domainIdRecursiveListProject, listAll, false); ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); List permittedAccounts = new ArrayList<>(); @@ -4917,7 +4926,8 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q return searchForTemplatesInternal(cmd.getId(), cmd.getIsoName(), cmd.getKeyword(), isoFilter, true, cmd.isBootable(), cmd.getPageSizeVal(), cmd.getStartIndex(), cmd.getZoneId(), cmd.getStoragePoolId(), cmd.getImageStoreId(), hypervisorType, true, cmd.listInReadyState(), permittedAccounts, caller, listProjectResourcesCriteria, - tags, showRemovedISO, null, null, cmd.getShowUnique(), null, null); + tags, showRemovedISO, null, null, cmd.getShowUnique(), null, null, + domainId, isRecursive); } @Override diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue index 00cb4748a88..e59735c51ea 100644 --- a/ui/src/components/view/InfoCard.vue +++ b/ui/src/components/view/InfoCard.vue @@ -1162,6 +1162,9 @@ export default { if (item.name === 'template') { query.templatefilter = 'self' query.filter = 'self' + } else if (item.name === 'iso') { + query.isofilter = 'self' + query.filter = 'self' } if (item.param === 'account') { diff --git a/ui/src/config/section/domain.js b/ui/src/config/section/domain.js index e6807f06278..fbe20ef8891 100644 --- a/ui/src/config/section/domain.js +++ b/ui/src/config/section/domain.js @@ -48,6 +48,11 @@ export default { name: 'template', title: 'label.templates', param: 'domainid' + }, + { + name: 'iso', + title: 'label.isos', + param: 'domainid' }], tabs: [ { From 714b04e3a5fec5bf0aaa6ec4d956a052cea71ab7 Mon Sep 17 00:00:00 2001 From: dahn Date: Thu, 17 Jul 2025 14:25:00 +0200 Subject: [PATCH 2/7] npe guard for get host info on vmware (#11054) Co-authored-by: Daan Hoogland --- .../main/java/com/cloud/hypervisor/vmware/mo/HostMO.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/HostMO.java b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/HostMO.java index 3b96e7e1999..93a471d56bb 100644 --- a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/HostMO.java +++ b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/HostMO.java @@ -75,7 +75,7 @@ import com.cloud.utils.Pair; public class HostMO extends BaseMO implements VmwareHypervisorHost { private static final Logger s_logger = Logger.getLogger(HostMO.class); - Map _vmCache = new HashMap(); + Map _vmCache = new HashMap<>(); //Map _vmInternalNameMapCache = new HashMap(); @@ -320,6 +320,11 @@ public class HostMO extends BaseMO implements VmwareHypervisorHost { public VmwareHostType getHostType() throws Exception { AboutInfo aboutInfo = getHostAboutInfo(); + if (aboutInfo == null) { + String msg = "no type info about host known, assuming ESXi"; + s_logger.warn(msg); + return VmwareHostType.ESXi; + } if ("VMware ESXi".equals(aboutInfo.getName())) return VmwareHostType.ESXi; else if ("VMware ESX".equals(aboutInfo.getName())) From 9c6dfd2b26457fecbb2f0676703756c40233949e Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Sun, 20 Jul 2025 21:31:52 +0530 Subject: [PATCH 3/7] Handle IllegalReferenceCountException for decoder, while uploading ISO from local (#10879) --- .../storage/resource/HttpUploadServerHandler.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/HttpUploadServerHandler.java b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/HttpUploadServerHandler.java index 9b1c81284f4..73103c37660 100644 --- a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/HttpUploadServerHandler.java +++ b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/resource/HttpUploadServerHandler.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import io.netty.util.IllegalReferenceCountException; import org.apache.cloudstack.storage.template.UploadEntity; import org.apache.cloudstack.utils.imagestore.ImageStoreUtil; import org.apache.commons.lang3.StringUtils; @@ -229,8 +230,15 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler Date: Mon, 21 Jul 2025 15:52:38 +0530 Subject: [PATCH 4/7] Add format and physicalsize in listIsoOs api response (#11214) --- .../java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java index 063766761db..26a18818dbf 100644 --- a/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java @@ -480,6 +480,7 @@ public class TemplateJoinDaoImpl extends GenericDaoBaseWithTagInformation 0) { isoResponse.setSize(isoSize); } + long isoPhysicalSize = iso.getPhysicalSize(); + if (isoPhysicalSize > 0) { + isoResponse.setPhysicalSize(isoPhysicalSize); + } if (iso.getUserDataId() != null) { isoResponse.setUserDataId(iso.getUserDataUUid()); From 264e404108ad3b35389e0a444db2fccb6592ae17 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Thu, 24 Jul 2025 09:30:20 +0530 Subject: [PATCH 5/7] Fix for dynamic scaling toggle for instance (#11086) * Fix for dynamic scaling toggle for instance * Update api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java --------- Co-authored-by: Suresh Kumar Anaparti --- .../command/user/config/ListCapabilitiesCmd.java | 1 + .../api/response/CapabilitiesResponse.java | 8 ++++++++ .../com/cloud/server/ManagementServerImpl.java | 1 + ui/src/views/compute/DeployVM.vue | 14 +++----------- ui/src/views/compute/DeployVnfAppliance.vue | 14 +++----------- ui/src/views/compute/EditVM.vue | 16 ++-------------- 6 files changed, 18 insertions(+), 36 deletions(-) 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 65920a97c98..4f036e89a64 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 @@ -71,6 +71,7 @@ public class ListCapabilitiesCmd extends BaseCmd { response.setInstancesStatsUserOnly((Boolean) capabilities.get(ApiConstants.INSTANCES_STATS_USER_ONLY)); response.setInstancesDisksStatsRetentionEnabled((Boolean) capabilities.get(ApiConstants.INSTANCES_DISKS_STATS_RETENTION_ENABLED)); response.setInstancesDisksStatsRetentionTime((Integer) capabilities.get(ApiConstants.INSTANCES_DISKS_STATS_RETENTION_TIME)); + response.setDynamicScalingEnabled((Boolean) capabilities.get(ApiConstants.DYNAMIC_SCALING_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 e4224c85e97..83fb4f4b372 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 @@ -124,6 +124,10 @@ public class CapabilitiesResponse extends BaseResponse { @Param(description = "the retention time for Instances disks stats", since = "4.18.0") private Integer instancesDisksStatsRetentionTime; + @SerializedName(ApiConstants.DYNAMIC_SCALING_ENABLED) + @Param(description = "true if dynamically scaling for instances is enabled", since = "4.21.0") + private Boolean dynamicScalingEnabled; + public void setSecurityGroupsEnabled(boolean securityGroupsEnabled) { this.securityGroupsEnabled = securityGroupsEnabled; } @@ -223,4 +227,8 @@ public class CapabilitiesResponse extends BaseResponse { public void setCustomHypervisorDisplayName(String customHypervisorDisplayName) { this.customHypervisorDisplayName = customHypervisorDisplayName; } + + public void setDynamicScalingEnabled(Boolean dynamicScalingEnabled) { + this.dynamicScalingEnabled = dynamicScalingEnabled; + } } diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 7ccd6b5e289..451c8f4e159 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -4408,6 +4408,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe capabilities.put(ApiConstants.INSTANCES_STATS_USER_ONLY, StatsCollector.vmStatsCollectUserVMOnly.value()); capabilities.put(ApiConstants.INSTANCES_DISKS_STATS_RETENTION_ENABLED, StatsCollector.vmDiskStatsRetentionEnabled.value()); capabilities.put(ApiConstants.INSTANCES_DISKS_STATS_RETENTION_TIME, StatsCollector.vmDiskStatsMaxRetentionTime.value()); + capabilities.put(ApiConstants.DYNAMIC_SCALING_ENABLED, UserVmManager.EnableDynamicallyScaleVm.value()); if (apiLimitEnabled) { capabilities.put("apiLimitInterval", apiLimitInterval); capabilities.put("apiLimitMax", apiLimitMax); diff --git a/ui/src/views/compute/DeployVM.vue b/ui/src/views/compute/DeployVM.vue index c99ffe4d321..30ac498c5c2 100644 --- a/ui/src/views/compute/DeployVM.vue +++ b/ui/src/views/compute/DeployVM.vue @@ -944,8 +944,7 @@ export default { keyboards: [], bootTypes: [], bootModes: [], - ioPolicyTypes: [], - dynamicScalingVmConfig: false + ioPolicyTypes: [] }, rowCount: {}, loading: { @@ -1175,13 +1174,6 @@ export default { type: 'Routing' }, field: 'hostid' - }, - dynamicScalingVmConfig: { - list: 'listConfigurations', - options: { - zoneid: _.get(this.zone, 'id'), - name: 'enable.dynamic.scale.vm' - } } } }, @@ -1312,7 +1304,7 @@ export default { return Boolean('listUserData' in this.$store.getters.apis) }, dynamicScalingVmConfigValue () { - return this.options.dynamicScalingVmConfig?.[0]?.value === 'true' + return this.$store.getters.features.dynamicscalingenabled }, isCustomizedDiskIOPS () { return this.diskSelected?.iscustomizediops || false @@ -2215,7 +2207,7 @@ export default { param.loading = true param.opts = [] const options = param.options || {} - if (!('listall' in options) && !['zones', 'pods', 'clusters', 'hosts', 'dynamicScalingVmConfig', 'hypervisors'].includes(name)) { + if (!('listall' in options) && !['zones', 'pods', 'clusters', 'hosts', 'hypervisors'].includes(name)) { options.listall = true } api(param.list, options).then((response) => { diff --git a/ui/src/views/compute/DeployVnfAppliance.vue b/ui/src/views/compute/DeployVnfAppliance.vue index 5e1baac3d8f..b69b67aa8d3 100644 --- a/ui/src/views/compute/DeployVnfAppliance.vue +++ b/ui/src/views/compute/DeployVnfAppliance.vue @@ -960,8 +960,7 @@ export default { keyboards: [], bootTypes: [], bootModes: [], - ioPolicyTypes: [], - dynamicScalingVmConfig: false + ioPolicyTypes: [] }, rowCount: {}, loading: { @@ -1194,13 +1193,6 @@ export default { type: 'Routing' }, field: 'hostid' - }, - dynamicScalingVmConfig: { - list: 'listConfigurations', - options: { - zoneid: _.get(this.zone, 'id'), - name: 'enable.dynamic.scale.vm' - } } } }, @@ -1332,7 +1324,7 @@ export default { return Boolean('listUserData' in this.$store.getters.apis) }, dynamicScalingVmConfigValue () { - return this.options.dynamicScalingVmConfig?.[0]?.value === 'true' + return this.$store.getters.features.dynamicscalingenabled }, isCustomizedDiskIOPS () { return this.diskSelected?.iscustomizediops || false @@ -2403,7 +2395,7 @@ export default { param.loading = true param.opts = [] const options = param.options || {} - if (!('listall' in options) && !['zones', 'pods', 'clusters', 'hosts', 'dynamicScalingVmConfig', 'hypervisors'].includes(name)) { + if (!('listall' in options) && !['zones', 'pods', 'clusters', 'hosts', 'hypervisors'].includes(name)) { options.listall = true } api(param.list, options).then((response) => { diff --git a/ui/src/views/compute/EditVM.vue b/ui/src/views/compute/EditVM.vue index 92079b84113..90cc96dd9ef 100644 --- a/ui/src/views/compute/EditVM.vue +++ b/ui/src/views/compute/EditVM.vue @@ -145,7 +145,6 @@ export default { template: {}, userDataEnabled: false, securityGroupsEnabled: false, - dynamicScalingVmConfig: false, loading: false, securitygroups: { loading: false, @@ -189,7 +188,6 @@ export default { this.fetchInstaceGroups() this.fetchServiceOfferingData() this.fetchTemplateData() - this.fetchDynamicScalingVmConfig() this.fetchUserData() }, fetchZoneDetails () { @@ -241,18 +239,8 @@ export default { this.template = templateResponses[0] }) }, - fetchDynamicScalingVmConfig () { - const params = {} - params.name = 'enable.dynamic.scale.vm' - params.zoneid = this.resource.zoneid - var apiName = 'listConfigurations' - api(apiName, params).then(json => { - const configResponse = json.listconfigurationsresponse.configuration - this.dynamicScalingVmConfig = configResponse[0]?.value === 'true' - }) - }, - canDynamicScalingEnabled () { - return this.template.isdynamicallyscalable && this.serviceOffering.dynamicscalingenabled && this.dynamicScalingVmConfig + isDynamicScalingEnabled () { + return this.template.isdynamicallyscalable && this.serviceOffering.dynamicscalingenabled && this.$store.getters.features.dynamicscalingenabled }, fetchOsTypes () { this.osTypes.loading = true From 75a2b3cc54cf6fadda3e137c2cd7f5b8a502803e Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Fri, 25 Jul 2025 14:47:14 +0530 Subject: [PATCH 6/7] Validate qcow2 file during import operation (#11264) --- .../cloud/agent/api/CheckVolumeAnswer.java | 15 +++- .../agent/api/CopyRemoteVolumeAnswer.java | 15 +++- .../LibvirtCheckVolumeCommandWrapper.java | 79 +++++++++++++++---- ...LibvirtCopyRemoteVolumeCommandWrapper.java | 76 +++++++++++++++--- ...virtGetVolumesOnStorageCommandWrapper.java | 66 ++++++++++------ .../LibvirtResizeVolumeCommandWrapper.java | 19 +---- .../kvm/storage/KVMPhysicalDisk.java | 33 ++++++++ .../vm/UnmanagedVMsManagerImpl.java | 37 ++++++++- 8 files changed, 265 insertions(+), 75 deletions(-) diff --git a/core/src/main/java/com/cloud/agent/api/CheckVolumeAnswer.java b/core/src/main/java/com/cloud/agent/api/CheckVolumeAnswer.java index 5a32ab59a7a..07b7e102df9 100644 --- a/core/src/main/java/com/cloud/agent/api/CheckVolumeAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/CheckVolumeAnswer.java @@ -17,22 +17,33 @@ package com.cloud.agent.api; +import org.apache.cloudstack.storage.volume.VolumeOnStorageTO; + +import java.util.Map; + public class CheckVolumeAnswer extends Answer { private long size; + private Map volumeDetails; CheckVolumeAnswer() { } - public CheckVolumeAnswer(CheckVolumeCommand cmd, String details, long size) { - super(cmd, true, details); + public CheckVolumeAnswer(CheckVolumeCommand cmd, final boolean success, String details, long size, + Map volumeDetails) { + super(cmd, success, details); this.size = size; + this.volumeDetails = volumeDetails; } public long getSize() { return size; } + public Map getVolumeDetails() { + return volumeDetails; + } + public String getString() { return "CheckVolumeAnswer [size=" + size + "]"; } diff --git a/core/src/main/java/com/cloud/agent/api/CopyRemoteVolumeAnswer.java b/core/src/main/java/com/cloud/agent/api/CopyRemoteVolumeAnswer.java index e79005be71b..4aec0b26581 100644 --- a/core/src/main/java/com/cloud/agent/api/CopyRemoteVolumeAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/CopyRemoteVolumeAnswer.java @@ -17,21 +17,28 @@ package com.cloud.agent.api; +import org.apache.cloudstack.storage.volume.VolumeOnStorageTO; + +import java.util.Map; + public class CopyRemoteVolumeAnswer extends Answer { private String remoteIp; private String filename; private long size; + private Map volumeDetails; CopyRemoteVolumeAnswer() { } - public CopyRemoteVolumeAnswer(CopyRemoteVolumeCommand cmd, String details, String filename, long size) { - super(cmd, true, details); + public CopyRemoteVolumeAnswer(CopyRemoteVolumeCommand cmd, final boolean success, String details, String filename, long size, + Map volumeDetails) { + super(cmd, success, details); this.remoteIp = cmd.getRemoteIp(); this.filename = filename; this.size = size; + this.volumeDetails = volumeDetails; } public String getRemoteIp() { @@ -54,6 +61,10 @@ public class CopyRemoteVolumeAnswer extends Answer { return size; } + public Map getVolumeDetails() { + return volumeDetails; + } + public String getString() { return "CopyRemoteVolumeAnswer [remoteIp=" + remoteIp + "]"; } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapper.java index 8b0a5aab461..2caf8da2914 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapper.java @@ -31,18 +31,25 @@ import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; import com.cloud.storage.Storage; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.storage.volume.VolumeOnStorageTO; 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.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import org.libvirt.LibvirtException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; import java.util.Map; @ResourceWrapper(handles = CheckVolumeCommand.class) public final class LibvirtCheckVolumeCommandWrapper extends CommandWrapper { private static final Logger s_logger = Logger.getLogger(LibvirtCheckVolumeCommandWrapper.class); + private static final List STORAGE_POOL_TYPES_SUPPORTED = Arrays.asList(Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.NetworkFilesystem); @Override public Answer execute(final CheckVolumeCommand command, final LibvirtComputingResource libvirtComputingResource) { @@ -53,34 +60,76 @@ public final class LibvirtCheckVolumeCommandWrapper extends CommandWrapper getVolumeDetails(KVMStoragePool pool, KVMPhysicalDisk disk) { + Map info = getDiskFileInfo(pool, disk, true); + if (MapUtils.isEmpty(info)) { + return null; + } + + Map volumeDetails = new HashMap<>(); + + String backingFilePath = info.get(QemuImg.BACKING_FILE); + if (StringUtils.isNotBlank(backingFilePath)) { + volumeDetails.put(VolumeOnStorageTO.Detail.BACKING_FILE, backingFilePath); + } + String backingFileFormat = info.get(QemuImg.BACKING_FILE_FORMAT); + if (StringUtils.isNotBlank(backingFileFormat)) { + volumeDetails.put(VolumeOnStorageTO.Detail.BACKING_FILE_FORMAT, backingFileFormat); + } + String clusterSize = info.get(QemuImg.CLUSTER_SIZE); + if (StringUtils.isNotBlank(clusterSize)) { + volumeDetails.put(VolumeOnStorageTO.Detail.CLUSTER_SIZE, clusterSize); + } + String fileFormat = info.get(QemuImg.FILE_FORMAT); + if (StringUtils.isNotBlank(fileFormat)) { + volumeDetails.put(VolumeOnStorageTO.Detail.FILE_FORMAT, fileFormat); + } + String encrypted = info.get(QemuImg.ENCRYPTED); + if (StringUtils.isNotBlank(encrypted) && encrypted.equalsIgnoreCase("yes")) { + volumeDetails.put(VolumeOnStorageTO.Detail.IS_ENCRYPTED, String.valueOf(Boolean.TRUE)); + } + Boolean isLocked = isDiskFileLocked(pool, disk); + volumeDetails.put(VolumeOnStorageTO.Detail.IS_LOCKED, String.valueOf(isLocked)); + + return volumeDetails; + } + + private Map getDiskFileInfo(KVMStoragePool pool, KVMPhysicalDisk disk, boolean secure) { + if (!STORAGE_POOL_TYPES_SUPPORTED.contains(pool.getType())) { + return new HashMap<>(); // unknown + } try { QemuImg qemu = new QemuImg(0); - QemuImgFile qemuFile = new QemuImgFile(path); - Map info = qemu.info(qemuFile); - if (info.containsKey(QemuImg.VIRTUAL_SIZE)) { - return Long.parseLong(info.get(QemuImg.VIRTUAL_SIZE)); - } else { - throw new CloudRuntimeException("Unable to determine virtual size of volume at path " + path); - } + QemuImgFile qemuFile = new QemuImgFile(disk.getPath(), disk.getFormat()); + return qemu.info(qemuFile, secure); } catch (QemuImgException | LibvirtException ex) { - throw new CloudRuntimeException("Error when inspecting volume at path " + path, ex); + logger.error("Failed to get info of disk file: " + ex.getMessage()); + return null; } } + + private boolean isDiskFileLocked(KVMStoragePool pool, KVMPhysicalDisk disk) { + Map info = getDiskFileInfo(pool, disk, false); + return info == null; + } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCopyRemoteVolumeCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCopyRemoteVolumeCommandWrapper.java index a5e1716da2e..6edf5cbd906 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCopyRemoteVolumeCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCopyRemoteVolumeCommandWrapper.java @@ -31,18 +31,25 @@ import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; import com.cloud.storage.Storage; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.storage.volume.VolumeOnStorageTO; 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.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import org.libvirt.LibvirtException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; import java.util.Map; @ResourceWrapper(handles = CopyRemoteVolumeCommand.class) public final class LibvirtCopyRemoteVolumeCommandWrapper extends CommandWrapper { private static final Logger s_logger = Logger.getLogger(LibvirtCopyRemoteVolumeCommandWrapper.class); + private static final List STORAGE_POOL_TYPES_SUPPORTED = Arrays.asList(Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.NetworkFilesystem); @Override public Answer execute(final CopyRemoteVolumeCommand command, final LibvirtComputingResource libvirtComputingResource) { @@ -58,14 +65,19 @@ public final class LibvirtCopyRemoteVolumeCommandWrapper extends CommandWrapper< int timeoutInSecs = command.getWait(); try { - if (storageFilerTO.getType() == Storage.StoragePoolType.Filesystem || - storageFilerTO.getType() == Storage.StoragePoolType.NetworkFilesystem) { + if (STORAGE_POOL_TYPES_SUPPORTED.contains(storageFilerTO.getType())) { String filename = libvirtComputingResource.copyVolume(srcIp, username, password, dstPath, srcFile, tmpPath, timeoutInSecs); s_logger.debug("Volume " + srcFile + " copy successful, copied to file: " + filename); final KVMPhysicalDisk vol = pool.getPhysicalDisk(filename); final String path = vol.getPath(); - long size = getVirtualSizeFromFile(path); - return new CopyRemoteVolumeAnswer(command, "", filename, size); + try { + KVMPhysicalDisk.checkQcow2File(path); + } catch (final CloudRuntimeException e) { + return new CopyRemoteVolumeAnswer(command, false, "", filename, 0, getVolumeDetails(pool, vol)); + } + + long size = KVMPhysicalDisk.getVirtualSizeFromFile(path); + return new CopyRemoteVolumeAnswer(command, true, "", filename, size, getVolumeDetails(pool, vol)); } else { String msg = "Unsupported storage pool type: " + storageFilerTO.getType().toString() + ", only local and NFS pools are supported"; return new Answer(command, false, msg); @@ -77,18 +89,56 @@ public final class LibvirtCopyRemoteVolumeCommandWrapper extends CommandWrapper< } } - private long getVirtualSizeFromFile(String path) { + private Map getVolumeDetails(KVMStoragePool pool, KVMPhysicalDisk disk) { + Map info = getDiskFileInfo(pool, disk, true); + if (MapUtils.isEmpty(info)) { + return null; + } + + Map volumeDetails = new HashMap<>(); + + String backingFilePath = info.get(QemuImg.BACKING_FILE); + if (StringUtils.isNotBlank(backingFilePath)) { + volumeDetails.put(VolumeOnStorageTO.Detail.BACKING_FILE, backingFilePath); + } + String backingFileFormat = info.get(QemuImg.BACKING_FILE_FORMAT); + if (StringUtils.isNotBlank(backingFileFormat)) { + volumeDetails.put(VolumeOnStorageTO.Detail.BACKING_FILE_FORMAT, backingFileFormat); + } + String clusterSize = info.get(QemuImg.CLUSTER_SIZE); + if (StringUtils.isNotBlank(clusterSize)) { + volumeDetails.put(VolumeOnStorageTO.Detail.CLUSTER_SIZE, clusterSize); + } + String fileFormat = info.get(QemuImg.FILE_FORMAT); + if (StringUtils.isNotBlank(fileFormat)) { + volumeDetails.put(VolumeOnStorageTO.Detail.FILE_FORMAT, fileFormat); + } + String encrypted = info.get(QemuImg.ENCRYPTED); + if (StringUtils.isNotBlank(encrypted) && encrypted.equalsIgnoreCase("yes")) { + volumeDetails.put(VolumeOnStorageTO.Detail.IS_ENCRYPTED, String.valueOf(Boolean.TRUE)); + } + Boolean isLocked = isDiskFileLocked(pool, disk); + volumeDetails.put(VolumeOnStorageTO.Detail.IS_LOCKED, String.valueOf(isLocked)); + + return volumeDetails; + } + + private Map getDiskFileInfo(KVMStoragePool pool, KVMPhysicalDisk disk, boolean secure) { + if (!STORAGE_POOL_TYPES_SUPPORTED.contains(pool.getType())) { + return new HashMap<>(); // unknown + } try { QemuImg qemu = new QemuImg(0); - QemuImgFile qemuFile = new QemuImgFile(path); - Map info = qemu.info(qemuFile); - if (info.containsKey(QemuImg.VIRTUAL_SIZE)) { - return Long.parseLong(info.get(QemuImg.VIRTUAL_SIZE)); - } else { - throw new CloudRuntimeException("Unable to determine virtual size of volume at path " + path); - } + QemuImgFile qemuFile = new QemuImgFile(disk.getPath(), disk.getFormat()); + return qemu.info(qemuFile, secure); } catch (QemuImgException | LibvirtException ex) { - throw new CloudRuntimeException("Error when inspecting volume at path " + path, ex); + logger.error("Failed to get info of disk file: " + ex.getMessage()); + return null; } } + + private boolean isDiskFileLocked(KVMStoragePool pool, KVMPhysicalDisk disk) { + Map info = getDiskFileInfo(pool, disk, false); + return info == null; + } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapper.java index 821a80f5cca..6facf169602 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapper.java @@ -36,6 +36,7 @@ import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; import org.apache.cloudstack.utils.qemu.QemuImgException; import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.StringUtils; import org.libvirt.LibvirtException; @@ -91,37 +92,46 @@ public final class LibvirtGetVolumesOnStorageCommandWrapper extends CommandWrapp if (disk.getQemuEncryptFormat() != null) { volumeOnStorageTO.setQemuEncryptFormat(disk.getQemuEncryptFormat().toString()); } - String backingFilePath = info.get(QemuImg.BACKING_FILE); - if (StringUtils.isNotBlank(backingFilePath)) { - volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.BACKING_FILE, backingFilePath); - } - String backingFileFormat = info.get(QemuImg.BACKING_FILE_FORMAT); - if (StringUtils.isNotBlank(backingFileFormat)) { - volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.BACKING_FILE_FORMAT, backingFileFormat); - } - String clusterSize = info.get(QemuImg.CLUSTER_SIZE); - if (StringUtils.isNotBlank(clusterSize)) { - volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.CLUSTER_SIZE, clusterSize); - } String fileFormat = info.get(QemuImg.FILE_FORMAT); - if (StringUtils.isNotBlank(fileFormat)) { - if (!fileFormat.equalsIgnoreCase(disk.getFormat().toString())) { - return new GetVolumesOnStorageAnswer(command, false, String.format("The file format is %s, but expected to be %s", fileFormat, disk.getFormat())); - } - volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.FILE_FORMAT, fileFormat); + if (StringUtils.isNotBlank(fileFormat) && !fileFormat.equalsIgnoreCase(disk.getFormat().toString())) { + return new GetVolumesOnStorageAnswer(command, false, String.format("The file format is %s, but expected to be %s", fileFormat, disk.getFormat())); } - String encrypted = info.get(QemuImg.ENCRYPTED); - if (StringUtils.isNotBlank(encrypted) && encrypted.equalsIgnoreCase("yes")) { - volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.IS_ENCRYPTED, String.valueOf(Boolean.TRUE)); - } - Boolean isLocked = isDiskFileLocked(storagePool, disk); - volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.IS_LOCKED, String.valueOf(isLocked)); + addDetailsToVolumeOnStorageTO(volumeOnStorageTO, info, storagePool, disk); volumes.add(volumeOnStorageTO); } return new GetVolumesOnStorageAnswer(command, volumes); } + private void addDetailsToVolumeOnStorageTO(VolumeOnStorageTO volumeOnStorageTO, final Map info, final KVMStoragePool storagePool, final KVMPhysicalDisk disk) { + if (MapUtils.isEmpty(info)) { + return; + } + + String backingFilePath = info.get(QemuImg.BACKING_FILE); + if (StringUtils.isNotBlank(backingFilePath)) { + volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.BACKING_FILE, backingFilePath); + } + String backingFileFormat = info.get(QemuImg.BACKING_FILE_FORMAT); + if (StringUtils.isNotBlank(backingFileFormat)) { + volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.BACKING_FILE_FORMAT, backingFileFormat); + } + String clusterSize = info.get(QemuImg.CLUSTER_SIZE); + if (StringUtils.isNotBlank(clusterSize)) { + volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.CLUSTER_SIZE, clusterSize); + } + String fileFormat = info.get(QemuImg.FILE_FORMAT); + if (StringUtils.isNotBlank(fileFormat)) { + volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.FILE_FORMAT, fileFormat); + } + String encrypted = info.get(QemuImg.ENCRYPTED); + if (StringUtils.isNotBlank(encrypted) && encrypted.equalsIgnoreCase("yes")) { + volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.IS_ENCRYPTED, String.valueOf(Boolean.TRUE)); + } + Boolean isLocked = isDiskFileLocked(storagePool, disk); + volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.IS_LOCKED, String.valueOf(isLocked)); + } + private GetVolumesOnStorageAnswer addAllVolumes(final GetVolumesOnStorageCommand command, final KVMStoragePool storagePool, String keyword) { List volumes = new ArrayList<>(); @@ -134,11 +144,21 @@ public final class LibvirtGetVolumesOnStorageCommandWrapper extends CommandWrapp if (!isDiskFormatSupported(disk)) { continue; } + Map info = getDiskFileInfo(storagePool, disk, true); + if (info == null) { + continue; + } VolumeOnStorageTO volumeOnStorageTO = new VolumeOnStorageTO(Hypervisor.HypervisorType.KVM, disk.getName(), disk.getName(), disk.getPath(), disk.getFormat().toString(), disk.getSize(), disk.getVirtualSize()); if (disk.getQemuEncryptFormat() != null) { volumeOnStorageTO.setQemuEncryptFormat(disk.getQemuEncryptFormat().toString()); } + String fileFormat = info.get(QemuImg.FILE_FORMAT); + if (StringUtils.isNotBlank(fileFormat) && !fileFormat.equalsIgnoreCase(disk.getFormat().toString())) { + continue; + } + addDetailsToVolumeOnStorageTO(volumeOnStorageTO, info, storagePool, disk); + volumes.add(volumeOnStorageTO); } return new GetVolumesOnStorageAnswer(command, volumes); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java index 4f1ad728b5d..aa1a0f41f1b 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java @@ -25,7 +25,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Map; import com.cloud.hypervisor.kvm.storage.ScaleIOStorageAdaptor; import org.apache.cloudstack.utils.cryptsetup.KeyFile; @@ -33,7 +32,6 @@ import org.apache.cloudstack.utils.qemu.QemuImageOptions; import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; import org.apache.cloudstack.utils.qemu.QemuImgException; -import org.apache.cloudstack.utils.qemu.QemuImgFile; import org.apache.cloudstack.utils.qemu.QemuObject; import org.apache.log4j.Logger; import org.libvirt.Connect; @@ -102,7 +100,7 @@ public final class LibvirtResizeVolumeCommandWrapper extends CommandWrapper info = qemu.info(qemuFile); - if (info.containsKey(QemuImg.VIRTUAL_SIZE)) { - return Long.parseLong(info.get(QemuImg.VIRTUAL_SIZE)); - } else { - throw new CloudRuntimeException("Unable to determine virtual size of volume at path " + path); - } - } catch (QemuImgException | LibvirtException ex) { - throw new CloudRuntimeException("Error when inspecting volume at path " + path, ex); - } - } - private Answer handleMultipathSCSIResize(ResizeVolumeCommand command, KVMStoragePool pool) { ((MultipathSCSIPool)pool).resize(command.getPath(), command.getInstanceName(), command.getNewSize()); return new ResizeVolumeAnswer(command, true, ""); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMPhysicalDisk.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMPhysicalDisk.java index 9d9a6415e27..c43f5101fbe 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMPhysicalDisk.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMPhysicalDisk.java @@ -16,13 +16,21 @@ // under the License. package com.cloud.hypervisor.kvm.storage; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.storage.formatinspector.Qcow2Inspector; +import org.apache.cloudstack.utils.imagestore.ImageStoreUtil; +import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; import org.apache.cloudstack.utils.qemu.QemuObject; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.lang3.StringUtils; +import org.libvirt.LibvirtException; import java.util.ArrayList; import java.util.List; +import java.util.Map; public class KVMPhysicalDisk { private String path; @@ -71,6 +79,31 @@ public class KVMPhysicalDisk { return hostIp; } + public static long getVirtualSizeFromFile(String path) { + try { + QemuImg qemu = new QemuImg(0); + QemuImgFile qemuFile = new QemuImgFile(path); + Map info = qemu.info(qemuFile); + if (info.containsKey(QemuImg.VIRTUAL_SIZE)) { + return Long.parseLong(info.get(QemuImg.VIRTUAL_SIZE)); + } else { + throw new CloudRuntimeException("Unable to determine virtual size of volume at path " + path); + } + } catch (QemuImgException | LibvirtException ex) { + throw new CloudRuntimeException("Error when inspecting volume at path " + path, ex); + } + } + + public static void checkQcow2File(String path) { + if (ImageStoreUtil.isCorrectExtension(path, "qcow2")) { + try { + Qcow2Inspector.validateQcow2File(path); + } catch (RuntimeException e) { + throw new CloudRuntimeException("The volume file at path " + path + " is not a valid QCOW2. Error: " + e.getMessage()); + } + } + } + private PhysicalDiskFormat format; private long size; private long virtualSize; 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 abb0e6b63c5..df87aff276d 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -170,6 +170,7 @@ import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.volume.VolumeOnStorageTO; import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; @@ -812,7 +813,8 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager { throw new CloudRuntimeException("Error while copying volume of remote instance: " + answer.getDetails()); } CopyRemoteVolumeAnswer copyRemoteVolumeAnswer = (CopyRemoteVolumeAnswer) answer; - if(!copyRemoteVolumeAnswer.getResult()) { + checkVolume(copyRemoteVolumeAnswer.getVolumeDetails()); + if (!copyRemoteVolumeAnswer.getResult()) { throw new CloudRuntimeException("Unable to copy volume of remote instance"); } diskProfile.setSize(copyRemoteVolumeAnswer.getSize()); @@ -2653,7 +2655,13 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager { throw new CloudRuntimeException("Disk not found or is invalid"); } CheckVolumeAnswer checkVolumeAnswer = (CheckVolumeAnswer) answer; - if(!checkVolumeAnswer.getResult()) { + try { + checkVolume(checkVolumeAnswer.getVolumeDetails()); + } catch (CloudRuntimeException e) { + cleanupFailedImportVM(userVm); + throw e; + } + if (!checkVolumeAnswer.getResult()) { cleanupFailedImportVM(userVm); throw new CloudRuntimeException("Disk not found or is invalid"); } @@ -2679,6 +2687,31 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager { return userVm; } + private void checkVolume(Map volumeDetails) { + if (MapUtils.isEmpty(volumeDetails)) { + return; + } + + if (volumeDetails.containsKey(VolumeOnStorageTO.Detail.IS_LOCKED)) { + String isLocked = volumeDetails.get(VolumeOnStorageTO.Detail.IS_LOCKED); + if (Boolean.parseBoolean(isLocked)) { + logFailureAndThrowException("Locked volume cannot be imported or unmanaged."); + } + } + if (volumeDetails.containsKey(VolumeOnStorageTO.Detail.IS_ENCRYPTED)) { + String isEncrypted = volumeDetails.get(VolumeOnStorageTO.Detail.IS_ENCRYPTED); + if (Boolean.parseBoolean(isEncrypted)) { + logFailureAndThrowException("Encrypted volume cannot be imported or unmanaged."); + } + } + if (volumeDetails.containsKey(VolumeOnStorageTO.Detail.BACKING_FILE)) { + String backingFile = volumeDetails.get(VolumeOnStorageTO.Detail.BACKING_FILE); + if (StringUtils.isNotBlank(backingFile)) { + logFailureAndThrowException("Volume with backing file cannot be imported or unmanaged."); + } + } + } + private NetworkVO getDefaultNetwork(DataCenter zone, Account owner, boolean selectAny) throws InsufficientCapacityException, ResourceAllocationException { NetworkVO defaultNetwork = null; From a4263da8aea93fd12ead37c61489b9e529c0cdbc Mon Sep 17 00:00:00 2001 From: ghernadi Date: Fri, 25 Jul 2025 13:51:11 +0200 Subject: [PATCH 7/7] linstor: Use template's uuid if pool's downloadPath is null as resource-name (#11053) Also added an integration test for templates from snapshots --- plugins/storage/volume/linstor/CHANGELOG.md | 6 ++ .../kvm/storage/LinstorStorageAdaptor.java | 2 +- .../LinstorPrimaryDataStoreDriverImpl.java | 12 ++- .../plugins/linstor/test_linstor_volumes.py | 73 +++++++++++++++++-- 4 files changed, 86 insertions(+), 7 deletions(-) diff --git a/plugins/storage/volume/linstor/CHANGELOG.md b/plugins/storage/volume/linstor/CHANGELOG.md index 2abda3ebc50..a96b7c75b2b 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-07-01] + +### Fixed + +- Regression in 4.19.3 and 4.21.0 with templates from snapshots + ## [2025-05-07] ### Added 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 9de2479b20b..4210008f1c0 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 @@ -619,7 +619,7 @@ public class LinstorStorageAdaptor implements StorageAdaptor { try { templateProps.load(new FileInputStream(propFile.toFile())); String desc = templateProps.getProperty("description"); - if (desc.startsWith("SystemVM Template")) { + if (desc != null && desc.startsWith("SystemVM Template")) { return true; } } catch (IOException e) { diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java index 3b384831518..71e1b528506 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java @@ -74,12 +74,14 @@ import com.cloud.storage.StorageManager; import com.cloud.storage.StoragePool; import com.cloud.storage.VMTemplateStoragePoolVO; import com.cloud.storage.VMTemplateStorageResourceAssoc; +import com.cloud.storage.VMTemplateVO; import com.cloud.storage.Volume; import com.cloud.storage.VolumeDetailVO; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.SnapshotDao; import com.cloud.storage.dao.SnapshotDetailsDao; import com.cloud.storage.dao.SnapshotDetailsVO; +import com.cloud.storage.dao.VMTemplateDao; import com.cloud.storage.dao.VMTemplatePoolDao; import com.cloud.storage.dao.VolumeDao; import com.cloud.storage.dao.VolumeDetailsDao; @@ -131,6 +133,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver ConfigurationDao _configDao; @Inject private HostDao _hostDao; + @Inject private VMTemplateDao _vmTemplateDao; private long volumeStatsLastUpdate = 0L; private final Map> volumeStats = new HashMap<>(); @@ -668,8 +671,15 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver storagePoolVO.getId(), csCloneId, null); if (tmplPoolRef != null) { - final String templateRscName = LinstorUtil.RSC_PREFIX + tmplPoolRef.getLocalDownloadPath(); + final String templateRscName; + if (tmplPoolRef.getLocalDownloadPath() == null) { + VMTemplateVO vmTemplateVO = _vmTemplateDao.findById(tmplPoolRef.getTemplateId()); + templateRscName = LinstorUtil.RSC_PREFIX + vmTemplateVO.getUuid(); + } else { + templateRscName = LinstorUtil.RSC_PREFIX + tmplPoolRef.getLocalDownloadPath(); + } final String rscName = LinstorUtil.RSC_PREFIX + volumeInfo.getUuid(); + final DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(storagePoolVO.getHostAddress()); try { diff --git a/test/integration/plugins/linstor/test_linstor_volumes.py b/test/integration/plugins/linstor/test_linstor_volumes.py index a2c7978d801..e0ba15a0499 100644 --- a/test/integration/plugins/linstor/test_linstor_volumes.py +++ b/test/integration/plugins/linstor/test_linstor_volumes.py @@ -953,9 +953,72 @@ class TestLinstorVolumes(cloudstackTestCase): snapshot.delete(self.apiClient) + @attr(tags=['basic'], required_hardware=False) + def test_10_create_template_from_snapshot(self): + """ + Create a template from a snapshot and start an instance from it + """ + self.virtual_machine.stop(self.apiClient) + + volume = list_volumes( + self.apiClient, + virtualmachineid = self.virtual_machine.id, + type = "ROOT", + listall = True, + ) + snapshot = Snapshot.create( + self.apiClient, + volume_id=volume[0].id, + account=self.account.name, + domainid=self.domain.id, + ) + self.cleanup.append(snapshot) + + self.assertIsNotNone(snapshot, "Could not create snapshot") + + services = { + "displaytext": "IntegrationTestTemplate", + "name": "int-test-template", + "ostypeid": self.template.ostypeid, + "ispublic": "true" + } + + custom_template = Template.create_from_snapshot( + self.apiClient, + snapshot, + services, + ) + self.cleanup.append(custom_template) + + # create VM from custom template + test_virtual_machine = VirtualMachine.create( + self.apiClient, + self.testdata[TestData.virtualMachine2], + accountid=self.account.name, + zoneid=self.zone.id, + serviceofferingid=self.compute_offering.id, + templateid=custom_template.id, + domainid=self.domain.id, + startvm=False, + mode='basic', + ) + self.cleanup.append(test_virtual_machine) + + TestLinstorVolumes._start_vm(test_virtual_machine) + + test_virtual_machine.stop(self.apiClient) + + test_virtual_machine.delete(self.apiClient, True) + self.cleanup.remove(test_virtual_machine) + + custom_template.delete(self.apiClient) + self.cleanup.remove(custom_template) + snapshot.delete(self.apiClient) + self.cleanup.remove(snapshot) + @attr(tags=['advanced', 'migration'], required_hardware=False) - def test_10_migrate_volume_to_same_instance_pool(self): + def test_11_migrate_volume_to_same_instance_pool(self): """Migrate volume to the same instance pool""" if not self.testdata[TestData.migrationTests]: @@ -1088,7 +1151,7 @@ class TestLinstorVolumes(cloudstackTestCase): test_virtual_machine.delete(self.apiClient, True) @attr(tags=['advanced', 'migration'], required_hardware=False) - def test_11_migrate_volume_to_distinct_instance_pool(self): + def test_12_migrate_volume_to_distinct_instance_pool(self): """Migrate volume to distinct instance pool""" if not self.testdata[TestData.migrationTests]: @@ -1221,7 +1284,7 @@ class TestLinstorVolumes(cloudstackTestCase): test_virtual_machine.delete(self.apiClient, True) @attr(tags=["basic"], required_hardware=False) - def test_12_create_vm_snapshots(self): + def test_13_create_vm_snapshots(self): """Test to create VM snapshots """ vm = TestLinstorVolumes._start_vm(self.virtual_machine) @@ -1251,7 +1314,7 @@ class TestLinstorVolumes(cloudstackTestCase): ) @attr(tags=["basic"], required_hardware=False) - def test_13_revert_vm_snapshots(self): + def test_14_revert_vm_snapshots(self): """Test to revert VM snapshots """ @@ -1313,7 +1376,7 @@ class TestLinstorVolumes(cloudstackTestCase): ) @attr(tags=["basic"], required_hardware=False) - def test_14_delete_vm_snapshots(self): + def test_15_delete_vm_snapshots(self): """Test to delete vm snapshots """