From 432a3065bc8690ee4a48e95a69181d693aad348f Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 24 Dec 2025 20:50:40 +0530 Subject: [PATCH 01/51] api: fix response annotation for createBackupSchedule (#11950) Currently, `createBackupSchedule` API documentation wrongly specifies BackupResponse as the API response. Signed-off-by: Abhishek Kumar --- .../api/command/user/backup/CreateBackupScheduleCmd.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupScheduleCmd.java index 8d7febaa734..05d903a611c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupScheduleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupScheduleCmd.java @@ -26,7 +26,6 @@ import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; -import org.apache.cloudstack.api.response.BackupResponse; import org.apache.cloudstack.api.response.BackupScheduleResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.backup.BackupManager; @@ -38,7 +37,7 @@ import com.cloud.utils.exception.CloudRuntimeException; @APICommand(name = "createBackupSchedule", description = "Creates a User-defined Instance backup schedule", - responseObject = BackupResponse.class, since = "4.14.0", + responseObject = BackupScheduleResponse.class, since = "4.14.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class CreateBackupScheduleCmd extends BaseCmd { From 34b8870f591edf5c332b4144ccb9a3ea7890088e Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 26 Dec 2025 11:36:32 +0530 Subject: [PATCH 02/51] systemvm-template: support on-demand download during setup and registration (#11656) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundling all hypervisor SystemVM templates in release packages simplifies installs but inflates build time and artifact size. This change enables downloading templates on demand when they’re not found after package installation. The download path is wired into both cloud-setup-management and the existing SystemVM template registration flow. For connected or mirrored environments, a repository URL prefix can be provided to support air-gapped setups: pass --systemvm-templates-repository to cloud-setup-management, or set system.vm.templates.download.repository= in server.properties for post-setup registration. If templates are already present (bundled or preseeded), behavior is unchanged and no download is attempted. --------- Signed-off-by: Abhishek Kumar --- client/bindir/cloud-setup-management.in | 141 ++ client/conf/server.properties.in | 5 + .../dc/dao/DataCenterDetailsDaoImpl.java | 3 +- .../com/cloud/storage/dao/VMTemplateDao.java | 5 +- .../cloud/storage/dao/VMTemplateDaoImpl.java | 37 +- .../upgrade/SystemVmTemplateRegistration.java | 1005 +++++++----- .../storage/dao/VMTemplateDaoImplTest.java | 87 +- .../SystemVmTemplateRegistrationTest.java | 1388 +++++++++++++++-- engine/schema/templateConfig.sh | 25 +- .../com/cloud/storage/StorageManagerImpl.java | 94 +- .../utils/server/ServerPropertiesUtil.java | 58 + .../server/ServerPropertiesUtilTest.java | 95 ++ 12 files changed, 2397 insertions(+), 546 deletions(-) create mode 100644 utils/src/main/java/org/apache/cloudstack/utils/server/ServerPropertiesUtil.java create mode 100644 utils/src/test/java/org/apache/cloudstack/utils/server/ServerPropertiesUtilTest.java diff --git a/client/bindir/cloud-setup-management.in b/client/bindir/cloud-setup-management.in index 84c87ae2e44..b4fe76cc8d8 100755 --- a/client/bindir/cloud-setup-management.in +++ b/client/bindir/cloud-setup-management.in @@ -36,6 +36,106 @@ from cloudutils.cloudException import CloudRuntimeException, CloudInternalExcept from cloudutils.globalEnv import globalEnv from cloudutils.serviceConfigServer import cloudManagementConfig from optparse import OptionParser +import urllib.request +import configparser +import hashlib + +SYSTEMVM_TEMPLATES_PATH = "/usr/share/cloudstack-management/templates/systemvm" +SYSTEMVM_TEMPLATES_METADATA_FILE = SYSTEMVM_TEMPLATES_PATH + "/metadata.ini" + +def verify_sha512_checksum(file_path, expected_checksum): + sha512 = hashlib.sha512() + try: + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + sha512.update(chunk) + return sha512.hexdigest().lower() == expected_checksum.lower() + except Exception as e: + print(f"Failed to verify checksum for {file_path}: {e}") + return False + +def download_file(url, dest_path, chunk_size=8 * 1024 * 1024): + """ + Downloads a file from the given URL to the specified destination path in chunks. + """ + try: + with urllib.request.urlopen(url) as response: + total_size = response.length if response.length else None + downloaded = 0 + try: + with open(dest_path, 'wb') as out_file: + while True: + chunk = response.read(chunk_size) + if not chunk: + break + out_file.write(chunk) + downloaded += len(chunk) + if total_size: + print(f"Downloaded {downloaded / (1024 * 1024):.2f}MB of {total_size / (1024 * 1024):.2f}MB", end='\r') + except PermissionError as pe: + print(f"Permission denied: {dest_path}") + raise + print(f"\nDownloaded file from {url} to {dest_path}") + except Exception as e: + print(f"Failed to download file: {e}") + raise + +def download_template_if_needed(template, url, filename, checksum): + dest_path = os.path.join(SYSTEMVM_TEMPLATES_PATH, filename) + if os.path.exists(dest_path): + if checksum and verify_sha512_checksum(dest_path, checksum): + print(f"{template} System VM template already exists at {dest_path} with valid checksum, skipping download.") + return + else: + print(f"{template} System VM template at {dest_path} has invalid or missing checksum, re-downloading...") + else: + print(f"Downloading {template} System VM template from {url} to {dest_path}...") + try: + download_file(url, dest_path) + #After download, verify checksum if provided + if checksum: + if verify_sha512_checksum(dest_path, checksum): + print(f"{template} System VM template downloaded and verified successfully.") + else: + print(f"ERROR: Checksum verification failed for {template} System VM template after download.") + except Exception as e: + print(f"ERROR: Failed to download {template} System VM template: {e}") + +def collect_template_metadata(selected_templates, options): + template_metadata_list = [] + if not os.path.exists(SYSTEMVM_TEMPLATES_METADATA_FILE): + print(f"ERROR: System VM templates metadata file not found at {SYSTEMVM_TEMPLATES_METADATA_FILE}, cannot download templates.") + sys.exit(1) + config = configparser.ConfigParser() + config.read(SYSTEMVM_TEMPLATES_METADATA_FILE) + template_repo_url = None + if options.systemvm_templates_repository: + if "default" in config and "downloadrepository" in config["default"]: + template_repo_url = config["default"]["downloadrepository"].strip() + if not template_repo_url: + print("ERROR: downloadrepository value is empty in metadata file, cannot use --systemvm-template-repository option.") + sys.exit(1) + for template in selected_templates: + if template in config: + url = config[template].get("downloadurl") + filename = config[template].get("filename") + checksum = config[template].get("checksum") + if url and filename: + if template_repo_url: + url = url.replace(template_repo_url, options.systemvm_templates_repository) + template_metadata_list.append({ + "template": template, + "url": url, + "filename": filename, + "checksum": checksum + }) + else: + print(f"ERROR: URL or filename not found for {template} System VM template in metadata.") + sys.exit(1) + else: + print(f"ERROR: No metadata found for {template} System VM template.") + sys.exit(1) + return template_metadata_list if __name__ == '__main__': initLoging("@MSLOGDIR@/setupManagement.log") @@ -45,6 +145,16 @@ if __name__ == '__main__': parser.add_option("--https", action="store_true", dest="https", help="Enable HTTPs connection of management server") parser.add_option("--tomcat7", action="store_true", dest="tomcat7", help="Depreciated option, don't use it") parser.add_option("--no-start", action="store_true", dest="nostart", help="Do not start management server after successful configuration") + parser.add_option( + "--systemvm-templates", + dest="systemvm_templates", + help="Specify System VM templates to download: all, kvm-aarch64, kvm-x86_64, xenserver, vmware or comma-separated list of hypervisor combinations (e.g., kvm-x86_64,xenserver). Default is kvm-x86_64.", + ) + parser.add_option( + "--systemvm-templates-repository", + dest="systemvm_templates_repository", + help="Specify the URL to download System VM templates from." + ) (options, args) = parser.parse_args() if options.https: glbEnv.svrMode = "HttpsServer" @@ -53,6 +163,34 @@ if __name__ == '__main__': if options.nostart: glbEnv.noStart = True + available_templates = ["kvm-aarch64", "kvm-x86_64", "xenserver", "vmware"] + templates_arg = options.systemvm_templates + + selected_templates = ["kvm-x86_64"] + if templates_arg: + templates_list = [t.strip().lower() for t in templates_arg.split(",")] + if "all" in templates_list: + if len(templates_list) > 1: + print("WARNING: 'all' specified for System VM templates, ignoring other specified templates.") + selected_templates = available_templates + else: + invalid_templates = [] + for t in templates_list: + if t in available_templates: + if t not in selected_templates: + selected_templates.append(t) + else: + if t not in invalid_templates: + invalid_templates.append(t) + if invalid_templates: + print(f"ERROR: Invalid System VM template names provided: {', '.join(invalid_templates)}") + sys.exit(1) + print(f"Selected systemvm templates to download: {', '.join(selected_templates) if selected_templates else 'None'}") + + template_metadata_list = [] + if selected_templates: + template_metadata_list = collect_template_metadata(selected_templates, options) + glbEnv.mode = "Server" print("Starting to configure CloudStack Management Server:") @@ -74,3 +212,6 @@ if __name__ == '__main__': syscfg.restore() except: pass + + for meta in template_metadata_list: + download_template_if_needed(meta["template"], meta["url"], meta["filename"], meta["checksum"]) diff --git a/client/conf/server.properties.in b/client/conf/server.properties.in index 5958486b4df..7c5e3f925b0 100644 --- a/client/conf/server.properties.in +++ b/client/conf/server.properties.in @@ -62,3 +62,8 @@ extensions.deployment.mode=@EXTENSIONSDEPLOYMENTMODE@ # Thread pool configuration #threads.min=10 #threads.max=500 + +# The URL prefix for the system VM templates repository. When downloading system VM templates, the server replaces the +# `downloadrepository` key value from the metadata file in template URLs. If not specified, the original template URL +# will be used for download. +# system.vm.templates.download.repository=http://download.cloudstack.org/systemvm/ diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterDetailsDaoImpl.java b/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterDetailsDaoImpl.java index bb03a96d02e..aec54e20d98 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterDetailsDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterDetailsDaoImpl.java @@ -31,7 +31,8 @@ public class DataCenterDetailsDaoImpl extends ResourceDetailsDaoBase DetailSearch; - DataCenterDetailsDaoImpl() { + public DataCenterDetailsDaoImpl() { + super(); DetailSearch = createSearchBuilder(); DetailSearch.and("zoneId", DetailSearch.entity().getResourceId(), SearchCriteria.Op.EQ); DetailSearch.and("name", DetailSearch.entity().getName(), SearchCriteria.Op.EQ); diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java index 6785c365329..4c9f906b68a 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java @@ -94,7 +94,7 @@ public interface VMTemplateDao extends GenericDao, StateDao< List listByParentTemplatetId(long parentTemplatetId); - VMTemplateVO findLatestTemplateByName(String name, CPU.CPUArch arch); + VMTemplateVO findLatestTemplateByName(String name, HypervisorType hypervisorType, CPU.CPUArch arch); List findTemplatesLinkedToUserdata(long userdataId); @@ -103,4 +103,7 @@ public interface VMTemplateDao extends GenericDao, StateDao< List listIdsByTemplateTag(String tag); List listIdsByExtensionId(long extensionId); + + VMTemplateVO findActiveSystemTemplateByHypervisorArchAndUrlPath(HypervisorType hypervisorType, + CPU.CPUArch arch, String urlPathSuffix); } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java index 359b7c10988..bcf8b39a291 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java @@ -245,13 +245,17 @@ public class VMTemplateDaoImpl extends GenericDaoBase implem @Override - public VMTemplateVO findLatestTemplateByName(String name, CPU.CPUArch arch) { + public VMTemplateVO findLatestTemplateByName(String name, HypervisorType hypervisorType, CPU.CPUArch arch) { SearchBuilder sb = createSearchBuilder(); sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ); + sb.and("hypervisorType", sb.entity().getHypervisorType(), SearchCriteria.Op.EQ); sb.and("arch", sb.entity().getArch(), SearchCriteria.Op.EQ); sb.done(); SearchCriteria sc = sb.create(); sc.setParameters("name", name); + if (hypervisorType != null) { + sc.setParameters("hypervisorType", hypervisorType); + } if (arch != null) { sc.setParameters("arch", arch); } @@ -850,6 +854,37 @@ public class VMTemplateDaoImpl extends GenericDaoBase implem return customSearch(sc, null); } + @Override + public VMTemplateVO findActiveSystemTemplateByHypervisorArchAndUrlPath(HypervisorType hypervisorType, + CPU.CPUArch arch, String urlPathSuffix) { + if (StringUtils.isBlank(urlPathSuffix)) { + return null; + } + SearchBuilder sb = createSearchBuilder(); + sb.and("templateType", sb.entity().getTemplateType(), SearchCriteria.Op.EQ); + sb.and("hypervisorType", sb.entity().getHypervisorType(), SearchCriteria.Op.EQ); + sb.and("arch", sb.entity().getArch(), SearchCriteria.Op.EQ); + sb.and("urlPathSuffix", sb.entity().getUrl(), SearchCriteria.Op.LIKE); + sb.and("state", sb.entity().getState(), SearchCriteria.Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("templateType", TemplateType.SYSTEM); + if (hypervisorType != null) { + sc.setParameters("hypervisorType", hypervisorType); + } + if (arch != null) { + sc.setParameters("arch", arch); + } + sc.setParameters("urlPathSuffix", "%" + urlPathSuffix); + sc.setParameters("state", VirtualMachineTemplate.State.Active); + Filter filter = new Filter(VMTemplateVO.class, "id", false, null, 1L); + List templates = listBy(sc, filter); + if (CollectionUtils.isNotEmpty(templates)) { + return templates.get(0); + } + return null; + } + @Override public boolean updateState( com.cloud.template.VirtualMachineTemplate.State currentState, diff --git a/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java b/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java index f13fe2c6bc3..79a4bd6d6d8 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java @@ -28,7 +28,6 @@ import java.sql.Connection; import java.sql.Date; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -51,6 +50,7 @@ import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; import org.apache.cloudstack.utils.security.DigestHelper; +import org.apache.cloudstack.utils.server.ServerPropertiesUtil; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -62,6 +62,8 @@ import com.cloud.dc.dao.ClusterDao; import com.cloud.dc.dao.ClusterDaoImpl; import com.cloud.dc.dao.DataCenterDao; import com.cloud.dc.dao.DataCenterDaoImpl; +import com.cloud.dc.dao.DataCenterDetailsDao; +import com.cloud.dc.dao.DataCenterDetailsDaoImpl; import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.DataStoreRole; import com.cloud.storage.GuestOSVO; @@ -103,14 +105,20 @@ public class SystemVmTemplateRegistration { private static final String METADATA_FILE = TEMPLATES_PATH + METADATA_FILE_NAME; public static final String TEMPORARY_SECONDARY_STORE = "tmp"; private static final String PARTIAL_TEMPLATE_FOLDER = String.format("/template/tmpl/%d/", Account.ACCOUNT_ID_SYSTEM); - private static final String storageScriptsDir = "scripts/storage/secondary"; + protected static final String STORAGE_SCRIPTS_DIR = "scripts/storage/secondary"; private static final Integer OTHER_LINUX_ID = 99; - private static Integer LINUX_12_ID = 363; + protected static Integer LINUX_12_ID = 363; private static final Integer SCRIPT_TIMEOUT = 1800000; private static final Integer LOCK_WAIT_TIMEOUT = 1200; + protected static final String TEMPLATE_DOWNLOAD_URL_KEY = "downloadurl"; + protected static final String TEMPLATES_DOWNLOAD_REPOSITORY_KEY = "downloadrepository"; + protected static final String TEMPLATES_CUSTOM_DOWNLOAD_REPOSITORY_KEY = "system.vm.templates.download.repository"; protected static final List DOWNLOADABLE_TEMPLATE_ARCH_TYPES = Arrays.asList( + CPU.CPUArch.amd64, CPU.CPUArch.arm64 ); + protected static final String MINIMUM_SYSTEM_VM_VERSION_KEY = "minreq.sysvmtemplate.version"; + protected static final String DEFAULT_SYSTEM_VM_GUEST_OS_NAME = "Debian GNU/Linux 12 (64-bit)"; public static String CS_MAJOR_VERSION = null; public static String CS_TINY_VERSION = null; @@ -134,7 +142,9 @@ public class SystemVmTemplateRegistration { @Inject ConfigurationDao configurationDao; @Inject - private GuestOSDao guestOSDao; + DataCenterDetailsDao dataCenterDetailsDao; + @Inject + GuestOSDao guestOSDao; private String systemVmTemplateVersion; @@ -142,6 +152,7 @@ public class SystemVmTemplateRegistration { public SystemVmTemplateRegistration() { dataCenterDao = new DataCenterDaoImpl(); + dataCenterDetailsDao = new DataCenterDetailsDaoImpl(); vmTemplateDao = new VMTemplateDaoImpl(); vmTemplateZoneDao = new VMTemplateZoneDaoImpl(); templateDataStoreDao = new BasicTemplateDataStoreDaoImpl(); @@ -155,33 +166,14 @@ public class SystemVmTemplateRegistration { } /** - * Convenience constructor method to use when there is no system VM template change for a new version. + * Convenience constructor method to use when there is no system VM Template change for a new version. */ public SystemVmTemplateRegistration(String systemVmTemplateVersion) { this(); this.systemVmTemplateVersion = systemVmTemplateVersion; } - public static String getMountCommand(String nfsVersion, String device, String dir) { - String cmd = MOUNT_COMMAND_BASE; - if (StringUtils.isNotBlank(nfsVersion)) { - cmd = String.format("%s -o vers=%s", cmd, nfsVersion); - } - return String.format("%s %s %s", cmd, device, dir); - } - - public String getSystemVmTemplateVersion() { - if (StringUtils.isEmpty(systemVmTemplateVersion)) { - return String.format("%s.%s", CS_MAJOR_VERSION, CS_TINY_VERSION); - } - return systemVmTemplateVersion; - } - - public File getTempDownloadDir() { - return tempDownloadDir; - } - - private static class SystemVMTemplateDetails { + protected static class SystemVMTemplateDetails { Long id; String uuid; String name; @@ -312,19 +304,19 @@ public class SystemVmTemplateRegistration { } } - public static final List> hypervisorList = Arrays.asList( + protected static final List> AVAILABLE_SYSTEM_TEMPLATES_HYPERVISOR_ARCH_LIST = Arrays.asList( new Pair<>(Hypervisor.HypervisorType.KVM, CPU.CPUArch.amd64), new Pair<>(Hypervisor.HypervisorType.KVM, CPU.CPUArch.arm64), - new Pair<>(Hypervisor.HypervisorType.VMware, null), - new Pair<>(Hypervisor.HypervisorType.XenServer, null), - new Pair<>(Hypervisor.HypervisorType.Hyperv, null), - new Pair<>(Hypervisor.HypervisorType.LXC, null), - new Pair<>(Hypervisor.HypervisorType.Ovm3, null) + new Pair<>(Hypervisor.HypervisorType.VMware, CPU.CPUArch.amd64), + new Pair<>(Hypervisor.HypervisorType.XenServer, CPU.CPUArch.amd64), + new Pair<>(Hypervisor.HypervisorType.Hyperv, CPU.CPUArch.amd64), + new Pair<>(Hypervisor.HypervisorType.LXC, CPU.CPUArch.amd64), + new Pair<>(Hypervisor.HypervisorType.Ovm3, CPU.CPUArch.amd64) ); - public static final Map NewTemplateMap = new HashMap<>(); + protected static final List METADATA_TEMPLATE_LIST = new ArrayList<>(); - public static final Map RouterTemplateConfigurationNames = new HashMap<>() { + protected static final Map ROUTER_TEMPLATE_CONFIGURATION_NAMES = new HashMap<>() { { put(Hypervisor.HypervisorType.KVM, "router.template.kvm"); put(Hypervisor.HypervisorType.VMware, "router.template.vmware"); @@ -335,18 +327,7 @@ public class SystemVmTemplateRegistration { } }; - public static Map hypervisorGuestOsMap = new HashMap<>() { - { - put(Hypervisor.HypervisorType.KVM, LINUX_12_ID); - put(Hypervisor.HypervisorType.XenServer, OTHER_LINUX_ID); - put(Hypervisor.HypervisorType.VMware, OTHER_LINUX_ID); - put(Hypervisor.HypervisorType.Hyperv, LINUX_12_ID); - put(Hypervisor.HypervisorType.LXC, LINUX_12_ID); - put(Hypervisor.HypervisorType.Ovm3, LINUX_12_ID); - } - }; - - public static final Map hypervisorImageFormat = new HashMap() { + protected static final Map HYPERVISOR_IMAGE_FORMAT_MAP = new HashMap<>() { { put(Hypervisor.HypervisorType.KVM, ImageFormat.QCOW2); put(Hypervisor.HypervisorType.XenServer, ImageFormat.VHD); @@ -357,69 +338,27 @@ public class SystemVmTemplateRegistration { } }; - public boolean validateIfSeeded(TemplateDataStoreVO templDataStoreVO, String url, String path, String nfsVersion) { - String filePath = null; - try { - filePath = Files.createTempDirectory(TEMPORARY_SECONDARY_STORE).toString(); - if (filePath == null) { - throw new CloudRuntimeException("Failed to create temporary directory to mount secondary store"); - } - mountStore(url, filePath, nfsVersion); - int lastIdx = path.lastIndexOf(File.separator); - String partialDirPath = path.substring(0, lastIdx); - String templatePath = filePath + File.separator + partialDirPath; - File templateProps = new File(templatePath + "/template.properties"); - if (templateProps.exists()) { - Pair templateSizes = readTemplatePropertiesSizes(templatePath + "/template.properties"); - updateSeededTemplateDetails(templDataStoreVO.getTemplateId(), templDataStoreVO.getDataStoreId(), - templateSizes.first(), templateSizes.second()); - LOGGER.info("SystemVM Template already seeded, skipping registration"); - return true; - } - LOGGER.info("SystemVM Template not seeded"); - return false; - } catch (Exception e) { - LOGGER.error("Failed to verify if the Template is seeded", e); - throw new CloudRuntimeException("Failed to verify if the Template is seeded", e); - } finally { - unmountStore(filePath); - try { - Files.delete(Path.of(filePath)); - } catch (IOException e) { - LOGGER.error(String.format("Failed to delete temporary directory: %s", filePath)); - } + protected static Map hypervisorGuestOsMap = new HashMap<>() { + { + put(Hypervisor.HypervisorType.KVM, LINUX_12_ID); + put(Hypervisor.HypervisorType.XenServer, OTHER_LINUX_ID); + put(Hypervisor.HypervisorType.VMware, OTHER_LINUX_ID); + put(Hypervisor.HypervisorType.Hyperv, LINUX_12_ID); + put(Hypervisor.HypervisorType.LXC, LINUX_12_ID); + put(Hypervisor.HypervisorType.Ovm3, LINUX_12_ID); } + }; + + private static boolean isRunningInTest() { + return "true".equalsIgnoreCase(System.getProperty("test.mode")); } private static String getHypervisorArchLog(Hypervisor.HypervisorType hypervisorType, CPU.CPUArch arch) { StringBuilder sb = new StringBuilder("hypervisor: ").append(hypervisorType.name()); - if (Hypervisor.HypervisorType.KVM.equals(hypervisorType)) { - sb.append(", arch: ").append(arch == null ? CPU.CPUArch.amd64.getType() : arch.getType()); - } + sb.append(", arch: ").append(arch == null ? CPU.CPUArch.amd64.getType() : arch.getType()); return sb.toString(); } - protected static String getHypervisorArchKey(Hypervisor.HypervisorType hypervisorType, CPU.CPUArch arch) { - if (Hypervisor.HypervisorType.KVM.equals(hypervisorType)) { - return String.format("%s-%s", hypervisorType.name().toLowerCase(), - arch == null ? CPU.CPUArch.amd64.getType() : arch.getType()); - } - return hypervisorType.name().toLowerCase(); - } - - protected static MetadataTemplateDetails getMetadataTemplateDetails(Hypervisor.HypervisorType hypervisorType, - CPU.CPUArch arch) { - return NewTemplateMap.get(getHypervisorArchKey(hypervisorType, arch)); - } - - public VMTemplateVO getRegisteredTemplate(String templateName, CPU.CPUArch arch) { - return vmTemplateDao.findLatestTemplateByName(templateName, arch); - } - - private static boolean isRunningInTest() { - return "true".equalsIgnoreCase(System.getProperty("test.mode")); - } - /** * Attempts to determine the templates directory path by locating the metadata file. *

@@ -460,7 +399,170 @@ public class SystemVmTemplateRegistration { throw new CloudRuntimeException(errMsg); } - private List getEligibleZoneIds() { + protected static void cleanupStore(Long templateId, String filePath) { + String destTempFolder = filePath + PARTIAL_TEMPLATE_FOLDER + String.valueOf(templateId); + try { + Files.deleteIfExists(Paths.get(destTempFolder)); + } catch (IOException e) { + LOGGER.error("Failed to cleanup mounted store at: {}", filePath, e); + } + } + + protected static Pair readTemplatePropertiesSizes(String path) { + File tmpFile = new File(path); + Long size = null; + Long physicalSize = 0L; + try (FileReader fr = new FileReader(tmpFile); BufferedReader brf = new BufferedReader(fr);) { + String line = null; + while ((line = brf.readLine()) != null) { + if (line.startsWith("size=")) { + physicalSize = Long.parseLong(line.split("=")[1]); + } else if (line.startsWith("virtualsize=")) { + size = Long.parseLong(line.split("=")[1]); + } + if (size == null) { + size = physicalSize; + } + } + } catch (IOException ex) { + LOGGER.warn("Failed to read from template.properties", ex); + } + return new Pair<>(size, physicalSize); + } + + protected static MetadataTemplateDetails getMetadataTemplateDetails(Hypervisor.HypervisorType hypervisorType, + CPU.CPUArch arch) { + return METADATA_TEMPLATE_LIST + .stream() + .filter(x -> Objects.equals(x.getHypervisorType(), hypervisorType) && + Objects.equals(x.getArch(), arch)) + .findFirst() + .orElse(null); + } + + protected static String getMetadataFilePath() { + return METADATA_FILE; + } + + protected static Ini.Section getMetadataSectionForHypervisorAndArch(Ini ini, + Hypervisor.HypervisorType hypervisorType, CPU.CPUArch arch) { + String key = String.format("%s-%s", hypervisorType.name().toLowerCase(), + arch.getType().toLowerCase()); + Ini.Section section = ini.get(key); + if (section == null && !Hypervisor.HypervisorType.KVM.equals(hypervisorType)) { + key = String.format("%s", hypervisorType.name().toLowerCase()); + section = ini.get(key); + } + return section; + } + + protected static String getMountCommand(String nfsVersion, String device, String dir) { + String cmd = MOUNT_COMMAND_BASE; + if (StringUtils.isNotBlank(nfsVersion)) { + cmd = String.format("%s -o vers=%s", cmd, nfsVersion); + } + return String.format("%s %s %s", cmd, device, dir); + } + + /** + * This method parses the metadata file consisting of the system VM Templates information + * @return the version of the system VM Template that is to be used. This is done in order + * to fallback on the latest available version of the system VM Template when there doesn't + * exist a template corresponding to the current code version. + */ + public static String parseMetadataFile() { + String metadataFilePath = getMetadataFilePath(); + String errMsg = String.format("Failed to parse system VM Template metadata file: %s", metadataFilePath); + final Ini ini = new Ini(); + try (FileReader reader = new FileReader(metadataFilePath)) { + ini.load(reader); + } catch (IOException e) { + LOGGER.error(errMsg, e); + throw new CloudRuntimeException(errMsg, e); + } + if (!ini.containsKey("default")) { + errMsg = String.format("%s as unable to default section", errMsg); + LOGGER.error(errMsg); + throw new CloudRuntimeException(errMsg); + } + Ini.Section defaultSection = ini.get("default"); + String defaultDownloadRepository = defaultSection.get(TEMPLATES_DOWNLOAD_REPOSITORY_KEY); + String customDownloadRepository = ServerPropertiesUtil.getProperty(TEMPLATES_CUSTOM_DOWNLOAD_REPOSITORY_KEY); + boolean updateCustomDownloadRepository = StringUtils.isNotBlank(customDownloadRepository) && + StringUtils.isNotBlank(defaultDownloadRepository); + for (Pair hypervisorTypeArchPair : AVAILABLE_SYSTEM_TEMPLATES_HYPERVISOR_ARCH_LIST) { + String key = String.format("%s-%s", hypervisorTypeArchPair.first().name().toLowerCase(), + hypervisorTypeArchPair.second().getType().toLowerCase()); + Ini.Section section = getMetadataSectionForHypervisorAndArch(ini, hypervisorTypeArchPair.first(), + hypervisorTypeArchPair.second()); + if (section == null) { + LOGGER.error("Failed to find details for {} in template metadata file: {}", + getHypervisorArchLog(hypervisorTypeArchPair.first(), hypervisorTypeArchPair.second()), + metadataFilePath); + continue; + } + String url = section.get(TEMPLATE_DOWNLOAD_URL_KEY); + if (StringUtils.isNotBlank(url) && updateCustomDownloadRepository) { + url = url.replaceFirst(defaultDownloadRepository.trim(), + customDownloadRepository.trim()); + LOGGER.debug("Updated download URL for {} using custom repository to {}", key, url); + } + METADATA_TEMPLATE_LIST.add(new MetadataTemplateDetails( + hypervisorTypeArchPair.first(), + section.get("templatename"), + section.get("filename"), + url, + section.get("checksum"), + hypervisorTypeArchPair.second(), + section.get("guestos"))); + } + return defaultSection.get("version").trim(); + } + + public static void mountStore(String storeUrl, String path, String nfsVersion) { + try { + if (storeUrl == null) { + return; + } + URI uri = new URI(UriUtils.encodeURIComponent(storeUrl)); + String host = uri.getHost(); + String mountPath = uri.getPath(); + Script.runSimpleBashScript(getMountCommand(nfsVersion, host + ":" + mountPath, path)); + } catch (Exception e) { + String msg = "NFS Store URL is not in the correct format"; + LOGGER.error(msg, e); + throw new CloudRuntimeException(msg, e); + } + } + + public static void unmountStore(String filePath) { + try { + LOGGER.info("Unmounting store"); + String umountCmd = String.format(UMOUNT_COMMAND, filePath); + Script.runSimpleBashScript(umountCmd); + try { + Files.deleteIfExists(Paths.get(filePath)); + } catch (IOException e) { + LOGGER.error(String.format("Failed to cleanup mounted store at: %s", filePath), e); + } + } catch (Exception e) { + String msg = String.format("Failed to unmount store mounted at %s", filePath); + LOGGER.error(msg, e); + throw new CloudRuntimeException(msg, e); + } + } + + protected File getTempDownloadDir() { + return tempDownloadDir; + } + + protected void readTemplateProperties(String path, SystemVMTemplateDetails details) { + Pair templateSizes = readTemplatePropertiesSizes(path); + details.setSize(templateSizes.first()); + details.setPhysicalSize(templateSizes.second()); + } + + protected List getEligibleZoneIds() { List zoneIds = new ArrayList<>(); List stores = imageStoreDao.findByProtocol("nfs"); for (ImageStoreVO store : stores) { @@ -484,20 +586,11 @@ public class SystemVmTemplateRegistration { return new Pair<>(url, storeId); } - public static void mountStore(String storeUrl, String path, String nfsVersion) { - try { - if (storeUrl == null) { - return; - } - URI uri = new URI(UriUtils.encodeURIComponent(storeUrl)); - String host = uri.getHost(); - String mountPath = uri.getPath(); - Script.runSimpleBashScript(getMountCommand(nfsVersion, host + ":" + mountPath, path)); - } catch (Exception e) { - String msg = "NFS Store URL is not in the correct format"; - LOGGER.error(msg, e); - throw new CloudRuntimeException(msg, e); + protected String getSystemVmTemplateVersion() { + if (StringUtils.isEmpty(systemVmTemplateVersion)) { + return String.format("%s.%s", CS_MAJOR_VERSION, CS_TINY_VERSION); } + return systemVmTemplateVersion; } private VMTemplateVO createTemplateObjectInDB(SystemVMTemplateDetails details) { @@ -527,7 +620,7 @@ public class SystemVmTemplateRegistration { return template; } - private VMTemplateZoneVO createOrUpdateTemplateZoneEntry(long zoneId, long templateId) { + protected VMTemplateZoneVO createOrUpdateTemplateZoneEntry(long zoneId, long templateId) { VMTemplateZoneVO templateZoneVO = vmTemplateZoneDao.findByZoneTemplate(zoneId, templateId); if (templateZoneVO == null) { templateZoneVO = new VMTemplateZoneVO(zoneId, templateId, new java.util.Date()); @@ -541,33 +634,37 @@ public class SystemVmTemplateRegistration { return templateZoneVO; } - private void createCrossZonesTemplateZoneRefEntries(Long templateId) { + protected void createCrossZonesTemplateZoneRefEntries(Long templateId) { List dcs = dataCenterDao.listAll(); for (DataCenterVO dc : dcs) { VMTemplateZoneVO templateZoneVO = createOrUpdateTemplateZoneEntry(dc.getId(), templateId); if (templateZoneVO == null) { - throw new CloudRuntimeException(String.format("Failed to create template_zone_ref record for the systemVM Template (id: %s) and zone: %s", templateId, dc)); + throw new CloudRuntimeException(String.format("Failed to create template-zone record for the system " + + "VM Template (ID : %d) and zone: %s", templateId, dc)); } } } - private void createTemplateStoreRefEntry(SystemVMTemplateDetails details) { - TemplateDataStoreVO templateDataStoreVO = new TemplateDataStoreVO(details.storeId, details.getId(), details.getCreated(), 0, - VMTemplateStorageResourceAssoc.Status.NOT_DOWNLOADED, null, null, null, details.getInstallPath(), details.getUrl()); + protected void createTemplateStoreRefEntry(SystemVMTemplateDetails details) { + TemplateDataStoreVO templateDataStoreVO = new TemplateDataStoreVO(details.getStoreId(), details.getId(), + details.getCreated(), 0, VMTemplateStorageResourceAssoc.Status.NOT_DOWNLOADED, + null, null, null, details.getInstallPath(), details.getUrl()); templateDataStoreVO.setDataStoreRole(DataStoreRole.Image); templateDataStoreVO = templateDataStoreDao.persist(templateDataStoreVO); if (templateDataStoreVO == null) { - throw new CloudRuntimeException(String.format("Failed to create template_store_ref record for the systemVM Template for hypervisor: %s", details.getHypervisorType().name())); + throw new CloudRuntimeException(String.format("Failed to create template-store record for the system VM " + + "template (ID : %d) and store (ID: %d)", details.getId(), details.getStoreId())); } } - public void updateTemplateDetails(SystemVMTemplateDetails details) { + protected void updateTemplateDetails(SystemVMTemplateDetails details) { VMTemplateVO template = vmTemplateDao.findById(details.getId()); template.setSize(details.getSize()); template.setState(VirtualMachineTemplate.State.Active); vmTemplateDao.update(template.getId(), template); - TemplateDataStoreVO templateDataStoreVO = templateDataStoreDao.findByStoreTemplate(details.getStoreId(), template.getId()); + TemplateDataStoreVO templateDataStoreVO = templateDataStoreDao.findByStoreTemplate(details.getStoreId(), + template.getId()); templateDataStoreVO.setSize(details.getSize()); templateDataStoreVO.setPhysicalSize(details.getPhysicalSize()); templateDataStoreVO.setDownloadPercent(100); @@ -576,11 +673,11 @@ public class SystemVmTemplateRegistration { templateDataStoreVO.setState(ObjectInDataStoreStateMachine.State.Ready); boolean updated = templateDataStoreDao.update(templateDataStoreVO.getId(), templateDataStoreVO); if (!updated) { - throw new CloudRuntimeException("Failed to update template_store_ref entry for registered systemVM Template"); + throw new CloudRuntimeException("Failed to update template-store record for registered system VM Template"); } } - public void updateSeededTemplateDetails(long templateId, long storeId, long size, long physicalSize) { + protected void updateSeededTemplateDetails(long templateId, long storeId, long size, long physicalSize) { VMTemplateVO template = vmTemplateDao.findById(templateId); template.setSize(size); vmTemplateDao.update(template.getId(), template); @@ -591,108 +688,77 @@ public class SystemVmTemplateRegistration { templateDataStoreVO.setLastUpdated(new Date(DateUtil.currentGMTTime().getTime())); boolean updated = templateDataStoreDao.update(templateDataStoreVO.getId(), templateDataStoreVO); if (!updated) { - throw new CloudRuntimeException("Failed to update template_store_ref entry for seeded systemVM template"); + throw new CloudRuntimeException("Failed to update template-store record for seeded system VM Template"); } } - public void updateSystemVMEntries(Long templateId, Hypervisor.HypervisorType hypervisorType) { + protected void updateSystemVMEntries(Long templateId, Hypervisor.HypervisorType hypervisorType) { vmInstanceDao.updateSystemVmTemplateId(templateId, hypervisorType); } - private void updateSystemVmTemplateGuestOsId() { - String systemVmGuestOsName = "Debian GNU/Linux 12 (64-bit)"; // default + protected void updateHypervisorGuestOsMap() { try { - GuestOSVO guestOS = guestOSDao.findOneByDisplayName(systemVmGuestOsName); - if (guestOS != null) { - LOGGER.debug("Updating SystemVM Template Guest OS [{}] id", systemVmGuestOsName); - SystemVmTemplateRegistration.LINUX_12_ID = Math.toIntExact(guestOS.getId()); - hypervisorGuestOsMap.put(Hypervisor.HypervisorType.KVM, LINUX_12_ID); - hypervisorGuestOsMap.put(Hypervisor.HypervisorType.Hyperv, LINUX_12_ID); - hypervisorGuestOsMap.put(Hypervisor.HypervisorType.LXC, LINUX_12_ID); - hypervisorGuestOsMap.put(Hypervisor.HypervisorType.Ovm3, LINUX_12_ID); + GuestOSVO guestOS = guestOSDao.findOneByDisplayName(DEFAULT_SYSTEM_VM_GUEST_OS_NAME); + if (guestOS == null) { + LOGGER.warn("Couldn't find Guest OS by name [{}] to update system VM Template guest OS ID", + DEFAULT_SYSTEM_VM_GUEST_OS_NAME); + return; } + LOGGER.debug("Updating system VM Template guest OS [{}] ID", DEFAULT_SYSTEM_VM_GUEST_OS_NAME); + SystemVmTemplateRegistration.LINUX_12_ID = Math.toIntExact(guestOS.getId()); + hypervisorGuestOsMap.put(Hypervisor.HypervisorType.KVM, LINUX_12_ID); + hypervisorGuestOsMap.put(Hypervisor.HypervisorType.Hyperv, LINUX_12_ID); + hypervisorGuestOsMap.put(Hypervisor.HypervisorType.LXC, LINUX_12_ID); + hypervisorGuestOsMap.put(Hypervisor.HypervisorType.Ovm3, LINUX_12_ID); } catch (Exception e) { - LOGGER.warn("Couldn't update SystemVM Template Guest OS id, due to {}", e.getMessage()); + LOGGER.warn("Couldn't update System VM template guest OS ID, due to {}", e.getMessage()); } } - public void updateConfigurationParams(Map configParams) { - for (Map.Entry config : configParams.entrySet()) { - boolean updated = configurationDao.update(config.getKey(), config.getValue()); - if (!updated) { - throw new CloudRuntimeException(String.format("Failed to update configuration parameter %s", config.getKey())); - } + protected void updateConfigurationParams(Hypervisor.HypervisorType hypervisorType, String templateName, Long zoneId) { + String configName = ROUTER_TEMPLATE_CONFIGURATION_NAMES.get(hypervisorType); + boolean updated = configurationDao.update(configName, templateName); + if (!updated) { + throw new CloudRuntimeException(String.format("Failed to update configuration parameter %s", configName)); + } + if (zoneId != null) { + dataCenterDetailsDao.removeDetail(zoneId, configName); + } + updated = configurationDao.update(MINIMUM_SYSTEM_VM_VERSION_KEY, getSystemVmTemplateVersion()); + if (!updated) { + throw new CloudRuntimeException(String.format("Failed to update configuration parameter %s", configName)); + } + if (zoneId != null) { + dataCenterDetailsDao.removeDetail(zoneId, MINIMUM_SYSTEM_VM_VERSION_KEY); } } - private static Pair readTemplatePropertiesSizes(String path) { - File tmpFile = new File(path); - Long size = null; - Long physicalSize = 0L; - try (FileReader fr = new FileReader(tmpFile); BufferedReader brf = new BufferedReader(fr);) { - String line = null; - while ((line = brf.readLine()) != null) { - if (line.startsWith("size=")) { - physicalSize = Long.parseLong(line.split("=")[1]); - } else if (line.startsWith("virtualsize=")) { - size = Long.parseLong(line.split("=")[1]); - } - if (size == null) { - size = physicalSize; - } - } - } catch (IOException ex) { - LOGGER.warn("Failed to read from template.properties", ex); - } - return new Pair<>(size, physicalSize); - } - - public static void readTemplateProperties(String path, SystemVMTemplateDetails details) { - Pair templateSizes = readTemplatePropertiesSizes(path); - details.setSize(templateSizes.first()); - details.setPhysicalSize(templateSizes.second()); - } - - private void updateTemplateTablesOnFailure(long templateId) { + protected void updateTemplateEntriesOnFailure(long templateId) { VMTemplateVO template = vmTemplateDao.createForUpdate(templateId); template.setState(VirtualMachineTemplate.State.Inactive); vmTemplateDao.update(template.getId(), template); vmTemplateDao.remove(templateId); - TemplateDataStoreVO templateDataStoreVO = templateDataStoreDao.findByTemplate(template.getId(), DataStoreRole.Image); + TemplateDataStoreVO templateDataStoreVO = templateDataStoreDao.findByTemplate(template.getId(), + DataStoreRole.Image); + if (templateDataStoreVO == null) { + return; + } templateDataStoreDao.remove(templateDataStoreVO.getId()); } - public static void unmountStore(String filePath) { - try { - LOGGER.info("Unmounting store"); - String umountCmd = String.format(UMOUNT_COMMAND, filePath); - Script.runSimpleBashScript(umountCmd); - try { - Files.deleteIfExists(Paths.get(filePath)); - } catch (IOException e) { - LOGGER.error(String.format("Failed to cleanup mounted store at: %s", filePath), e); - } - } catch (Exception e) { - String msg = String.format("Failed to unmount store mounted at %s", filePath); - LOGGER.error(msg, e); - throw new CloudRuntimeException(msg, e); - } - } - - private void setupTemplate(String templateName, Hypervisor.HypervisorType hypervisor, CPU.CPUArch arch, - String destTempFolder) throws CloudRuntimeException { - String setupTmpltScript = Script.findScript(storageScriptsDir, "setup-sysvm-tmplt"); + protected void setupTemplateOnStore(String templateName, MetadataTemplateDetails templateDetails, + String destTempFolder) throws CloudRuntimeException { + String setupTmpltScript = Script.findScript(STORAGE_SCRIPTS_DIR, "setup-sysvm-tmplt"); if (setupTmpltScript == null) { - throw new CloudRuntimeException("Unable to find the createtmplt.sh"); + throw new CloudRuntimeException("Unable to find the setup-sysvm-tmplt script"); } Script scr = new Script(setupTmpltScript, SCRIPT_TIMEOUT, LOGGER); scr.add("-u", templateName); - MetadataTemplateDetails templateDetails = NewTemplateMap.get(getHypervisorArchKey(hypervisor, arch)); String filePath = StringUtils.isNotBlank(templateDetails.getDownloadedFilePath()) ? templateDetails.getDownloadedFilePath() : templateDetails.getDefaultFilePath(); scr.add("-f", filePath); - scr.add("-h", hypervisor.name().toLowerCase(Locale.ROOT)); + scr.add("-h", templateDetails.getHypervisorType().name().toLowerCase(Locale.ROOT)); scr.add("-d", destTempFolder); String result = scr.execute(); if (result != null) { @@ -702,17 +768,33 @@ public class SystemVmTemplateRegistration { } } - private Long performTemplateRegistrationOperations(Hypervisor.HypervisorType hypervisor, - String name, CPU.CPUArch arch, String url, String checksum, ImageFormat format, long guestOsId, - Long storeId, Long templateId, String filePath, TemplateDataStoreVO templateDataStoreVO) { + /** + * Register or update a system VM Template record and seed it on the target store. + * + * @param name display name of the template + * @param templateDetails metadata for the template + * @param url download URL of the template + * @param checksum expected checksum of the template file + * @param format image format of the template + * @param guestOsId guest OS id + * @param storeId target image store id + * @param templateId existing template id if present, otherwise {@code null} + * @param filePath temporary mount path for the store + * @param templateDataStoreVO existing template-store mapping; may be {@code null} + * @return the id of the template that was created or updated + */ + protected Long performTemplateRegistrationOperations(String name, MetadataTemplateDetails templateDetails, + String url, String checksum, ImageFormat format, long guestOsId, Long storeId, Long templateId, + String filePath, TemplateDataStoreVO templateDataStoreVO) { String templateName = UUID.randomUUID().toString(); Date created = new Date(DateUtil.currentGMTTime().getTime()); - SystemVMTemplateDetails details = new SystemVMTemplateDetails(templateName, name, created, - url, checksum, format, (int) guestOsId, hypervisor, arch, storeId); + SystemVMTemplateDetails details = new SystemVMTemplateDetails(templateName, name, created, url, checksum, + format, (int) guestOsId, templateDetails.getHypervisorType(), templateDetails.getArch(), storeId); if (templateId == null) { VMTemplateVO template = createTemplateObjectInDB(details); if (template == null) { - throw new CloudRuntimeException(String.format("Failed to register Template for hypervisor: %s", hypervisor.name())); + throw new CloudRuntimeException(String.format("Failed to register Template for hypervisor: %s", + templateDetails.getHypervisorType().name())); } templateId = template.getId(); } @@ -721,153 +803,126 @@ public class SystemVmTemplateRegistration { details.setId(templateId); String destTempFolderName = String.valueOf(templateId); String destTempFolder = filePath + PARTIAL_TEMPLATE_FOLDER + destTempFolderName; - details.setInstallPath(PARTIAL_TEMPLATE_FOLDER + destTempFolderName + File.separator + templateName + "." + hypervisorImageFormat.get(hypervisor).getFileExtension()); + details.setInstallPath(String.format("%s%s%s%s.%s", PARTIAL_TEMPLATE_FOLDER, destTempFolderName, + File.separator, templateName, + HYPERVISOR_IMAGE_FORMAT_MAP.get(templateDetails.getHypervisorType()).getFileExtension())); if (templateDataStoreVO == null) { createTemplateStoreRefEntry(details); } - setupTemplate(templateName, hypervisor, arch, destTempFolder); + setupTemplateOnStore(templateName, templateDetails, destTempFolder); readTemplateProperties(destTempFolder + "/template.properties", details); details.setUpdated(new Date(DateUtil.currentGMTTime().getTime())); updateTemplateDetails(details); return templateId; } - public void registerTemplate(Hypervisor.HypervisorType hypervisor, String name, Long storeId, - VMTemplateVO templateVO, TemplateDataStoreVO templateDataStoreVO, String filePath) { + /** + * Add an existing system VM Template to a secondary image store and update related DB entries. + * + * @param templateVO the existing VM template (must not be null) + * @param templateDetails the metadata details of the template to be added + * @param templateDataStoreVO optional existing template-store mapping; may be null + * @param zoneId zone id where the operation is performed + * @param storeId target image store id + * @param filePath temporary mount path for the store + * @throws CloudRuntimeException on failure; the method attempts rollback/cleanup + */ + protected void addExistingTemplateToStore(VMTemplateVO templateVO, MetadataTemplateDetails templateDetails, + TemplateDataStoreVO templateDataStoreVO, long zoneId, Long storeId, String filePath) { try { - performTemplateRegistrationOperations(hypervisor, name, templateVO.getArch(), templateVO.getUrl(), + performTemplateRegistrationOperations(templateVO.getName(), templateDetails, templateVO.getUrl(), templateVO.getChecksum(), templateVO.getFormat(), templateVO.getGuestOSId(), storeId, templateVO.getId(), filePath, templateDataStoreVO); } catch (Exception e) { - String errMsg = String.format("Failed to register Template for hypervisor: %s", hypervisor); + String errMsg = String.format("Failed to add %s to store ID: %d, zone ID: %d", templateVO, storeId, zoneId); LOGGER.error(errMsg, e); - updateTemplateTablesOnFailure(templateVO.getId()); cleanupStore(templateVO.getId(), filePath); throw new CloudRuntimeException(errMsg, e); } } - public void registerTemplateForNonExistingEntries(Hypervisor.HypervisorType hypervisor, CPU.CPUArch arch, - String name, Pair storeUrlAndId, String filePath) { + /** + * Registers a new system VM Template for the given hypervisor/arch when no existing template is present. + * + * @param name the name of the new template + * @param templateDetails the metadata details of the template to be registered + * @param zoneId the zone id for which the new template should be seeded + * @param storeId the store id on which the new template will be seeded + * @param filePath temporary mount path for the store + * @throws CloudRuntimeException on failure; the method attempts rollback/cleanup + */ + protected void registerNewTemplate(String name, MetadataTemplateDetails templateDetails, long zoneId, Long storeId, + String filePath) { Long templateId = null; + Hypervisor.HypervisorType hypervisor = templateDetails.getHypervisorType(); try { - MetadataTemplateDetails templateDetails = getMetadataTemplateDetails(hypervisor, arch); - templateId = performTemplateRegistrationOperations(hypervisor, name, - templateDetails.getArch(), templateDetails.getUrl(), - templateDetails.getChecksum(), hypervisorImageFormat.get(hypervisor), - hypervisorGuestOsMap.get(hypervisor), storeUrlAndId.second(), null, filePath, null); - Map configParams = new HashMap<>(); - configParams.put(RouterTemplateConfigurationNames.get(hypervisor), templateDetails.getName()); - configParams.put("minreq.sysvmtemplate.version", getSystemVmTemplateVersion()); - updateConfigurationParams(configParams); + templateId = performTemplateRegistrationOperations(name, templateDetails, templateDetails.getUrl(), + templateDetails.getChecksum(), HYPERVISOR_IMAGE_FORMAT_MAP.get(hypervisor), + hypervisorGuestOsMap.get(hypervisor), storeId, null, filePath, null); + updateConfigurationParams(hypervisor, name, zoneId); updateSystemVMEntries(templateId, hypervisor); } catch (Exception e) { String errMsg = String.format("Failed to register Template for hypervisor: %s", hypervisor); LOGGER.error(errMsg, e); if (templateId != null) { - updateTemplateTablesOnFailure(templateId); + updateTemplateEntriesOnFailure(templateId); cleanupStore(templateId, filePath); } throw new CloudRuntimeException(errMsg, e); } } - protected void validateTemplateFileForHypervisorAndArch(Hypervisor.HypervisorType hypervisor, CPU.CPUArch arch) { + /** + * Validate presence and integrity of metadata and local template file for the given hypervisor/arch. + * + * @param hypervisor target hypervisor type + * @param arch target CPU architecture + * @return validated MetadataTemplateDetails + * @throws CloudRuntimeException if template is not available, missing, or checksum validation fails + */ + protected MetadataTemplateDetails getValidatedTemplateDetailsForHypervisorAndArch( + Hypervisor.HypervisorType hypervisor, CPU.CPUArch arch) { + if (!AVAILABLE_SYSTEM_TEMPLATES_HYPERVISOR_ARCH_LIST.contains(new Pair<>(hypervisor, arch))) { + throw new CloudRuntimeException("No system VM Template available for the given hypervisor and arch"); + } MetadataTemplateDetails templateDetails = getMetadataTemplateDetails(hypervisor, arch); + if (templateDetails == null) { + throw new CloudRuntimeException("No template details found for the given hypervisor and arch"); + } File templateFile = getTemplateFile(templateDetails); if (templateFile == null) { throw new CloudRuntimeException("Failed to find local template file"); } - if (isTemplateFileChecksumDifferent(templateDetails, templateFile)) { + if (templateDetails.isFileChecksumDifferent(templateFile)) { throw new CloudRuntimeException("Checksum failed for local template file"); } - } - - public void validateAndRegisterTemplate(Hypervisor.HypervisorType hypervisor, String name, Long storeId, - VMTemplateVO templateVO, TemplateDataStoreVO templateDataStoreVO, String filePath) { - validateTemplateFileForHypervisorAndArch(hypervisor, templateVO.getArch()); - registerTemplate(hypervisor, name, storeId, templateVO, templateDataStoreVO, filePath); - } - - public void validateAndRegisterTemplateForNonExistingEntries(Hypervisor.HypervisorType hypervisor, - CPU.CPUArch arch, String name, Pair storeUrlAndId, String filePath) { - validateTemplateFileForHypervisorAndArch(hypervisor, arch); - registerTemplateForNonExistingEntries(hypervisor, arch, name, storeUrlAndId, filePath); - } - - protected static String getMetadataFilePath() { - return METADATA_FILE; + return templateDetails; } /** - * This method parses the metadata file consisting of the systemVM templates information - * @return the version of the systemvm template that is to be used. This is done in order - * to fallback on the latest available version of the systemVM template when there doesn't - * exist a template corresponding to the current code version. + * Return the local template file. Downloads it if not present locally and url is present. + * + * @param templateDetails template metadata; may set `downloadedFilePath` + * @return the template {@code File} on disk, or {@code null} if not found/downloaded */ - public static String parseMetadataFile() { - String metadataFilePath = getMetadataFilePath(); - String errMsg = String.format("Failed to parse systemVM Template metadata file: %s", metadataFilePath); - final Ini ini = new Ini(); - try (FileReader reader = new FileReader(metadataFilePath)) { - ini.load(reader); - } catch (IOException e) { - LOGGER.error(errMsg, e); - throw new CloudRuntimeException(errMsg, e); - } - if (!ini.containsKey("default")) { - errMsg = String.format("%s as unable to default section", errMsg); - LOGGER.error(errMsg); - throw new CloudRuntimeException(errMsg); - } - for (Pair hypervisorType : hypervisorList) { - String key = getHypervisorArchKey(hypervisorType.first(), hypervisorType.second()); - Ini.Section section = ini.get(key); - if (section == null) { - LOGGER.error("Failed to find details for {} in template metadata file: {}", - key, metadataFilePath); - continue; - } - NewTemplateMap.put(key, new MetadataTemplateDetails( - hypervisorType.first(), - section.get("templatename"), - section.get("filename"), - section.get("downloadurl"), - section.get("checksum"), - hypervisorType.second(), - section.get("guestos"))); - } - Ini.Section defaultSection = ini.get("default"); - return defaultSection.get("version").trim(); - } - - - private static void cleanupStore(Long templateId, String filePath) { - String destTempFolder = filePath + PARTIAL_TEMPLATE_FOLDER + String.valueOf(templateId); - try { - Files.deleteIfExists(Paths.get(destTempFolder)); - } catch (IOException e) { - LOGGER.error(String.format("Failed to cleanup mounted store at: %s", filePath), e); - } - } - protected File getTemplateFile(MetadataTemplateDetails templateDetails) { File templateFile = new File(templateDetails.getDefaultFilePath()); if (templateFile.exists()) { return templateFile; } LOGGER.debug("{} is not present", templateFile.getAbsolutePath()); - if (DOWNLOADABLE_TEMPLATE_ARCH_TYPES.contains(templateDetails.getArch()) && - StringUtils.isNotBlank(templateDetails.getUrl())) { + if (StringUtils.isNotBlank(templateDetails.getUrl())) { LOGGER.debug("Downloading the template file {} for {}", templateDetails.getUrl(), templateDetails.getHypervisorArchLog()); Path path = Path.of(TEMPLATES_PATH); if (!Files.isWritable(path)) { - templateFile = new File(tempDownloadDir, templateDetails.getFilename()); + templateFile = new File(getTempDownloadDir(), templateDetails.getFilename()); } if (!templateFile.exists() && !HttpUtils.downloadFileWithProgress(templateDetails.getUrl(), templateFile.getAbsolutePath(), LOGGER)) { + LOGGER.error("Failed to download template for {} using url: {}", + templateDetails.getHypervisorArchLog(), templateDetails.getUrl()); return null; } templateDetails.setDownloadedFilePath(templateFile.getAbsolutePath()); @@ -875,32 +930,28 @@ public class SystemVmTemplateRegistration { return templateFile; } - protected boolean isTemplateFileChecksumDifferent(MetadataTemplateDetails templateDetails, File templateFile) { - String templateChecksum = DigestHelper.calculateChecksum(templateFile); - if (!templateChecksum.equals(templateDetails.getChecksum())) { - LOGGER.error("Checksum {} for file {} does not match checksum {} from metadata", - templateChecksum, templateFile, templateDetails.getChecksum()); - return true; - } - return false; - } - - protected void validateTemplates(List> hypervisorsArchInUse) { + /** + * Validate that templates for the provided hypervisor/architecture pairs which are in use and are valid. + * + * If a template is missing or validation fails for any required pair, a + * {@link CloudRuntimeException} is thrown to abort the upgrade. If system VM Template for a hypervisor/arch is + * not considered available then validation is skipped for that pair. + * + * @param hypervisorArchList list of hypervisor/architecture pairs to validate + */ + protected void validateTemplates(List> hypervisorArchList) { boolean templatesFound = true; - for (Pair hypervisorArch : hypervisorsArchInUse) { - MetadataTemplateDetails matchedTemplate = getMetadataTemplateDetails(hypervisorArch.first(), - hypervisorArch.second()); - if (matchedTemplate == null) { - templatesFound = false; - break; - } - File tempFile = getTemplateFile(matchedTemplate); - if (tempFile == null) { - LOGGER.warn("Failed to download template for {}, moving ahead", - matchedTemplate.getHypervisorArchLog()); + for (Pair hypervisorArch : hypervisorArchList) { + if (!AVAILABLE_SYSTEM_TEMPLATES_HYPERVISOR_ARCH_LIST.contains(hypervisorArch)) { + LOGGER.info("No system VM Template available for {}. Skipping validation.", + getHypervisorArchLog(hypervisorArch.first(), hypervisorArch.second())); continue; } - if (isTemplateFileChecksumDifferent(matchedTemplate, tempFile)) { + try { + getValidatedTemplateDetailsForHypervisorAndArch(hypervisorArch.first(), hypervisorArch.second()); + } catch (CloudRuntimeException e) { + LOGGER.error("Validation failed for {}: {}", + getHypervisorArchLog(hypervisorArch.first(), hypervisorArch.second()), e.getMessage()); templatesFound = false; break; } @@ -912,10 +963,20 @@ public class SystemVmTemplateRegistration { } } - protected void registerTemplatesForZone(long zoneId, String filePath) { + /** + * Register or ensure system VM Templates are present on the NFS store for a given zone. + * + * Mounts the zone image store, enumerates hypervisors and architectures in the zone, + * and for each template either adds an existing template to the store or registers + * a new template as required. + * + * @param zoneId the zone id + * @param storeMountPath temporary mount path for the store + */ + protected void registerTemplatesForZone(long zoneId, String storeMountPath) { Pair storeUrlAndId = getNfsStoreInZone(zoneId); String nfsVersion = getNfsVersion(storeUrlAndId.second()); - mountStore(storeUrlAndId.first(), filePath, nfsVersion); + mountStore(storeUrlAndId.first(), storeMountPath, nfsVersion); List> hypervisorArchList = clusterDao.listDistinctHypervisorsAndArchExcludingExternalType(zoneId); for (Pair hypervisorArch : hypervisorArchList) { @@ -925,7 +986,8 @@ public class SystemVmTemplateRegistration { if (templateDetails == null) { continue; } - VMTemplateVO templateVO = getRegisteredTemplate(templateDetails.getName(), templateDetails.getArch()); + VMTemplateVO templateVO = getRegisteredTemplate(templateDetails.getName(), + templateDetails.getHypervisorType(), templateDetails.getArch(), templateDetails.getUrl()); if (templateVO != null) { TemplateDataStoreVO templateDataStoreVO = templateDataStoreDao.findByStoreTemplate(storeUrlAndId.second(), templateVO.getId()); @@ -935,22 +997,22 @@ public class SystemVmTemplateRegistration { continue; } } - registerTemplate(hypervisorType, templateDetails.getName(), storeUrlAndId.second(), templateVO, - templateDataStoreVO, filePath); - updateRegisteredTemplateDetails(templateVO.getId(), templateDetails); + addExistingTemplateToStore(templateVO, templateDetails, templateDataStoreVO, zoneId, + storeUrlAndId.second(), storeMountPath); + updateRegisteredTemplateDetails(templateVO.getId(), templateDetails, zoneId); continue; } - registerTemplateForNonExistingEntries(hypervisorType, templateDetails.getArch(), templateDetails.getName(), - storeUrlAndId, filePath); + registerNewTemplate(templateDetails.getName(), templateDetails, zoneId, storeUrlAndId.second(), + storeMountPath); } } - public void registerTemplates(List> hypervisorsArchInUse) { + protected void registerTemplates(List> hypervisorsArchInUse) { GlobalLock lock = GlobalLock.getInternLock("UpgradeDatabase-Lock"); try { LOGGER.info("Grabbing lock to register Templates."); if (!lock.lock(LOCK_WAIT_TIMEOUT)) { - throw new CloudRuntimeException("Unable to acquire lock to register SystemVM Template."); + throw new CloudRuntimeException("Unable to acquire lock to register system VM Template."); } try { validateTemplates(hypervisorsArchInUse); @@ -970,13 +1032,13 @@ public class SystemVmTemplateRegistration { unmountStore(filePath); } catch (Exception e) { unmountStore(filePath); - throw new CloudRuntimeException("Failed to register SystemVM Template. Upgrade failed"); + throw new CloudRuntimeException("Failed to register system VM Template. Upgrade Failed"); } } } }); } catch (Exception e) { - throw new CloudRuntimeException("Failed to register SystemVM Template. Upgrade failed"); + throw new CloudRuntimeException("Failed to register system VM Template. Upgrade Failed"); } } finally { lock.unlock(); @@ -984,7 +1046,18 @@ public class SystemVmTemplateRegistration { } } - private void updateRegisteredTemplateDetails(Long templateId, MetadataTemplateDetails templateDetails) { + /** + * Update the DB record for an existing template to mark it as a system template, + * set the guest OS (if resolvable), and propagate the change to system VM entries + * and related configuration for the template's hypervisor. + * + * @param templateId id of the template to update + * @param templateDetails metadata used to update the template record + * @param zoneId zone id whose per-zone details (if any) should be cleared; may be null + * @throws CloudRuntimeException if updating the template record fails + */ + protected void updateRegisteredTemplateDetails(Long templateId, MetadataTemplateDetails templateDetails, + Long zoneId) { VMTemplateVO templateVO = vmTemplateDao.findById(templateId); templateVO.setTemplateType(Storage.TemplateType.SYSTEM); GuestOSVO guestOS = guestOSDao.findOneByDisplayName(templateDetails.getGuestOs()); @@ -993,20 +1066,18 @@ public class SystemVmTemplateRegistration { } boolean updated = vmTemplateDao.update(templateVO.getId(), templateVO); if (!updated) { - String errMsg = String.format("updateSystemVmTemplates:Exception while updating Template with id %s to be marked as 'system'", templateId); + String errMsg = String.format("Exception while updating template with id %s to be marked as 'system'", + templateId); LOGGER.error(errMsg); throw new CloudRuntimeException(errMsg); } Hypervisor.HypervisorType hypervisorType = templateDetails.getHypervisorType(); updateSystemVMEntries(templateId, hypervisorType); - // Change value of global configuration parameter router.template.* for the corresponding hypervisor and minreq.sysvmtemplate.version for the ACS version - Map configParams = new HashMap<>(); - configParams.put(RouterTemplateConfigurationNames.get(hypervisorType), templateDetails.getName()); - configParams.put("minreq.sysvmtemplate.version", getSystemVmTemplateVersion()); - updateConfigurationParams(configParams); + updateConfigurationParams(hypervisorType, templateDetails.getName(), zoneId); } - private void updateTemplateUrlChecksumAndGuestOsId(VMTemplateVO templateVO, MetadataTemplateDetails templateDetails) { + protected void updateTemplateUrlChecksumAndGuestOsId(VMTemplateVO templateVO, + MetadataTemplateDetails templateDetails) { templateVO.setUrl(templateDetails.getUrl()); templateVO.setChecksum(templateDetails.getChecksum()); GuestOSVO guestOS = guestOSDao.findOneByDisplayName(templateDetails.getGuestOs()); @@ -1015,78 +1086,64 @@ public class SystemVmTemplateRegistration { } boolean updated = vmTemplateDao.update(templateVO.getId(), templateVO); if (!updated) { - String errMsg = String.format("updateSystemVmTemplates:Exception while updating 'url' and 'checksum' for hypervisor type %s", templateDetails.getHypervisorType()); + String errMsg = String.format("Exception while updating 'url' and 'checksum' for hypervisor type %s", + templateDetails.getHypervisorType()); LOGGER.error(errMsg); throw new CloudRuntimeException(errMsg); } } - protected boolean registerOrUpdateSystemVmTemplate(MetadataTemplateDetails templateDetails, - List> hypervisorsInUse) { - LOGGER.debug("Updating System VM template for {}", templateDetails.getHypervisorArchLog()); - VMTemplateVO registeredTemplate = getRegisteredTemplate(templateDetails.getName(), templateDetails.getArch()); - // change template type to SYSTEM + /** + * Updates or registers the system VM Template for the given hypervisor/arch if not already present. + * Returns true if a new template was registered. + * If there is an existing system VM Template for the given hypervisor/arch, its details are updated. + * If no existing template is found, new templates are registered for the valid hypervisor/arch which are in use. + */ + protected boolean updateOrRegisterSystemVmTemplate(MetadataTemplateDetails templateDetails, + List> hypervisorArchInUse) { + String systemVmTemplateLog = String.format("%s system VM Template for %s", getSystemVmTemplateVersion(), + templateDetails.getHypervisorArchLog()); + LOGGER.debug("Registering or updating {}", systemVmTemplateLog, + templateDetails.getHypervisorArchLog()); + VMTemplateVO registeredTemplate = getRegisteredTemplate(templateDetails.getName(), + templateDetails.getHypervisorType(), templateDetails.getArch(), templateDetails.getUrl()); if (registeredTemplate != null) { - updateRegisteredTemplateDetails(registeredTemplate.getId(), templateDetails); - } else { - boolean isHypervisorArchMatchMetadata = hypervisorsInUse.stream() - .anyMatch(p -> p.first().equals(templateDetails.getHypervisorType()) - && Objects.equals(p.second(), templateDetails.getArch())); - if (isHypervisorArchMatchMetadata) { - try { - registerTemplates(hypervisorsInUse); - return true; - } catch (final Exception e) { - throw new CloudRuntimeException(String.format("Failed to register %s templates for hypervisors: [%s]. " + - "Cannot upgrade system VMs", - getSystemVmTemplateVersion(), - StringUtils.join(hypervisorsInUse.stream() - .map(x -> getHypervisorArchKey(x.first(), x.second())) - .collect(Collectors.toList()), ",")), e); - } - } else { - LOGGER.warn("Cannot upgrade {} system VM template for {} as it is not used, not failing upgrade", - getSystemVmTemplateVersion(), templateDetails.getHypervisorArchLog()); - VMTemplateVO templateVO = vmTemplateDao.findLatestTemplateByTypeAndHypervisorAndArch( - templateDetails.getHypervisorType(), templateDetails.getArch(), Storage.TemplateType.SYSTEM); - if (templateVO != null) { - updateTemplateUrlChecksumAndGuestOsId(templateVO, templateDetails); - } - } + LOGGER.info("{} is already registered, updating details for: {}", + systemVmTemplateLog, templateDetails.getHypervisorArchLog(), registeredTemplate); + updateRegisteredTemplateDetails(registeredTemplate.getId(), templateDetails, null); + return false; } - return false; - } - - public void updateSystemVmTemplates(final Connection conn) { - LOGGER.debug("Updating System Vm template IDs"); - updateSystemVmTemplateGuestOsId(); - Transaction.execute(new TransactionCallbackNoReturn() { - @Override - public void doInTransactionWithoutResult(final TransactionStatus status) { - List> hypervisorsInUse; - try { - hypervisorsInUse = clusterDao.listDistinctHypervisorsAndArchExcludingExternalType(null); - } catch (final Exception e) { - throw new CloudRuntimeException("Exception while getting hypervisor types from clusters", e); - } - Collection templateEntries = NewTemplateMap.values(); - for (MetadataTemplateDetails templateDetails : templateEntries) { - try { - if (registerOrUpdateSystemVmTemplate(templateDetails, hypervisorsInUse)) { - break; - } - } catch (final Exception e) { - String errMsg = "Exception while registering/updating system VM Templates for hypervisors in metadata"; - LOGGER.error(errMsg, e); - throw new CloudRuntimeException(errMsg, e); - } - } - LOGGER.debug("Updating System Vm Template IDs Complete"); + boolean isHypervisorArchMatchMetadata = hypervisorArchInUse.stream() + .anyMatch(p -> p.first().equals(templateDetails.getHypervisorType()) + && Objects.equals(p.second(), templateDetails.getArch())); + if (!isHypervisorArchMatchMetadata) { + LOGGER.warn("Skipping upgrading {} as it is not used, not failing upgrade", + getSystemVmTemplateVersion(), templateDetails.getHypervisorArchLog()); + VMTemplateVO templateVO = vmTemplateDao.findLatestTemplateByTypeAndHypervisorAndArch( + templateDetails.getHypervisorType(), templateDetails.getArch(), Storage.TemplateType.SYSTEM); + if (templateVO != null) { + updateTemplateUrlChecksumAndGuestOsId(templateVO, templateDetails); } - }); + return false; + } + try { + registerTemplates(hypervisorArchInUse); + return true; + } catch (final Exception e) { + throw new CloudRuntimeException(String.format("Failed to register %s templates for hypervisors: [%s]. " + + "Cannot upgrade system VMs", + getSystemVmTemplateVersion(), + StringUtils.join(hypervisorArchInUse.stream() + .map(x -> String.format("%s-%s", x.first().name(), x.second().name())) + .collect(Collectors.toList()), ",")), e); + } } - public String getNfsVersion(long storeId) { + /** + * Return NFS version for the store: store-specific config if present + * or global config if absent. Returns null if not set. + */ + protected String getNfsVersion(long storeId) { final String configKey = "secstorage.nfs.version"; final Map storeDetails = imageStoreDetailsDao.getDetails(storeId); if (storeDetails != null && storeDetails.containsKey(configKey)) { @@ -1099,6 +1156,148 @@ public class SystemVmTemplateRegistration { return null; } + /** + * Validate metadata for the given template's hypervisor/arch and add the existing template + * to the specified secondary store. On success, database entries are created/updated. + * + * @param templateVO template to add + * @param templateDataStoreVO existing template-store mapping; may be null + * @param zoneId zone id where the operation is performed + * @param storeId target image store id + * @param filePath temporary mount path for the store + * @throws CloudRuntimeException on failure; the method attempts rollback/cleanup + */ + public void validateAndAddTemplateToStore(VMTemplateVO templateVO, TemplateDataStoreVO templateDataStoreVO, + long zoneId, long storeId, String filePath) { + MetadataTemplateDetails templateDetails = getValidatedTemplateDetailsForHypervisorAndArch( + templateVO.getHypervisorType(), templateVO.getArch()); + addExistingTemplateToStore(templateVO, templateDetails, templateDataStoreVO, zoneId, storeId, filePath); + } + + /** + * Validate metadata for the given hypervisor/arch and register a new system VM Template + * on the specified store and zone. Creates DB entries and seeds the template on the store. + * + * @param hypervisor hypervisor type + * @param arch cpu architecture + * @param name template name to register + * @param zoneId zone id where the operation is performed + * @param storeId target image store id + * @param filePath temporary mount path for the store + * @throws CloudRuntimeException on failure; the method attempts rollback/cleanup + */ + public void validateAndRegisterNewTemplate(Hypervisor.HypervisorType hypervisor, CPU.CPUArch arch, String name, + long zoneId, long storeId, String filePath) { + MetadataTemplateDetails templateDetails = getValidatedTemplateDetailsForHypervisorAndArch(hypervisor, arch); + registerNewTemplate(name, templateDetails, zoneId, storeId, filePath); + } + + /** + * Check whether the template at the given `path` on NFS `url` is already seeded. + * If found, updates DB with sizes and returns true; otherwise returns false. + * + * @throws CloudRuntimeException on any error + */ + public boolean validateIfSeeded(TemplateDataStoreVO templDataStoreVO, String url, String path, String nfsVersion) { + String filePath = null; + try { + filePath = Files.createTempDirectory(TEMPORARY_SECONDARY_STORE).toString(); + if (filePath == null) { + throw new CloudRuntimeException("Failed to create temporary directory to mount secondary store"); + } + mountStore(url, filePath, nfsVersion); + int lastIdx = path.lastIndexOf(File.separator); + String partialDirPath = path.substring(0, lastIdx); + String templatePath = filePath + File.separator + partialDirPath; + File templateProps = new File(templatePath + "/template.properties"); + if (templateProps.exists()) { + Pair templateSizes = readTemplatePropertiesSizes(templatePath + "/template.properties"); + updateSeededTemplateDetails(templDataStoreVO.getTemplateId(), templDataStoreVO.getDataStoreId(), + templateSizes.first(), templateSizes.second()); + LOGGER.info("System VM template already seeded, skipping registration"); + return true; + } + LOGGER.info("System VM template not seeded"); + return false; + } catch (Exception e) { + LOGGER.error("Failed to verify if the template is seeded", e); + throw new CloudRuntimeException("Failed to verify if the template is seeded", e); + } finally { + unmountStore(filePath); + try { + Files.delete(Path.of(filePath)); + } catch (IOException e) { + LOGGER.error("Failed to delete temporary directory: {}", filePath); + } + } + } + + /** + * Finds a registered system VM Template matching the provided criteria. + * + *

The method first attempts to locate the latest template by {@code templateName}, + * {@code hypervisorType} and {@code arch}. If none is found and a non-blank {@code url} + * is provided, it falls back to searching for an active system template by the + * URL path segment (the substring after the last '/' in the URL).

+ * + * @param templateName the template name to search for + * @param hypervisorType the hypervisor type + * @param arch the CPU architecture + * @param url optional download URL used as a fallback; may be {@code null} or blank + * @return the matching {@code VMTemplateVO} if found; {@code null} otherwise + */ + public VMTemplateVO getRegisteredTemplate(String templateName, Hypervisor.HypervisorType hypervisorType, + CPU.CPUArch arch, String url) { + VMTemplateVO registeredTemplate = vmTemplateDao.findLatestTemplateByName(templateName, hypervisorType, arch); + if (registeredTemplate == null && StringUtils.isNotBlank(url)) { + String urlPath = url.substring(url.lastIndexOf("/") + 1); + LOGGER.debug("No template found by name, falling back to search existing SYSTEM template by " + + "urlPath: {}, hypervisor: {}, arch:{}", urlPath, hypervisorType, arch); + registeredTemplate = vmTemplateDao.findActiveSystemTemplateByHypervisorArchAndUrlPath(hypervisorType, arch, + urlPath); + } + LOGGER.debug("Found existing registered template for hypervisor: {}, arch: {}: {}", hypervisorType, + arch, registeredTemplate); + return registeredTemplate; + } + + /** + * Update or register system VM Templates based on metadata. + * + * Runs the registration logic inside a database transaction: obtains the + * set of hypervisors/architectures in use, iterates over metadata entries + * and attempts to register or update each template. + * + * @param conn retained for compatibility with callers (not used directly) + */ + public void updateSystemVmTemplates(final Connection conn) { + LOGGER.debug("Updating System VM templates"); + updateHypervisorGuestOsMap(); + Transaction.execute(new TransactionCallbackNoReturn() { + @Override + public void doInTransactionWithoutResult(final TransactionStatus status) { + List> hypervisorsInUse; + try { + hypervisorsInUse = clusterDao.listDistinctHypervisorsAndArchExcludingExternalType(null); + } catch (final Exception e) { + throw new CloudRuntimeException("Exception while getting hypervisor types from clusters", e); + } + for (MetadataTemplateDetails templateDetails : METADATA_TEMPLATE_LIST) { + try { + if (updateOrRegisterSystemVmTemplate(templateDetails, hypervisorsInUse)) { + break; + } + } catch (final Exception e) { + String errMsg = "Exception while registering/updating system VM Templates for hypervisors in metadata"; + LOGGER.error(errMsg, e); + throw new CloudRuntimeException(errMsg, e); + } + } + LOGGER.debug("Updating System VM Templates Complete"); + } + }); + } + protected static class MetadataTemplateDetails { private final Hypervisor.HypervisorType hypervisorType; private final String name; @@ -1160,6 +1359,16 @@ public class SystemVmTemplateRegistration { return TEMPLATES_PATH + filename; } + public boolean isFileChecksumDifferent(File file) { + String fileChecksum = DigestHelper.calculateChecksum(file); + if (!fileChecksum.equals(getChecksum())) { + LOGGER.error("Checksum {} for file {} does not match checksum {} from metadata", + fileChecksum, file, getChecksum()); + return true; + } + return false; + } + public String getHypervisorArchLog() { return SystemVmTemplateRegistration.getHypervisorArchLog(hypervisorType, arch); } diff --git a/engine/schema/src/test/java/com/cloud/storage/dao/VMTemplateDaoImplTest.java b/engine/schema/src/test/java/com/cloud/storage/dao/VMTemplateDaoImplTest.java index 3c8e4c046ae..5cff77869be 100644 --- a/engine/schema/src/test/java/com/cloud/storage/dao/VMTemplateDaoImplTest.java +++ b/engine/schema/src/test/java/com/cloud/storage/dao/VMTemplateDaoImplTest.java @@ -76,7 +76,8 @@ public class VMTemplateDaoImplTest { VMTemplateVO expectedTemplate = new VMTemplateVO(); List returnedList = Collections.singletonList(expectedTemplate); doReturn(returnedList).when(templateDao).listBy(any(SearchCriteria.class), any(Filter.class)); - VMTemplateVO result = templateDao.findLatestTemplateByName("test", CPU.CPUArch.getDefault()); + VMTemplateVO result = templateDao.findLatestTemplateByName("test", Hypervisor.HypervisorType.KVM, + CPU.CPUArch.getDefault()); assertNotNull("Expected a non-null template", result); assertEquals("Expected the returned template to be the first element", expectedTemplate, result); } @@ -85,7 +86,8 @@ public class VMTemplateDaoImplTest { public void testFindLatestTemplateByName_ReturnsNullWhenNoTemplateFound() { List emptyList = Collections.emptyList(); doReturn(emptyList).when(templateDao).listBy(any(SearchCriteria.class), any(Filter.class)); - VMTemplateVO result = templateDao.findLatestTemplateByName("test", CPU.CPUArch.getDefault()); + VMTemplateVO result = templateDao.findLatestTemplateByName("test", Hypervisor.HypervisorType.VMware, + CPU.CPUArch.getDefault()); assertNull("Expected null when no templates are found", result); } @@ -94,7 +96,8 @@ public class VMTemplateDaoImplTest { VMTemplateVO expectedTemplate = new VMTemplateVO(); List returnedList = Collections.singletonList(expectedTemplate); doReturn(returnedList).when(templateDao).listBy(any(SearchCriteria.class), any(Filter.class)); - VMTemplateVO result = templateDao.findLatestTemplateByName("test", null); + VMTemplateVO result = templateDao.findLatestTemplateByName("test", Hypervisor.HypervisorType.XenServer, + null); assertNotNull("Expected a non-null template even if arch is null", result); assertEquals("Expected the returned template to be the first element", expectedTemplate, result); } @@ -337,4 +340,82 @@ public class VMTemplateDaoImplTest { VMTemplateVO readyTemplate = templateDao.findSystemVMReadyTemplate(zoneId, Hypervisor.HypervisorType.KVM, CPU.CPUArch.arm64.getType()); Assert.assertEquals(CPU.CPUArch.arm64, readyTemplate.getArch()); } + + @Test + public void findActiveSystemTemplateByHypervisorArchAndUrlPath_ReturnsTemplate() { + VMTemplateVO expectedTemplate = mock(VMTemplateVO.class); + SearchBuilder sb = mock(SearchBuilder.class); + when(sb.entity()).thenReturn(expectedTemplate); + SearchCriteriasc = mock(SearchCriteria.class); + when(sb.create()).thenReturn(sc); + when(templateDao.createSearchBuilder()).thenReturn(sb); + List templates = Collections.singletonList(expectedTemplate); + doReturn(templates).when(templateDao).listBy(any(SearchCriteria.class), any(Filter.class)); + + VMTemplateVO result = templateDao.findActiveSystemTemplateByHypervisorArchAndUrlPath( + Hypervisor.HypervisorType.KVM, CPU.CPUArch.amd64, "testPath"); + + assertNotNull(result); + assertEquals(expectedTemplate, result); + } + + @Test + public void findActiveSystemTemplateByHypervisorArchAndUrlPath_ReturnsNullWhenNoTemplatesFound() { + VMTemplateVO template = mock(VMTemplateVO.class); + SearchBuilder sb = mock(SearchBuilder.class); + when(sb.entity()).thenReturn(template); + SearchCriteriasc = mock(SearchCriteria.class); + when(sb.create()).thenReturn(sc); + when(templateDao.createSearchBuilder()).thenReturn(sb); + doReturn(Collections.emptyList()).when(templateDao).listBy(any(SearchCriteria.class), any(Filter.class)); + + VMTemplateVO result = templateDao.findActiveSystemTemplateByHypervisorArchAndUrlPath( + Hypervisor.HypervisorType.KVM, CPU.CPUArch.amd64, "testPath"); + + assertNull(result); + } + + @Test + public void findActiveSystemTemplateByHypervisorArchAndUrlPath_NullHypervisor() { + VMTemplateVO expectedTemplate = mock(VMTemplateVO.class); + SearchBuilder sb = mock(SearchBuilder.class); + when(sb.entity()).thenReturn(expectedTemplate); + SearchCriteriasc = mock(SearchCriteria.class); + when(sb.create()).thenReturn(sc); + when(templateDao.createSearchBuilder()).thenReturn(sb); + List templates = Collections.singletonList(expectedTemplate); + doReturn(templates).when(templateDao).listBy(any(SearchCriteria.class), any(Filter.class)); + + VMTemplateVO result = templateDao.findActiveSystemTemplateByHypervisorArchAndUrlPath( + null, CPU.CPUArch.amd64, "testPath"); + + assertNotNull(result); + assertEquals(expectedTemplate, result); + } + + @Test + public void findActiveSystemTemplateByHypervisorArchAndUrlPath_NullArch() { + VMTemplateVO expectedTemplate = mock(VMTemplateVO.class); + SearchBuilder sb = mock(SearchBuilder.class); + when(sb.entity()).thenReturn(expectedTemplate); + SearchCriteriasc = mock(SearchCriteria.class); + when(sb.create()).thenReturn(sc); + when(templateDao.createSearchBuilder()).thenReturn(sb); + List templates = Collections.singletonList(expectedTemplate); + doReturn(templates).when(templateDao).listBy(any(SearchCriteria.class), any(Filter.class)); + + VMTemplateVO result = templateDao.findActiveSystemTemplateByHypervisorArchAndUrlPath( + Hypervisor.HypervisorType.KVM, null, "testPath"); + + assertNotNull(result); + assertEquals(expectedTemplate, result); + } + + @Test + public void findActiveSystemTemplateByHypervisorArchAndUrlPath_EmptyUrlPathSuffix() { + VMTemplateVO result = templateDao.findActiveSystemTemplateByHypervisorArchAndUrlPath( + Hypervisor.HypervisorType.KVM, CPU.CPUArch.amd64, ""); + + assertNull(result); + } } diff --git a/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java b/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java index b943f48ad36..8028e78c907 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java @@ -17,6 +17,7 @@ package com.cloud.upgrade; +import static com.cloud.upgrade.SystemVmTemplateRegistration.DEFAULT_SYSTEM_VM_GUEST_OS_NAME; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -25,24 +26,41 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +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; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.File; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Date; import java.util.List; +import java.util.Map; +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.framework.config.impl.ConfigurationVO; +import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; +import org.apache.cloudstack.storage.datastore.db.ImageStoreDetailsDao; +import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; import org.apache.cloudstack.utils.security.DigestHelper; import org.apache.commons.lang3.StringUtils; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -53,15 +71,27 @@ import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; import com.cloud.cpu.CPU; +import com.cloud.dc.DataCenterVO; import com.cloud.dc.dao.ClusterDao; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.dc.dao.DataCenterDetailsDao; import com.cloud.hypervisor.Hypervisor; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.GuestOSVO; +import com.cloud.storage.Storage; +import com.cloud.storage.VMTemplateStorageResourceAssoc; import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.VMTemplateZoneVO; +import com.cloud.storage.dao.GuestOSDao; import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.storage.dao.VMTemplateZoneDao; +import com.cloud.template.VirtualMachineTemplate; import com.cloud.utils.HttpUtils; import com.cloud.utils.Pair; import com.cloud.utils.UriUtils; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.script.Script; +import com.cloud.vm.dao.VMInstanceDao; @RunWith(MockitoJUnitRunner.class) public class SystemVmTemplateRegistrationTest { @@ -72,10 +102,42 @@ public class SystemVmTemplateRegistrationTest { @Mock VMTemplateDao vmTemplateDao; + @Mock + GuestOSDao guestOSDao; + + @Mock + TemplateDataStoreDao templateDataStoreDao; + + @Mock + ConfigurationDao configurationDao; + + @Mock + DataCenterDao dataCenterDao; + + @Mock + DataCenterDetailsDao dataCenterDetailsDao; + + @Mock + VMTemplateZoneDao vmTemplateZoneDao; + + @Mock + ImageStoreDao imageStoreDao; + + @Mock + ImageStoreDetailsDao imageStoreDetailsDao; + + @Mock + VMInstanceDao vmInstanceDao; + @Spy @InjectMocks SystemVmTemplateRegistration systemVmTemplateRegistration = new SystemVmTemplateRegistration(); + @Before + public void setup() { + SystemVmTemplateRegistration.METADATA_TEMPLATE_LIST.clear(); + } + private void setupMetadataFile(MockedStatic mockedStatic, String content) { try { String location = "metadata.ini"; @@ -98,7 +160,7 @@ public class SystemVmTemplateRegistrationTest { setupMetadataFile(mockedStatic, null); CloudRuntimeException exception = assertThrows(CloudRuntimeException.class, SystemVmTemplateRegistration::parseMetadataFile); - assertTrue(exception.getMessage().contains("Failed to parse systemVM Template metadata file")); + assertTrue(exception.getMessage().contains("Failed to parse system VM Template metadata file")); } } @@ -109,7 +171,7 @@ public class SystemVmTemplateRegistrationTest { setupMetadataFile(mockedStatic, "abc"); CloudRuntimeException exception = assertThrows(CloudRuntimeException.class, SystemVmTemplateRegistration::parseMetadataFile); - assertTrue(exception.getMessage().contains("Failed to parse systemVM Template metadata file")); + assertTrue(exception.getMessage().contains("Failed to parse system VM Template metadata file")); } } @@ -141,21 +203,25 @@ public class SystemVmTemplateRegistrationTest { String version = SystemVmTemplateRegistration.parseMetadataFile(); assertEquals("x.y.z.0", version); } - assertNull(SystemVmTemplateRegistration.NewTemplateMap.get("xenserver")); + assertNull(SystemVmTemplateRegistration.getMetadataTemplateDetails(Hypervisor.HypervisorType.XenServer, + CPU.CPUArch.getDefault())); SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = - SystemVmTemplateRegistration.NewTemplateMap.get("kvm-x86_64"); + SystemVmTemplateRegistration.getMetadataTemplateDetails(Hypervisor.HypervisorType.KVM, + CPU.CPUArch.amd64); assertNotNull(templateDetails); assertEquals(CPU.CPUArch.amd64, templateDetails.getArch()); assertEquals(Hypervisor.HypervisorType.KVM, templateDetails.getHypervisorType()); templateDetails = - SystemVmTemplateRegistration.NewTemplateMap.get("kvm-aarch64"); + SystemVmTemplateRegistration.getMetadataTemplateDetails(Hypervisor.HypervisorType.KVM, + CPU.CPUArch.arm64); assertNotNull(templateDetails); assertEquals(CPU.CPUArch.arm64, templateDetails.getArch()); assertEquals(Hypervisor.HypervisorType.KVM, templateDetails.getHypervisorType()); templateDetails = - SystemVmTemplateRegistration.NewTemplateMap.get("vmware"); + SystemVmTemplateRegistration.getMetadataTemplateDetails(Hypervisor.HypervisorType.VMware, + CPU.CPUArch.getDefault()); assertNotNull(templateDetails); - assertNull(templateDetails.getArch()); + assertEquals(CPU.CPUArch.getDefault(), templateDetails.getArch()); assertEquals(Hypervisor.HypervisorType.VMware, templateDetails.getHypervisorType()); } @@ -193,11 +259,10 @@ public class SystemVmTemplateRegistrationTest { SystemVmTemplateRegistration.MetadataTemplateDetails details = new SystemVmTemplateRegistration.MetadataTemplateDetails(Hypervisor.HypervisorType.KVM, "name", "file", "url", "checksum", CPU.CPUArch.amd64, "guestos"); - SystemVmTemplateRegistration.NewTemplateMap.put(SystemVmTemplateRegistration.getHypervisorArchKey( - details.getHypervisorType(), details.getArch()), details); + SystemVmTemplateRegistration.METADATA_TEMPLATE_LIST.add(details); doReturn(null).when(systemVmTemplateRegistration).getTemplateFile(details); try { - systemVmTemplateRegistration.validateTemplateFileForHypervisorAndArch(details.getHypervisorType(), + systemVmTemplateRegistration.getValidatedTemplateDetailsForHypervisorAndArch(details.getHypervisorType(), details.getArch()); fail("Expected CloudRuntimeException due to missing template file"); } catch (CloudRuntimeException e) { @@ -211,12 +276,11 @@ public class SystemVmTemplateRegistrationTest { new SystemVmTemplateRegistration.MetadataTemplateDetails(Hypervisor.HypervisorType.KVM, "name", "file", "url", "checksum", CPU.CPUArch.amd64, "guestos"); File dummyFile = new File("dummy.txt"); - SystemVmTemplateRegistration.NewTemplateMap.put(SystemVmTemplateRegistration.getHypervisorArchKey( - details.getHypervisorType(), details.getArch()), details); + SystemVmTemplateRegistration.METADATA_TEMPLATE_LIST.add(details); doReturn(dummyFile).when(systemVmTemplateRegistration).getTemplateFile(details); - doReturn(true).when(systemVmTemplateRegistration).isTemplateFileChecksumDifferent(details, dummyFile); - try { - systemVmTemplateRegistration.validateTemplateFileForHypervisorAndArch(details.getHypervisorType(), + try (MockedStatic digestMock = Mockito.mockStatic(DigestHelper.class)) { + digestMock.when(() -> DigestHelper.calculateChecksum(dummyFile)).thenReturn("differentChecksum"); + systemVmTemplateRegistration.getValidatedTemplateDetailsForHypervisorAndArch(details.getHypervisorType(), details.getArch()); fail("Expected CloudRuntimeException due to checksum failure"); } catch (CloudRuntimeException e) { @@ -230,42 +294,55 @@ public class SystemVmTemplateRegistrationTest { new SystemVmTemplateRegistration.MetadataTemplateDetails(Hypervisor.HypervisorType.KVM, "name", "file", "url", "checksum", CPU.CPUArch.amd64, "guestos"); File dummyFile = new File("dummy.txt"); - SystemVmTemplateRegistration.NewTemplateMap.put(SystemVmTemplateRegistration.getHypervisorArchKey( - details.getHypervisorType(), details.getArch()), details); + SystemVmTemplateRegistration.METADATA_TEMPLATE_LIST.add(details); doReturn(dummyFile).when(systemVmTemplateRegistration).getTemplateFile(details); - doReturn(false).when(systemVmTemplateRegistration).isTemplateFileChecksumDifferent(details, dummyFile); - systemVmTemplateRegistration.validateTemplateFileForHypervisorAndArch(details.getHypervisorType(), - details.getArch()); + try (MockedStatic digestMock = Mockito.mockStatic(DigestHelper.class)) { + digestMock.when(() -> DigestHelper.calculateChecksum(dummyFile)).thenReturn("checksum"); + systemVmTemplateRegistration.getValidatedTemplateDetailsForHypervisorAndArch(details.getHypervisorType(), + details.getArch()); + } } @Test - public void testValidateAndRegisterTemplate() { + public void testValidateAndAddExistingTemplateToStore() { + long zoneId = 1L; Hypervisor.HypervisorType hypervisor = Hypervisor.HypervisorType.KVM; - String name = "TestTemplate"; - Long storeId = 123L; VMTemplateVO templateVO = new VMTemplateVO(); - templateVO.setArch(CPU.CPUArch.x86); + templateVO.setHypervisorType(hypervisor); + templateVO.setArch(CPU.CPUArch.getDefault()); TemplateDataStoreVO templateDataStoreVO = new TemplateDataStoreVO(); + Long storeId = 123L; String filePath = "/dummy/path"; - doNothing().when(systemVmTemplateRegistration).validateTemplateFileForHypervisorAndArch(hypervisor, templateVO.getArch()); - doNothing().when(systemVmTemplateRegistration).registerTemplate(hypervisor, name, storeId, templateVO, templateDataStoreVO, filePath); - systemVmTemplateRegistration.validateAndRegisterTemplate(hypervisor, name, storeId, templateVO, templateDataStoreVO, filePath); - verify(systemVmTemplateRegistration).validateTemplateFileForHypervisorAndArch(eq(hypervisor), eq(templateVO.getArch())); - verify(systemVmTemplateRegistration).registerTemplate(eq(hypervisor), eq(name), eq(storeId), eq(templateVO), eq(templateDataStoreVO), eq(filePath)); + SystemVmTemplateRegistration.MetadataTemplateDetails details = + mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); + doReturn(details).when(systemVmTemplateRegistration) + .getValidatedTemplateDetailsForHypervisorAndArch(hypervisor, templateVO.getArch()); + doNothing().when(systemVmTemplateRegistration).addExistingTemplateToStore(templateVO, details, + templateDataStoreVO, zoneId, storeId, filePath); + systemVmTemplateRegistration.validateAndAddTemplateToStore(templateVO, templateDataStoreVO, zoneId, storeId, + filePath); + verify(systemVmTemplateRegistration) + .getValidatedTemplateDetailsForHypervisorAndArch(hypervisor, templateVO.getArch()); + verify(systemVmTemplateRegistration).addExistingTemplateToStore(templateVO, details, templateDataStoreVO, + zoneId, storeId, filePath); } @Test - public void testValidateAndRegisterTemplateForNonExistingEntries() { + public void testValidateAndAddExistingTemplateToStoreForNonExistingEntries() { + long zoneId = 1L; Hypervisor.HypervisorType hypervisor = Hypervisor.HypervisorType.KVM; CPU.CPUArch arch = CPU.CPUArch.amd64; String name = "TestTemplateNonExisting"; - Pair storeUrlAndId = new Pair<>("nfs://dummy", 456L); + long storeId = 123L; String filePath = "/dummy/path/nonexisting"; - doNothing().when(systemVmTemplateRegistration).validateTemplateFileForHypervisorAndArch(hypervisor, arch); - doNothing().when(systemVmTemplateRegistration).registerTemplateForNonExistingEntries(hypervisor, arch, name, storeUrlAndId, filePath); - systemVmTemplateRegistration.validateAndRegisterTemplateForNonExistingEntries(hypervisor, arch, name, storeUrlAndId, filePath); - verify(systemVmTemplateRegistration).validateTemplateFileForHypervisorAndArch(eq(hypervisor), eq(arch)); - verify(systemVmTemplateRegistration).registerTemplateForNonExistingEntries(eq(hypervisor), eq(arch), eq(name), eq(storeUrlAndId), eq(filePath)); + SystemVmTemplateRegistration.MetadataTemplateDetails details = + mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); + doReturn(details).when(systemVmTemplateRegistration) + .getValidatedTemplateDetailsForHypervisorAndArch(hypervisor, arch); + doNothing().when(systemVmTemplateRegistration).registerNewTemplate(name, details, zoneId, storeId, filePath); + systemVmTemplateRegistration.validateAndRegisterNewTemplate(hypervisor, arch, name, zoneId, storeId, filePath); + verify(systemVmTemplateRegistration).getValidatedTemplateDetailsForHypervisorAndArch(hypervisor, arch); + verify(systemVmTemplateRegistration).registerNewTemplate(name, details, zoneId, storeId, filePath); } @Test @@ -316,86 +393,73 @@ public class SystemVmTemplateRegistrationTest { } @Test - public void testIsTemplateFileChecksumDifferent_noMismatch() { - SystemVmTemplateRegistration.MetadataTemplateDetails details = - Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); - when(details.getChecksum()).thenReturn("dummyChecksum"); - File file = new File("dummy.txt"); - try (MockedStatic digestMock = Mockito.mockStatic(DigestHelper.class)) { - digestMock.when(() -> DigestHelper.calculateChecksum(file)).thenReturn("dummyChecksum"); - boolean result = systemVmTemplateRegistration.isTemplateFileChecksumDifferent(details, file); - assertFalse(result); - } - } - - @Test - public void testIsTemplateFileChecksumDifferent_mismatch() { - SystemVmTemplateRegistration.MetadataTemplateDetails details = - Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); - when(details.getChecksum()).thenReturn("expectedChecksum"); - File file = new File("dummy.txt"); - try (MockedStatic digestMock = Mockito.mockStatic(DigestHelper.class)) { - digestMock.when(() -> DigestHelper.calculateChecksum(file)).thenReturn("actualChecksum"); - boolean result = systemVmTemplateRegistration.isTemplateFileChecksumDifferent(details, file); - assertTrue(result); - } - } - - @Test(expected = CloudRuntimeException.class) - public void testValidateTemplates_metadataTemplateFailure() { + public void testValidateTemplates_metadataTemplateSkip() { + Hypervisor.HypervisorType hypervisorType = Hypervisor.HypervisorType.VMware; + CPU.CPUArch arch = CPU.CPUArch.arm64; List> list = new ArrayList<>(); - list.add(new Pair<>(Hypervisor.HypervisorType.KVM, CPU.CPUArch.amd64)); + list.add(new Pair<>(hypervisorType, arch)); systemVmTemplateRegistration.validateTemplates(list); + verify(systemVmTemplateRegistration, never()).getValidatedTemplateDetailsForHypervisorAndArch(hypervisorType, + arch); } @Test(expected = CloudRuntimeException.class) public void testValidateTemplates_fileFailure() { + Hypervisor.HypervisorType hypervisorType = Hypervisor.HypervisorType.KVM; + CPU.CPUArch arch = CPU.CPUArch.amd64; List> list = new ArrayList<>(); - list.add(new Pair<>(Hypervisor.HypervisorType.KVM, CPU.CPUArch.amd64)); - + list.add(new Pair<>(hypervisorType, arch)); SystemVmTemplateRegistration.MetadataTemplateDetails details = Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); - SystemVmTemplateRegistration.NewTemplateMap.put(SystemVmTemplateRegistration.getHypervisorArchKey( - Hypervisor.HypervisorType.KVM, CPU.CPUArch.amd64), details); + when(details.getHypervisorType()).thenReturn(hypervisorType); + when(details.getArch()).thenReturn(arch); File mockFile = Mockito.mock(File.class); + when(details.isFileChecksumDifferent(mockFile)).thenReturn(true); + SystemVmTemplateRegistration.METADATA_TEMPLATE_LIST.add(details); doReturn(mockFile).when(systemVmTemplateRegistration).getTemplateFile(details); - doReturn(true).when(systemVmTemplateRegistration).isTemplateFileChecksumDifferent(details, mockFile); systemVmTemplateRegistration.validateTemplates(list); } + @Test(expected = CloudRuntimeException.class) public void testValidateTemplates_downloadableFileNotFound() { CPU.CPUArch arch = SystemVmTemplateRegistration.DOWNLOADABLE_TEMPLATE_ARCH_TYPES.get(0); List> list = new ArrayList<>(); - list.add(new Pair<>(Hypervisor.HypervisorType.KVM, arch)); + Hypervisor.HypervisorType hypervisorType = Hypervisor.HypervisorType.KVM; + list.add(new Pair<>(hypervisorType, arch)); SystemVmTemplateRegistration.MetadataTemplateDetails details = Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); - SystemVmTemplateRegistration.NewTemplateMap.put(SystemVmTemplateRegistration.getHypervisorArchKey( - Hypervisor.HypervisorType.KVM, arch), details); + when(details.getHypervisorType()).thenReturn(hypervisorType); + when(details.getArch()).thenReturn(arch); + SystemVmTemplateRegistration.METADATA_TEMPLATE_LIST.add(details); doReturn(null).when(systemVmTemplateRegistration).getTemplateFile(details); systemVmTemplateRegistration.validateTemplates(list); } @Test public void testValidateTemplates_success() { + Hypervisor.HypervisorType hypervisorType = Hypervisor.HypervisorType.KVM; + CPU.CPUArch arch = CPU.CPUArch.amd64; List> list = new ArrayList<>(); - list.add(new Pair<>(Hypervisor.HypervisorType.KVM, CPU.CPUArch.amd64)); - + list.add(new Pair<>(hypervisorType, arch)); SystemVmTemplateRegistration.MetadataTemplateDetails details = Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); - SystemVmTemplateRegistration.NewTemplateMap.put(SystemVmTemplateRegistration.getHypervisorArchKey( - Hypervisor.HypervisorType.KVM, CPU.CPUArch.amd64), details); + when(details.getHypervisorType()).thenReturn(hypervisorType); + when(details.getArch()).thenReturn(arch); File mockFile = Mockito.mock(File.class); + when(details.isFileChecksumDifferent(mockFile)).thenReturn(false); + SystemVmTemplateRegistration.METADATA_TEMPLATE_LIST.add(details); doReturn(mockFile).when(systemVmTemplateRegistration).getTemplateFile(details); - doReturn(false).when(systemVmTemplateRegistration).isTemplateFileChecksumDifferent(details, mockFile); systemVmTemplateRegistration.validateTemplates(list); } @Test - public void testRegisterTemplatesForZone() { + public void testAddExistingTemplatesForZoneToStore() { long zoneId = 1L; String filePath = "dummyFilePath"; String nfsVersion = "nfs3"; Pair storeUrlAndId = new Pair<>("nfs://dummy", 100L); + String name = "existing"; + String url = "url"; doReturn(storeUrlAndId).when(systemVmTemplateRegistration).getNfsStoreInZone(zoneId); doReturn(nfsVersion).when(systemVmTemplateRegistration).getNfsVersion(storeUrlAndId.second()); try (MockedStatic mockedStatic = Mockito.mockStatic( @@ -407,21 +471,1169 @@ public class SystemVmTemplateRegistrationTest { doReturn(hypervisorArchList).when(clusterDao).listDistinctHypervisorsAndArchExcludingExternalType(zoneId); SystemVmTemplateRegistration.MetadataTemplateDetails details = Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); - String name = "existing"; - Mockito.when(details.getArch()).thenReturn(CPU.CPUArch.getDefault()); - Mockito.when(details.getName()).thenReturn(name); + when(details.getArch()).thenReturn(CPU.CPUArch.getDefault()); + when(details.getName()).thenReturn(name); + when(details.getUrl()).thenReturn(url); mockedStatic.when(() -> SystemVmTemplateRegistration.getMetadataTemplateDetails(Mockito.any(), Mockito.any())).thenReturn(details); - when(systemVmTemplateRegistration.getRegisteredTemplate(name, arch)) - .thenReturn(null); - doNothing().when(systemVmTemplateRegistration).registerTemplateForNonExistingEntries( - hypervisorType, arch, - name, storeUrlAndId, filePath); + doNothing().when(systemVmTemplateRegistration).registerNewTemplate(name, details, zoneId, + storeUrlAndId.second(), filePath); systemVmTemplateRegistration.registerTemplatesForZone(zoneId, filePath); mockedStatic.verify(() -> SystemVmTemplateRegistration.mountStore(storeUrlAndId.first(), filePath, nfsVersion)); - verify(systemVmTemplateRegistration).registerTemplateForNonExistingEntries(hypervisorType, - arch, name, storeUrlAndId, filePath); + verify(systemVmTemplateRegistration).registerNewTemplate(name, details, zoneId, + storeUrlAndId.second(), filePath); } } + + @Test + public void updateOrRegisterSystemVmTemplate_UpdatesRegisteredTemplate() { + SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); + when(templateDetails.getName()).thenReturn("templateName"); + when(templateDetails.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + when(templateDetails.getArch()).thenReturn(CPU.CPUArch.amd64); + when(templateDetails.getUrl()).thenReturn("http://example.com/template"); + VMTemplateVO registeredTemplate = Mockito.mock(VMTemplateVO.class); + when(registeredTemplate.getId()).thenReturn(1L); + doReturn(registeredTemplate).when(systemVmTemplateRegistration).getRegisteredTemplate( + "templateName", Hypervisor.HypervisorType.KVM, CPU.CPUArch.amd64, "http://example.com/template"); + doNothing().when(systemVmTemplateRegistration).updateRegisteredTemplateDetails(1L, templateDetails, + null); + + boolean result = systemVmTemplateRegistration.updateOrRegisterSystemVmTemplate(templateDetails, + new ArrayList<>()); + + assertFalse(result); + verify(systemVmTemplateRegistration).updateRegisteredTemplateDetails(1L, templateDetails, null); + } + + @Test + public void updateOrRegisterSystemVmTemplate_SkipsUnusedHypervisorArch() { + SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); + when(templateDetails.getName()).thenReturn("templateName"); + when(templateDetails.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + when(templateDetails.getArch()).thenReturn(CPU.CPUArch.amd64); + when(templateDetails.getUrl()).thenReturn("http://example.com/template"); + doReturn(null).when(systemVmTemplateRegistration).getRegisteredTemplate( + "templateName", Hypervisor.HypervisorType.KVM, CPU.CPUArch.amd64, "http://example.com/template"); + doReturn(null).when(vmTemplateDao).findLatestTemplateByTypeAndHypervisorAndArch( + Hypervisor.HypervisorType.KVM, CPU.CPUArch.amd64, Storage.TemplateType.SYSTEM); + + boolean result = systemVmTemplateRegistration.updateOrRegisterSystemVmTemplate(templateDetails, new ArrayList<>()); + + assertFalse(result); + verify(systemVmTemplateRegistration, never()).registerTemplates(anyList()); + } + + @Test + public void updateOrRegisterSystemVmTemplate_RegistersNewTemplate() { + SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); + when(templateDetails.getName()).thenReturn("templateName"); + when(templateDetails.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + when(templateDetails.getArch()).thenReturn(CPU.CPUArch.amd64); + when(templateDetails.getUrl()).thenReturn("http://example.com/template"); + doReturn(null).when(systemVmTemplateRegistration).getRegisteredTemplate( + "templateName", Hypervisor.HypervisorType.KVM, CPU.CPUArch.amd64, "http://example.com/template"); + List> hypervisorsInUse = new ArrayList<>(); + hypervisorsInUse.add(new Pair<>(Hypervisor.HypervisorType.KVM, CPU.CPUArch.amd64)); + doNothing().when(systemVmTemplateRegistration).registerTemplates(hypervisorsInUse); + + boolean result = systemVmTemplateRegistration.updateOrRegisterSystemVmTemplate(templateDetails, hypervisorsInUse); + + assertTrue(result); + verify(systemVmTemplateRegistration).registerTemplates(eq(hypervisorsInUse)); + } + + @Test + public void updateOrRegisterSystemVmTemplate_ThrowsExceptionOnRegistrationFailure() { + SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); + when(templateDetails.getName()).thenReturn("templateName"); + when(templateDetails.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + when(templateDetails.getArch()).thenReturn(CPU.CPUArch.amd64); + when(templateDetails.getUrl()).thenReturn("http://example.com/template"); + doReturn(null).when(systemVmTemplateRegistration).getRegisteredTemplate( + "templateName", Hypervisor.HypervisorType.KVM, CPU.CPUArch.amd64, "http://example.com/template"); + List> hypervisorsInUse = new ArrayList<>(); + hypervisorsInUse.add(new Pair<>(Hypervisor.HypervisorType.KVM, CPU.CPUArch.amd64)); + doThrow(new CloudRuntimeException("Registration failed")).when(systemVmTemplateRegistration).registerTemplates(hypervisorsInUse); + + CloudRuntimeException exception = assertThrows(CloudRuntimeException.class, + () -> systemVmTemplateRegistration.updateOrRegisterSystemVmTemplate(templateDetails, hypervisorsInUse)); + + assertTrue(exception.getMessage().contains("Failed to register")); + } + + @Test + public void updateRegisteredTemplateDetails_UpdatesTemplateSuccessfully() { + Long templateId = 1L; + Long zoneId = 2L; + SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = + Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); + VMTemplateVO templateVO = Mockito.mock(VMTemplateVO.class); + GuestOSVO guestOS = Mockito.mock(GuestOSVO.class); + + when(templateDetails.getGuestOs()).thenReturn("Debian"); + when(templateDetails.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + when(templateDetails.getName()).thenReturn("templateName"); + when(vmTemplateDao.findById(templateId)).thenReturn(templateVO); + when(guestOSDao.findOneByDisplayName("Debian")).thenReturn(guestOS); + when(guestOS.getId()).thenReturn(10L); + when(vmTemplateDao.update(templateVO.getId(), templateVO)).thenReturn(true); + doNothing().when(systemVmTemplateRegistration).updateSystemVMEntries(templateId, Hypervisor.HypervisorType.KVM); + doNothing().when(systemVmTemplateRegistration).updateConfigurationParams(Hypervisor.HypervisorType.KVM, + "templateName", zoneId); + + systemVmTemplateRegistration.updateRegisteredTemplateDetails(templateId, templateDetails, zoneId); + + verify(templateVO).setTemplateType(Storage.TemplateType.SYSTEM); + verify(templateVO).setGuestOSId(10); + verify(vmTemplateDao).update(templateVO.getId(), templateVO); + verify(systemVmTemplateRegistration).updateSystemVMEntries(templateId, Hypervisor.HypervisorType.KVM); + verify(systemVmTemplateRegistration).updateConfigurationParams(Hypervisor.HypervisorType.KVM, + "templateName", zoneId); + } + + @Test + public void updateRegisteredTemplateDetails_ThrowsExceptionWhenUpdateFails() { + Long templateId = 1L; + Long zoneId = 2L; + SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = + Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); + VMTemplateVO templateVO = Mockito.mock(VMTemplateVO.class); + + when(templateDetails.getGuestOs()).thenReturn("Debian"); + when(vmTemplateDao.findById(templateId)).thenReturn(templateVO); + when(vmTemplateDao.update(templateVO.getId(), templateVO)).thenReturn(false); + + CloudRuntimeException exception = assertThrows(CloudRuntimeException.class, + () -> systemVmTemplateRegistration.updateRegisteredTemplateDetails(templateId, templateDetails, zoneId)); + + assertTrue(exception.getMessage().contains("Exception while updating template with id")); + verify(systemVmTemplateRegistration, never()).updateSystemVMEntries(anyLong(), any()); + verify(systemVmTemplateRegistration, never()).updateConfigurationParams(any(), any(), any()); + } + + @Test + public void updateRegisteredTemplateDetails_SkipsGuestOSUpdateWhenNotFound() { + Long templateId = 1L; + Long zoneId = 2L; + SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = + Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); + VMTemplateVO templateVO = Mockito.mock(VMTemplateVO.class); + + when(templateDetails.getGuestOs()).thenReturn("NonExistentOS"); + when(templateDetails.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + when(templateDetails.getName()).thenReturn("templateName"); + when(vmTemplateDao.findById(templateId)).thenReturn(templateVO); + when(guestOSDao.findOneByDisplayName("NonExistentOS")).thenReturn(null); + when(vmTemplateDao.update(templateVO.getId(), templateVO)).thenReturn(true); + doNothing().when(systemVmTemplateRegistration).updateSystemVMEntries(templateId, Hypervisor.HypervisorType.KVM); + doNothing().when(systemVmTemplateRegistration).updateConfigurationParams(Hypervisor.HypervisorType.KVM, + "templateName", zoneId); + + systemVmTemplateRegistration.updateRegisteredTemplateDetails(templateId, templateDetails, zoneId); + + verify(templateVO, never()).setGuestOSId(anyInt()); + verify(vmTemplateDao).update(templateVO.getId(), templateVO); + verify(systemVmTemplateRegistration).updateSystemVMEntries(templateId, Hypervisor.HypervisorType.KVM); + verify(systemVmTemplateRegistration).updateConfigurationParams(Hypervisor.HypervisorType.KVM, + "templateName", zoneId); + } + + @Test + public void registerTemplatesForZone_SuccessfullyRegistersNewTemplate() { + long zoneId = 1L; + String storeMountPath = "/mnt/nfs"; + Pair storeUrlAndId = new Pair<>("nfs://dummy", 100L); + String nfsVersion = "nfs3"; + List> hypervisorArchList = new ArrayList<>(); + Hypervisor.HypervisorType hypervisorType = Hypervisor.HypervisorType.KVM; + CPU.CPUArch arch = CPU.CPUArch.amd64; + hypervisorArchList.add(new Pair<>(hypervisorType, arch)); + SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = + Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); + when(templateDetails.getHypervisorType()).thenReturn(hypervisorType); + when(templateDetails.getArch()).thenReturn(arch); + String name = "TestTemplate"; + String url = "http://example.com/template"; + when(templateDetails.getName()).thenReturn(name); + when(templateDetails.getUrl()).thenReturn(url); + doReturn(storeUrlAndId).when(systemVmTemplateRegistration).getNfsStoreInZone(zoneId); + doReturn(nfsVersion).when(systemVmTemplateRegistration).getNfsVersion(storeUrlAndId.second()); + doReturn(null).when(systemVmTemplateRegistration).getRegisteredTemplate( + name, hypervisorType, arch, url); + doNothing().when(systemVmTemplateRegistration).registerNewTemplate( + name, templateDetails, zoneId, storeUrlAndId.second(), storeMountPath); + doReturn(hypervisorArchList).when(clusterDao).listDistinctHypervisorsAndArchExcludingExternalType(zoneId); + try (MockedStatic mockedStatic = + Mockito.mockStatic(SystemVmTemplateRegistration.class)) { + mockedStatic.when(() -> SystemVmTemplateRegistration.getMetadataTemplateDetails( + hypervisorType, arch)).thenReturn(templateDetails); + + systemVmTemplateRegistration.registerTemplatesForZone(zoneId, storeMountPath); + + mockedStatic.verify(() -> SystemVmTemplateRegistration.mountStore( + eq(storeUrlAndId.first()), eq(storeMountPath), eq(nfsVersion)), times(1)); + verify(systemVmTemplateRegistration).registerNewTemplate( + templateDetails.getName(), templateDetails, zoneId, storeUrlAndId.second(), storeMountPath); + } + } + + @Test + public void registerTemplatesForZone_SkipsWhenTemplateDetailsNotFound() { + long zoneId = 1L; + String storeMountPath = "/mnt/nfs"; + Pair storeUrlAndId = new Pair<>("nfs://dummy", 100L); + String nfsVersion = "nfs3"; + List> hypervisorArchList = new ArrayList<>(); + Hypervisor.HypervisorType hypervisorType = Hypervisor.HypervisorType.KVM; + CPU.CPUArch arch = CPU.CPUArch.amd64; + hypervisorArchList.add(new Pair<>(hypervisorType, arch)); + doReturn(storeUrlAndId).when(systemVmTemplateRegistration).getNfsStoreInZone(zoneId); + doReturn(nfsVersion).when(systemVmTemplateRegistration).getNfsVersion(storeUrlAndId.second()); + doReturn(hypervisorArchList).when(clusterDao).listDistinctHypervisorsAndArchExcludingExternalType(zoneId); + + try (MockedStatic mockedStatic = + Mockito.mockStatic(SystemVmTemplateRegistration.class)) { + mockedStatic.when(() -> SystemVmTemplateRegistration.getMetadataTemplateDetails( + hypervisorType, arch)).thenReturn(null); + + systemVmTemplateRegistration.registerTemplatesForZone(zoneId, storeMountPath); + + mockedStatic.verify(() -> SystemVmTemplateRegistration.mountStore( + eq(storeUrlAndId.first()), eq(storeMountPath), eq(nfsVersion)), times(1)); + verify(systemVmTemplateRegistration, never()).registerNewTemplate(any(), any(), anyLong(), anyLong(), any()); + } + } + + @Test + public void registerTemplatesForZone_AddsExistingTemplateToStore() { + long zoneId = 1L; + String storeMountPath = "/mnt/nfs"; + Pair storeUrlAndId = new Pair<>("nfs://dummy", 100L); + String nfsVersion = "nfs3"; + List> hypervisorArchList = new ArrayList<>(); + Hypervisor.HypervisorType hypervisorType = Hypervisor.HypervisorType.KVM; + CPU.CPUArch arch = CPU.CPUArch.amd64; + hypervisorArchList.add(new Pair<>(hypervisorType, arch)); + SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = + Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); + when(templateDetails.getHypervisorType()).thenReturn(hypervisorType); + when(templateDetails.getArch()).thenReturn(arch); + String name = "TestTemplate"; + String url = "http://example.com/template"; + when(templateDetails.getName()).thenReturn(name); + when(templateDetails.getUrl()).thenReturn(url); + VMTemplateVO templateVO = Mockito.mock(VMTemplateVO.class); + long templateId = 100L; + when(templateVO.getId()).thenReturn(templateId); + TemplateDataStoreVO templateDataStoreVO = Mockito.mock(TemplateDataStoreVO.class); + String installPath = "/template/install/path"; + when(templateDataStoreVO.getInstallPath()).thenReturn(installPath); + + doReturn(storeUrlAndId).when(systemVmTemplateRegistration).getNfsStoreInZone(zoneId); + doReturn(nfsVersion).when(systemVmTemplateRegistration).getNfsVersion(storeUrlAndId.second()); + doReturn(hypervisorArchList).when(clusterDao).listDistinctHypervisorsAndArchExcludingExternalType(zoneId); + doReturn(templateVO).when(systemVmTemplateRegistration).getRegisteredTemplate(name, hypervisorType, arch, url); + doReturn(templateDataStoreVO).when(templateDataStoreDao) + .findByStoreTemplate(storeUrlAndId.second(), templateId); + doReturn(false).when(systemVmTemplateRegistration).validateIfSeeded( + templateDataStoreVO, storeUrlAndId.first(), installPath, nfsVersion); + doNothing().when(systemVmTemplateRegistration).addExistingTemplateToStore( + templateVO, templateDetails, templateDataStoreVO, zoneId, storeUrlAndId.second(), storeMountPath); + doNothing().when(systemVmTemplateRegistration).updateRegisteredTemplateDetails( + templateId, templateDetails, zoneId); + + try (MockedStatic mockedStatic = + Mockito.mockStatic(SystemVmTemplateRegistration.class)) { + mockedStatic.when(() -> SystemVmTemplateRegistration.getMetadataTemplateDetails( + hypervisorType, arch)).thenReturn(templateDetails); + + systemVmTemplateRegistration.registerTemplatesForZone(zoneId, storeMountPath); + + verify(systemVmTemplateRegistration).addExistingTemplateToStore( + templateVO, templateDetails, templateDataStoreVO, zoneId, storeUrlAndId.second(), storeMountPath); + verify(systemVmTemplateRegistration).updateRegisteredTemplateDetails(templateId, templateDetails, zoneId); + } + } + + @Test + public void performTemplateRegistrationOperations_CreatesNewTemplateWhenNotExists() { + String name = "TestTemplate"; + String url = "http://example.com/template"; + String checksum = "abc123"; + Storage.ImageFormat format = Storage.ImageFormat.QCOW2; + long guestOsId = 1L; + Long storeId = 100L; + Long templateId = null; + String filePath = "/mnt/nfs"; + TemplateDataStoreVO templateDataStoreVO = null; + SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); + + when(templateDetails.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + when(templateDetails.getArch()).thenReturn(CPU.CPUArch.amd64); + doReturn(new VMTemplateVO()).when(vmTemplateDao).persist(any()); + doNothing().when(systemVmTemplateRegistration).createCrossZonesTemplateZoneRefEntries(anyLong()); + doNothing().when(systemVmTemplateRegistration).createTemplateStoreRefEntry(any()); + doNothing().when(systemVmTemplateRegistration).setupTemplateOnStore(anyString(), any(), anyString()); + doNothing().when(systemVmTemplateRegistration).readTemplateProperties(anyString(), any()); + doNothing().when(systemVmTemplateRegistration).updateTemplateDetails(any()); + + Long result = systemVmTemplateRegistration.performTemplateRegistrationOperations(name, templateDetails, url, checksum, format, guestOsId, storeId, templateId, filePath, templateDataStoreVO); + + assertNotNull(result); + verify(vmTemplateDao).persist(any()); + verify(systemVmTemplateRegistration).createCrossZonesTemplateZoneRefEntries(anyLong()); + verify(systemVmTemplateRegistration).createTemplateStoreRefEntry(any()); + verify(systemVmTemplateRegistration).setupTemplateOnStore(anyString(), any(), anyString()); + verify(systemVmTemplateRegistration).updateTemplateDetails(any()); + } + + @Test + public void performTemplateRegistrationOperations_UpdatesExistingTemplate() { + String name = "TestTemplate"; + String url = "http://example.com/template"; + String checksum = "abc123"; + Storage.ImageFormat format = Storage.ImageFormat.QCOW2; + long guestOsId = 1L; + Long storeId = 100L; + Long templateId = 1L; + String filePath = "/mnt/nfs"; + TemplateDataStoreVO templateDataStoreVO = Mockito.mock(TemplateDataStoreVO.class); + SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); + + when(templateDetails.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + when(templateDetails.getArch()).thenReturn(CPU.CPUArch.amd64); + doNothing().when(systemVmTemplateRegistration).createCrossZonesTemplateZoneRefEntries(anyLong()); + doNothing().when(systemVmTemplateRegistration).setupTemplateOnStore(anyString(), any(), anyString()); + doNothing().when(systemVmTemplateRegistration).readTemplateProperties(anyString(), any()); + doNothing().when(systemVmTemplateRegistration).updateTemplateDetails(any()); + + Long result = systemVmTemplateRegistration.performTemplateRegistrationOperations(name, templateDetails, url, checksum, format, guestOsId, storeId, templateId, filePath, templateDataStoreVO); + + assertNotNull(result); + assertEquals(templateId, result); + verify(vmTemplateDao, never()).persist(any()); + verify(systemVmTemplateRegistration).createCrossZonesTemplateZoneRefEntries(anyLong()); + verify(systemVmTemplateRegistration, never()).createTemplateStoreRefEntry(any()); + verify(systemVmTemplateRegistration).setupTemplateOnStore(anyString(), any(), anyString()); + verify(systemVmTemplateRegistration).updateTemplateDetails(any()); + } + + @Test + public void performTemplateRegistrationOperations_ThrowsExceptionWhenTemplateCreationFails() { + String name = "TestTemplate"; + String url = "http://example.com/template"; + String checksum = "abc123"; + Storage.ImageFormat format = Storage.ImageFormat.QCOW2; + long guestOsId = 1L; + Long storeId = 100L; + Long templateId = null; + String filePath = "/mnt/nfs"; + TemplateDataStoreVO templateDataStoreVO = null; + SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); + + when(templateDetails.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + when(templateDetails.getArch()).thenReturn(CPU.CPUArch.amd64); + doReturn(null).when(vmTemplateDao).persist(any()); + + assertThrows(CloudRuntimeException.class, () -> { + systemVmTemplateRegistration.performTemplateRegistrationOperations(name, templateDetails, url, checksum, format, guestOsId, storeId, templateId, filePath, templateDataStoreVO); + }); + + verify(vmTemplateDao).persist(any()); + verify(systemVmTemplateRegistration, never()).createCrossZonesTemplateZoneRefEntries(anyLong()); + verify(systemVmTemplateRegistration, never()).createTemplateStoreRefEntry(any()); + verify(systemVmTemplateRegistration, never()).setupTemplateOnStore(anyString(), any(), anyString()); + verify(systemVmTemplateRegistration, never()).updateTemplateDetails(any()); + } + + @Test + public void setupTemplateOnStore_ThrowsExceptionWhenScriptNotFound() { + String templateName = "templateName"; + String destTempFolder = "/tmp/folder"; + SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = + Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); + + try (MockedStatic + + diff --git a/ui/src/config/section/tools.js b/ui/src/config/section/tools.js index a07228ca87b..5b7f4b9af32 100644 --- a/ui/src/config/section/tools.js +++ b/ui/src/config/section/tools.js @@ -116,6 +116,10 @@ export default { name: 'details', component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue'))) }, + { + name: 'filters', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/WebhookFiltersTab.vue'))) + }, { name: 'recent.deliveries', component: shallowRef(defineAsyncComponent(() => import('@/components/view/WebhookDeliveriesTab.vue'))) From fca928d609baffa87750d9de8b3d48e8746d64ce Mon Sep 17 00:00:00 2001 From: YoulongChen <30854794+YLChen-007@users.noreply.github.com> Date: Mon, 5 Jan 2026 20:28:48 +0800 Subject: [PATCH 14/51] fix HMAC Signatures and API Keys Logged in Plaintext (#12021) Co-authored-by: chenyoulong20g@ict.ac.cn Co-authored-by: dahn --- .../java/com/cloud/storage/template/HttpTemplateDownloader.java | 2 +- .../schema/src/main/java/com/cloud/upgrade/DatabaseCreator.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/cloud/storage/template/HttpTemplateDownloader.java b/core/src/main/java/com/cloud/storage/template/HttpTemplateDownloader.java index cf49217ef5b..6fe001de72c 100755 --- a/core/src/main/java/com/cloud/storage/template/HttpTemplateDownloader.java +++ b/core/src/main/java/com/cloud/storage/template/HttpTemplateDownloader.java @@ -151,7 +151,7 @@ public class HttpTemplateDownloader extends ManagedContextRunnable implements Te client.getParams().setAuthenticationPreemptive(true); Credentials defaultcreds = new UsernamePasswordCredentials(user, password); client.getState().setCredentials(new AuthScope(hostAndPort.first(), hostAndPort.second(), AuthScope.ANY_REALM), defaultcreds); - logger.info("Added username=" + user + ", password=" + password + "for host " + hostAndPort.first() + ":" + hostAndPort.second()); + logger.info("Added username={}, password=****** for host {}:{}", user, hostAndPort.first(), hostAndPort.second()); } else { logger.info("No credentials configured for host=" + hostAndPort.first() + ":" + hostAndPort.second()); } diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseCreator.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseCreator.java index 384826227af..cccfbe8a006 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseCreator.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseCreator.java @@ -99,7 +99,7 @@ public class DatabaseCreator { String username = dbProperties.getProperty(String.format("db.%s.username", database)); String password = dbProperties.getProperty(String.format("db.%s.password", database)); String dbName = dbProperties.getProperty(String.format("db.%s.name", database)); - System.out.println(String.format("========> Initializing database=%s with host=%s port=%s username=%s password=%s", dbName, host, port, username, password)); + System.out.println(String.format("========> Initializing database=%s with host=%s port=%s username=%s password=******", dbName, host, port, username)); List queries = new ArrayList(); queries.add(String.format("drop database if exists `%s`", dbName)); From a29de0ed0665ea97a8bcdf35a2651b4f5df26494 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Mon, 5 Jan 2026 21:00:39 +0530 Subject: [PATCH 15/51] Retry cloneVM task when any file access issue while cloning from volume or template (#12335) --- .../vmware/mo/VirtualMachineMO.java | 54 +++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/VirtualMachineMO.java b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/VirtualMachineMO.java index 9802328827a..950ec9010cb 100644 --- a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/VirtualMachineMO.java +++ b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/VirtualMachineMO.java @@ -788,11 +788,8 @@ public class VirtualMachineMO extends BaseMO { cloneSpec.setMemory(false); cloneSpec.setConfig(vmConfigSpec); - ManagedObjectReference morTask = _context.getService().cloneVMTask(_mor, morFolder, cloneName, cloneSpec); - - boolean result = _context.getVimClient().waitForTask(morTask); + boolean result = cloneVM(cloneName, morFolder, cloneSpec); if (result) { - _context.waitForTaskProgressDone(morTask); VirtualMachineMO clonedVm = dcMo.findVm(cloneName); if (clonedVm == null) { logger.error(String.format("Failed to clone Instance %s", cloneName)); @@ -802,10 +799,9 @@ public class VirtualMachineMO extends BaseMO { clonedVm.tagAsWorkerVM(); makeSureVMHasOnlyRequiredDisk(clonedVm, requiredDisk, dsMo, dcMo); return clonedVm; - } else { - logger.error("VMware cloneVM_Task failed due to " + TaskMO.getTaskFailureInfo(_context, morTask)); - return null; } + + return null; } private void makeSureVMHasOnlyRequiredDisk(VirtualMachineMO clonedVm, VirtualDisk requiredDisk, DatastoreMO dsMo, DatacenterMO dcMo) throws Exception { @@ -852,16 +848,42 @@ public class VirtualMachineMO extends BaseMO { setDiskProvisioningType(relocSpec, morDs, diskProvisioningType); - ManagedObjectReference morTask = _context.getService().cloneVMTask(_mor, morFolder, cloneName, cloneSpec); + return cloneVM(cloneName, morFolder, cloneSpec); + } + private boolean cloneVMTask(String cloneName, ManagedObjectReference morFolder, VirtualMachineCloneSpec cloneSpec) throws Exception { + ManagedObjectReference morTask = _context.getService().cloneVMTask(_mor, morFolder, cloneName, cloneSpec); boolean result = _context.getVimClient().waitForTask(morTask); if (result) { _context.waitForTaskProgressDone(morTask); return true; - } else { - logger.error("VMware cloneVM_Task failed due to " + TaskMO.getTaskFailureInfo(_context, morTask)); } + logger.error("VMware cloneVM_Task failed due to {}", TaskMO.getTaskFailureInfo(_context, morTask)); + return false; + } + + private boolean cloneVM(final String cloneName, final ManagedObjectReference morFolder, final VirtualMachineCloneSpec cloneSpec) throws Exception { + final int retry = 20; + int retryAttempt = 0; + while (++retryAttempt <= retry) { + try { + logger.debug("Cloning VM {}, attempt #{}", cloneName, retryAttempt); + return cloneVMTask(cloneName, morFolder, cloneSpec); + } catch (Exception e) { + logger.info("Got exception while cloning VM {}", cloneName, e); + if (e.getMessage() != null && e.getMessage().contains("Unable to access file")) { + logger.debug("Failed to clone VM {}. Retrying", cloneName); + try { + Thread.sleep(1000); + } catch (InterruptedException ie) { + logger.debug("Waiting to clone VM {} been interrupted: ", cloneName); + } + } else { + throw e; + } + } + } return false; } @@ -925,17 +947,7 @@ public class VirtualMachineMO extends BaseMO { cloneSpec.setLocation(rSpec); cloneSpec.setSnapshot(morBaseSnapshot); - ManagedObjectReference morTask = _context.getService().cloneVMTask(_mor, morFolder, cloneName, cloneSpec); - - boolean result = _context.getVimClient().waitForTask(morTask); - if (result) { - _context.waitForTaskProgressDone(morTask); - return true; - } else { - logger.error("VMware cloneVM_Task failed due to " + TaskMO.getTaskFailureInfo(_context, morTask)); - } - - return false; + return cloneVM(cloneName, morFolder, cloneSpec); } public VirtualMachineRuntimeInfo getRuntimeInfo() throws Exception { From 2d4b7ba3578a705b3a1bbc08d2a38d334b03816f Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Tue, 6 Jan 2026 12:08:18 +0530 Subject: [PATCH 16/51] Add mountopts to backup repository response (#12360) --- .../backup/repository/AddBackupRepositoryCmd.java | 3 ++- .../api/response/BackupRepositoryResponse.java | 12 ++++++++++++ .../cloudstack/backup/BackupRepositoryService.java | 1 - .../main/java/com/cloud/api/ApiResponseHelper.java | 3 +++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/AddBackupRepositoryCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/AddBackupRepositoryCmd.java index 64998a74954..7caa4ce710f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/AddBackupRepositoryCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/repository/AddBackupRepositoryCmd.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.user.backup.repository; +import com.cloud.utils.StringUtils; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; @@ -100,7 +101,7 @@ public class AddBackupRepositoryCmd extends BaseCmd { } public String getMountOptions() { - return mountOptions == null ? "" : mountOptions; + return StringUtils.isBlank(mountOptions) ? "" : mountOptions; } public Long getZoneId() { diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupRepositoryResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupRepositoryResponse.java index 327bbae0051..0d3c830950b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/BackupRepositoryResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupRepositoryResponse.java @@ -57,6 +57,10 @@ public class BackupRepositoryResponse extends BaseResponse { @Param(description = "backup type") private String type; + @SerializedName(ApiConstants.MOUNT_OPTIONS) + @Param(description = "mount options", since = "4.22.1") + private String mountOptions; + @SerializedName(ApiConstants.CAPACITY_BYTES) @Param(description = "capacity of the backup repository") private Long capacityBytes; @@ -128,6 +132,14 @@ public class BackupRepositoryResponse extends BaseResponse { this.type = type; } + public String getMountOptions() { + return mountOptions; + } + + public void setMountOptions(String mountOptions) { + this.mountOptions = mountOptions; + } + public Long getCapacityBytes() { return capacityBytes; } diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupRepositoryService.java b/api/src/main/java/org/apache/cloudstack/backup/BackupRepositoryService.java index 875fc3b3d90..cc8144ebe40 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupRepositoryService.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupRepositoryService.java @@ -32,5 +32,4 @@ public interface BackupRepositoryService { BackupRepository updateBackupRepository(UpdateBackupRepositoryCmd cmd); boolean deleteBackupRepository(DeleteBackupRepositoryCmd cmd); Pair, Integer> listBackupRepositories(ListBackupRepositoriesCmd cmd); - } diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 83b6e4d2bf1..f8e6753fb78 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -5526,6 +5526,9 @@ public class ApiResponseHelper implements ResponseGenerator { response.setAddress(backupRepository.getAddress()); response.setProviderName(backupRepository.getProvider()); response.setType(backupRepository.getType()); + if (StringUtils.isNotBlank(backupRepository.getMountOptions())) { + response.setMountOptions(backupRepository.getMountOptions()); + } response.setCapacityBytes(backupRepository.getCapacityBytes()); response.setCrossZoneInstanceCreation(backupRepository.crossZoneInstanceCreationEnabled()); response.setObjectName("backuprepository"); From c465caf81e743b1a0d6f919e472e2d82d1c71a66 Mon Sep 17 00:00:00 2001 From: dahn Date: Tue, 6 Jan 2026 08:17:37 +0100 Subject: [PATCH 17/51] Adjust close periods (#12376) --- .github/workflows/stale.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f12fbe93de6..e90c75979b6 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -33,9 +33,11 @@ jobs: stale-issue-message: 'This issue is stale because it has been open for 120 days with no activity. It may be removed by administrators of this project at any time. Remove the stale label or comment to request for removal of it to prevent this.' stale-pr-message: 'This PR is stale because it has been open for 120 days with no activity. It may be removed by administrators of this project at any time. Remove the stale label or comment to request for removal of it to prevent this.' close-issue-message: 'This issue was closed because it has been stale for 120 days with no activity.' - close-pr-message: 'This PR was closed because it has been stale for 120 days with no activity.' + close-pr-message: 'This PR was closed because it has been stale for 240 days with no activity.' stale-issue-label: 'no-issue-activity' stale-pr-label: 'no-pr-activity' days-before-stale: 120 + days-before-close: -1 + days-before-pr-close: 240 exempt-issue-labels: 'gsoc,good-first-issue,long-term-plan' exempt-pr-labels: 'status:ready-for-merge,status:needs-testing,status:on-hold' From 57331aca2fc4b0c7051a94a52b18969ac6920fd4 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Wed, 7 Jan 2026 09:25:11 +0530 Subject: [PATCH 18/51] Skip removal of offerings if in use during domain removal (#11780) This PR fixes #11502 - Prevent service offering update to specific domains if any instance for the offering are outside of those - Removal of offerings is skipped if it is in use by any Instance. --- .../java/com/cloud/storage/dao/VolumeDao.java | 2 + .../com/cloud/storage/dao/VolumeDaoImpl.java | 14 +++++++ .../java/com/cloud/vm/dao/VMInstanceDao.java | 3 ++ .../com/cloud/vm/dao/VMInstanceDaoImpl.java | 37 +++++++++++++++++++ .../ConfigurationManagerImpl.java | 8 +++- .../com/cloud/user/DomainManagerImpl.java | 25 +++++++++++-- 6 files changed, 84 insertions(+), 5 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java index e6ffca06f9e..4936af3caab 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java @@ -162,4 +162,6 @@ public interface VolumeDao extends GenericDao, StateDao searchRemovedByVms(List vmIds, Long batchSize); VolumeVO findOneByIScsiName(String iScsiName); + + int getVolumeCountByOfferingId(long diskOfferingId); } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java index 750dbf2bee0..5ef64b04664 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java @@ -77,6 +77,7 @@ public class VolumeDaoImpl extends GenericDaoBase implements Vol protected GenericSearchBuilder primaryStorageSearch2; protected GenericSearchBuilder secondaryStorageSearch; private final SearchBuilder poolAndPathSearch; + final GenericSearchBuilder CountByOfferingId; @Inject ReservationDao reservationDao; @@ -504,6 +505,11 @@ public class VolumeDaoImpl extends GenericDaoBase implements Vol poolAndPathSearch.and("poolId", poolAndPathSearch.entity().getPoolId(), Op.EQ); poolAndPathSearch.and("path", poolAndPathSearch.entity().getPath(), Op.EQ); poolAndPathSearch.done(); + + CountByOfferingId = createSearchBuilder(Integer.class); + CountByOfferingId.select(null, Func.COUNT, CountByOfferingId.entity().getId()); + CountByOfferingId.and("diskOfferingId", CountByOfferingId.entity().getDiskOfferingId(), Op.EQ); + CountByOfferingId.done(); } @Override @@ -909,4 +915,12 @@ public class VolumeDaoImpl extends GenericDaoBase implements Vol sc.setParameters("iScsiName", iScsiName); return findOneIncludingRemovedBy(sc); } + + @Override + public int getVolumeCountByOfferingId(long diskOfferingId) { + SearchCriteria sc = CountByOfferingId.create(); + sc.setParameters("diskOfferingId", diskOfferingId); + List results = customSearch(sc, null); + return results.get(0); + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java index 823642d8c3d..56e16ddd871 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java @@ -187,4 +187,7 @@ public interface VMInstanceDao extends GenericDao, StateDao< Map getNameIdMapForVmIds(Collection ids); + int getVmCountByOfferingId(Long serviceOfferingId); + + int getVmCountByOfferingNotInDomain(Long serviceOfferingId, List domainIds); } 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 29ab74dfbfd..518bc3cf497 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 @@ -104,6 +104,8 @@ public class VMInstanceDaoImpl extends GenericDaoBase implem protected SearchBuilder LastHostAndStatesSearch; protected SearchBuilder VmsNotInClusterUsingPool; protected SearchBuilder IdsPowerStateSelectSearch; + GenericSearchBuilder CountByOfferingId; + GenericSearchBuilder CountUserVmNotInDomain; @Inject ResourceTagDao tagsDao; @@ -344,6 +346,18 @@ public class VMInstanceDaoImpl extends GenericDaoBase implem IdsPowerStateSelectSearch.entity().getPowerStateUpdateCount(), IdsPowerStateSelectSearch.entity().getPowerStateUpdateTime()); IdsPowerStateSelectSearch.done(); + + CountByOfferingId = createSearchBuilder(Integer.class); + CountByOfferingId.select(null, Func.COUNT, CountByOfferingId.entity().getId()); + CountByOfferingId.and("serviceOfferingId", CountByOfferingId.entity().getServiceOfferingId(), Op.EQ); + CountByOfferingId.done(); + + CountUserVmNotInDomain = createSearchBuilder(Integer.class); + CountUserVmNotInDomain.select(null, Func.COUNT, CountUserVmNotInDomain.entity().getId()); + CountUserVmNotInDomain.and("serviceOfferingId", CountUserVmNotInDomain.entity().getServiceOfferingId(), Op.EQ); + CountUserVmNotInDomain.and("domainIdsNotIn", CountUserVmNotInDomain.entity().getDomainId(), Op.NIN); + CountUserVmNotInDomain.done(); + } @Override @@ -1224,4 +1238,27 @@ public class VMInstanceDaoImpl extends GenericDaoBase implem return vms.stream() .collect(Collectors.toMap(VMInstanceVO::getInstanceName, VMInstanceVO::getId)); } + + @Override + public int getVmCountByOfferingId(Long serviceOfferingId) { + if (serviceOfferingId == null) { + return 0; + } + SearchCriteria sc = CountByOfferingId.create(); + sc.setParameters("serviceOfferingId", serviceOfferingId); + List count = customSearch(sc, null); + return count.get(0); + } + + @Override + public int getVmCountByOfferingNotInDomain(Long serviceOfferingId, List domainIds) { + if (serviceOfferingId == null || CollectionUtils.isEmpty(domainIds)) { + return 0; + } + SearchCriteria sc = CountUserVmNotInDomain.create(); + sc.setParameters("serviceOfferingId", serviceOfferingId); + sc.setParameters("domainIdsNotIn", domainIds.toArray()); + List count = customSearch(sc, null); + return count.get(0); + } } diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 246681f7585..62b3c23d27e 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -50,7 +50,7 @@ import java.util.stream.Collectors; import javax.inject.Inject; import javax.naming.ConfigurationException; - +import com.cloud.exception.UnsupportedServiceException; import com.cloud.network.as.AutoScaleManager; import com.cloud.user.AccountManagerImpl; import org.apache.cloudstack.acl.RoleType; @@ -3722,6 +3722,12 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati List filteredDomainIds = filterChildSubDomains(domainIds); Collections.sort(filteredDomainIds); + // avoid domain update of service offering if any instance is associated to it + int instanceCount = _vmInstanceDao.getVmCountByOfferingNotInDomain(offeringHandle.getId(), filteredDomainIds); + if (instanceCount > 0) { + throw new UnsupportedServiceException("There are Instances associated to this service offering outside of the specified domains."); + } + List filteredZoneIds = new ArrayList<>(); if (CollectionUtils.isNotEmpty(zoneIds)) { filteredZoneIds.addAll(zoneIds); diff --git a/server/src/main/java/com/cloud/user/DomainManagerImpl.java b/server/src/main/java/com/cloud/user/DomainManagerImpl.java index 6fc9c6f5ef5..28f9bd3ab39 100644 --- a/server/src/main/java/com/cloud/user/DomainManagerImpl.java +++ b/server/src/main/java/com/cloud/user/DomainManagerImpl.java @@ -34,6 +34,8 @@ import com.cloud.api.query.vo.NetworkOfferingJoinVO; import com.cloud.api.query.vo.VpcOfferingJoinVO; import com.cloud.configuration.Resource; import com.cloud.domain.dao.DomainDetailsDao; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.vpc.dao.VpcDao; import com.cloud.network.vpc.dao.VpcOfferingDao; import com.cloud.network.vpc.dao.VpcOfferingDetailsDao; import com.cloud.offerings.dao.NetworkOfferingDao; @@ -85,6 +87,7 @@ import com.cloud.projects.dao.ProjectDao; import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.service.dao.ServiceOfferingDetailsDao; import com.cloud.storage.dao.DiskOfferingDao; +import com.cloud.storage.dao.VolumeDao; import com.cloud.user.dao.AccountDao; import com.cloud.utils.Pair; import com.cloud.utils.component.ManagerBase; @@ -101,6 +104,8 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils; import com.cloud.vm.ReservationContext; import com.cloud.vm.ReservationContextImpl; +import com.cloud.vm.dao.VMInstanceDao; + import org.apache.commons.lang3.StringUtils; @Component @@ -141,6 +146,14 @@ public class DomainManagerImpl extends ManagerBase implements DomainManager, Dom @Inject private ProjectDao _projectDao; @Inject + private VMInstanceDao vmInstanceDao; + @Inject + private NetworkDao networkDao; + @Inject + private VolumeDao volumeDao; + @Inject + private VpcDao vpcDao; + @Inject private ProjectManager _projectMgr; @Inject private RegionManager _regionMgr; @@ -543,7 +556,8 @@ public class DomainManagerImpl extends ManagerBase implements DomainManager, Dom List vpcOfferingsDetailsToRemove = new ArrayList<>(); List vpcOfferingsForThisDomain = vpcOfferingJoinDao.findByDomainId(domainId); for (VpcOfferingJoinVO vpcOffering : vpcOfferingsForThisDomain) { - if (domainIdString.equals(vpcOffering.getDomainId())) { + int vpcCount = vpcDao.getVpcCountByOfferingId(vpcOffering.getId()); + if (vpcCount == 0) { vpcOfferingDao.remove(vpcOffering.getId()); } else { vpcOfferingsDetailsToRemove.add(vpcOffering.getId()); @@ -558,7 +572,8 @@ public class DomainManagerImpl extends ManagerBase implements DomainManager, Dom List networkOfferingsDetailsToRemove = new ArrayList<>(); List networkOfferingsForThisDomain = networkOfferingJoinDao.findByDomainId(domainId, false); for (NetworkOfferingJoinVO networkOffering : networkOfferingsForThisDomain) { - if (domainIdString.equals(networkOffering.getDomainId())) { + int networkCount = networkDao.getNetworkCountByNetworkOffId(networkOffering.getId()); + if (networkCount == 0) { networkOfferingDao.remove(networkOffering.getId()); } else { networkOfferingsDetailsToRemove.add(networkOffering.getId()); @@ -573,7 +588,8 @@ public class DomainManagerImpl extends ManagerBase implements DomainManager, Dom List serviceOfferingsDetailsToRemove = new ArrayList<>(); List serviceOfferingsForThisDomain = serviceOfferingJoinDao.findByDomainId(domainId); for (ServiceOfferingJoinVO serviceOffering : serviceOfferingsForThisDomain) { - if (domainIdString.equals(serviceOffering.getDomainId())) { + int vmCount = vmInstanceDao.getVmCountByOfferingId(serviceOffering.getId()); + if (vmCount == 0) { serviceOfferingDao.remove(serviceOffering.getId()); } else { serviceOfferingsDetailsToRemove.add(serviceOffering.getId()); @@ -588,7 +604,8 @@ public class DomainManagerImpl extends ManagerBase implements DomainManager, Dom List diskOfferingsDetailsToRemove = new ArrayList<>(); List diskOfferingsForThisDomain = diskOfferingJoinDao.findByDomainId(domainId); for (DiskOfferingJoinVO diskOffering : diskOfferingsForThisDomain) { - if (domainIdString.equals(diskOffering.getDomainId())) { + int volumeCount = volumeDao.getVolumeCountByOfferingId(diskOffering.getId()); + if (volumeCount == 0) { diskOfferingDao.remove(diskOffering.getId()); } else { diskOfferingsDetailsToRemove.add(diskOffering.getId()); From 750290b8aede34c1da4012307ca8bc9ebfdb1343 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Wed, 7 Jan 2026 01:09:15 -0500 Subject: [PATCH 19/51] Prevent NPE when removing NIC from a stopped VM using service offering with CPU cap set (#12232) This PR fixes: #12225 --------- Co-authored-by: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> --- server/src/main/java/com/cloud/hypervisor/KVMGuru.java | 3 ++- server/src/test/java/com/cloud/hypervisor/KVMGuruTest.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/cloud/hypervisor/KVMGuru.java b/server/src/main/java/com/cloud/hypervisor/KVMGuru.java index 9edaa5e6d64..62063714b89 100644 --- a/server/src/main/java/com/cloud/hypervisor/KVMGuru.java +++ b/server/src/main/java/com/cloud/hypervisor/KVMGuru.java @@ -132,7 +132,8 @@ public class KVMGuru extends HypervisorGuruBase implements HypervisorGuru { VirtualMachine vm = vmProfile.getVirtualMachine(); HostVO host = hostDao.findById(vm.getHostId()); if (host == null) { - throw new CloudRuntimeException("Host with id: " + vm.getHostId() + " not found"); + logger.warn("Host is not available. Skipping setting CPU quota percentage for VM: {}", vm); + return; } logger.debug("Limiting CPU usage for VM: {} on host: {}", vm, host); double hostMaxSpeed = getHostCPUSpeed(host); diff --git a/server/src/test/java/com/cloud/hypervisor/KVMGuruTest.java b/server/src/test/java/com/cloud/hypervisor/KVMGuruTest.java index eea8bb9de68..07e19d99f39 100644 --- a/server/src/test/java/com/cloud/hypervisor/KVMGuruTest.java +++ b/server/src/test/java/com/cloud/hypervisor/KVMGuruTest.java @@ -32,7 +32,6 @@ import com.cloud.storage.GuestOSVO; import com.cloud.storage.dao.GuestOSDao; import com.cloud.storage.dao.GuestOSHypervisorDao; import com.cloud.utils.Pair; -import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineProfile; import org.apache.cloudstack.api.ApiConstants; @@ -141,10 +140,11 @@ public class KVMGuruTest { Mockito.verify(vmTO).setCpuQuotaPercentage(Mockito.anyDouble()); } - @Test(expected = CloudRuntimeException.class) + @Test public void testSetVmQuotaPercentageNullHost() { Mockito.when(hostDao.findById(hostId)).thenReturn(null); guru.setVmQuotaPercentage(vmTO, vmProfile); + Mockito.verify(vmTO, Mockito.never()).setCpuQuotaPercentage(Mockito.anyDouble()); } @Test From e47d7bc6ff12167f97932c922475ce8cb6593abe Mon Sep 17 00:00:00 2001 From: John Bampton Date: Thu, 8 Jan 2026 02:22:52 +1000 Subject: [PATCH 20/51] [CI] Dependabot: add a cooldown period for new releases (#12384) --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 88985cbdef1..41b307863fc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -26,3 +26,5 @@ updates: directory: "/" # Location of package manifests schedule: interval: "daily" + cooldown: + default-days: 7 From fd1c67f47390d2d4ff8e121d83de7284e0211c1b Mon Sep 17 00:00:00 2001 From: John Bampton Date: Thu, 8 Jan 2026 20:26:40 +1000 Subject: [PATCH 21/51] Standardize and auto add license headers to properties files (#12231) --- .pre-commit-config.yaml | 10 +++++++ .../hypervisors/ovm3/sonar-project.properties | 28 +++++++++---------- .../ovm3/src/test/resources/log4j.properties | 28 +++++++++---------- .../src/test/resources/log4j.properties | 26 +++++++++-------- systemvm/agent/conf/environment.properties | 17 +++++++++++ 5 files changed, 69 insertions(+), 40 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ef0b0c204eb..26adafcbf26 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -62,6 +62,16 @@ repos: - .github/workflows/license-templates/LICENSE.txt - --fuzzy-match-generates-todo exclude: ^(CHANGES|ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE)\.md$|^ui/docs/(full|smoke)-test-plan\.template\.md$ + - id: insert-license + name: add license for all properties files + description: automatically adds a licence header to all properties files that don't have a license header + files: \.properties$ + args: + - --comment-style + - '|#|' + - --license-filepath + - .github/workflows/license-templates/LICENSE.txt + - --fuzzy-match-generates-todo - id: insert-license name: add license for all Shell files description: automatically adds a licence header to all Shell files that don't have a license header diff --git a/plugins/hypervisors/ovm3/sonar-project.properties b/plugins/hypervisors/ovm3/sonar-project.properties index d632dfb9f91..7355f1df4f7 100644 --- a/plugins/hypervisors/ovm3/sonar-project.properties +++ b/plugins/hypervisors/ovm3/sonar-project.properties @@ -1,19 +1,19 @@ -#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 +# 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 +# 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. +# 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. # Required metadata sonar.projectKey=cloud-plugin-hypervisor-ovm3 diff --git a/plugins/hypervisors/ovm3/src/test/resources/log4j.properties b/plugins/hypervisors/ovm3/src/test/resources/log4j.properties index 82ee5c55c4c..0f72e39d855 100644 --- a/plugins/hypervisors/ovm3/src/test/resources/log4j.properties +++ b/plugins/hypervisors/ovm3/src/test/resources/log4j.properties @@ -1,19 +1,19 @@ -#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 +# 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 +# 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. +# 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. # Root logger option log4j.rootLogger=DEBUG, stdout diff --git a/plugins/network-elements/globodns/src/test/resources/log4j.properties b/plugins/network-elements/globodns/src/test/resources/log4j.properties index 1bac606ff63..8b1d961f7b6 100644 --- a/plugins/network-elements/globodns/src/test/resources/log4j.properties +++ b/plugins/network-elements/globodns/src/test/resources/log4j.properties @@ -1,17 +1,19 @@ -# 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 +# 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 +# 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. +# 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. # Define the root logger with appender file #log = /var/log/log4j diff --git a/systemvm/agent/conf/environment.properties b/systemvm/agent/conf/environment.properties index 269acad9152..20ca1a2b432 100644 --- a/systemvm/agent/conf/environment.properties +++ b/systemvm/agent/conf/environment.properties @@ -1,2 +1,19 @@ +# 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. + paths.script=../../scripts/storage/secondary/ paths.pid=. From bc76f2042d74ecee12a4cbea95e70b4bd75aae85 Mon Sep 17 00:00:00 2001 From: Tonitzpp <134986282+Tonitzpp@users.noreply.github.com> Date: Thu, 8 Jan 2026 09:55:34 -0300 Subject: [PATCH 22/51] Change migration volume exception messages (#12367) Co-authored-by: toni.zamparetti Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../main/java/com/cloud/storage/VolumeApiServiceImpl.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 4f03e788173..bdf9bc1bc1d 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -2179,14 +2179,16 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic } Collections.shuffle(suitableStoragePoolsWithEnoughSpace); MigrateVolumeCmd migrateVolumeCmd = new MigrateVolumeCmd(volume.getId(), suitableStoragePoolsWithEnoughSpace.get(0).getId(), newDiskOffering.getId(), true); + String volumeUuid = volume.getUuid(); try { Volume result = migrateVolume(migrateVolumeCmd); volume = (result != null) ? _volsDao.findById(result.getId()) : null; if (volume == null) { - throw new CloudRuntimeException(String.format("Volume change offering operation failed for volume: %s migration failed to storage pool %s", volume, suitableStoragePools.get(0))); + throw new CloudRuntimeException("Change offering for the volume failed."); } } catch (Exception e) { - throw new CloudRuntimeException(String.format("Volume change offering operation failed for volume: %s migration failed to storage pool %s due to %s", volume, suitableStoragePools.get(0), e.getMessage())); + logger.error("Volume change offering operation failed for volume ID: {} migration failed to storage pool {} due to {}", volumeUuid, suitableStoragePoolsWithEnoughSpace.get(0).getId(), e.getMessage()); + throw new CloudRuntimeException("Change offering for the volume failed.", e); } } @@ -2199,7 +2201,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic if (volumeMigrateRequired) { logger.warn(String.format("Volume change offering operation succeeded for volume ID: %s but volume resize operation failed, so please try resize volume operation separately", volume.getUuid())); } else { - throw new CloudRuntimeException(String.format("Volume change offering operation failed for volume ID: %s due to resize volume operation failed", volume.getUuid())); + throw new CloudRuntimeException(String.format("Volume disk offering change operation failed for volume ID [%s] because the volume resize operation failed.", volume.getUuid())); } } } From bc3d7c314bb61af7dfb822a644c3032ff8d5c0aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernardo=20De=20Marco=20Gon=C3=A7alves?= Date: Fri, 9 Jan 2026 05:17:44 -0300 Subject: [PATCH 23/51] Change the `value` parameter of the `updateConfiguration` API to be required (#10790) --- .../com/cloud/configuration/ConfigurationManagerImpl.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 705e7dea158..e6abc21e7da 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -1031,6 +1031,10 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati category = config.getCategory(); } + if (value == null) { + throw new InvalidParameterValueException(String.format("The new value for the [%s] configuration must be given.", name)); + } + validateIpAddressRelatedConfigValues(name, value); validateConflictingConfigValue(name, value); @@ -1039,10 +1043,6 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati throw new CloudRuntimeException("Only Root Admin is allowed to edit this configuration."); } - if (value == null) { - return _configDao.findByName(name); - } - ConfigKey.Scope scope = null; Long id = null; int paramCountCheck = 0; From 1ef636577167cc7cc7a6ce63c54b54c1ab32aabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20B=C3=B6ck?= <89930804+erikbocks@users.noreply.github.com> Date: Fri, 9 Jan 2026 05:23:46 -0300 Subject: [PATCH 24/51] Change internal ID to UUID in user disable event (#11824) --- .../cloudstack/api/command/admin/user/DisableUserCmd.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DisableUserCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DisableUserCmd.java index 974c1c7bebe..6ce669d8523 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DisableUserCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DisableUserCmd.java @@ -78,12 +78,12 @@ public class DisableUserCmd extends BaseAsyncCmd { @Override public String getEventDescription() { - return "disabling user: " + getId(); + return "disabling user: " + this._uuidMgr.getUuid(User.class, getId()); } @Override public void execute() { - CallContext.current().setEventDetails("UserId: " + getId()); + CallContext.current().setEventDetails("User ID: " + this._uuidMgr.getUuid(User.class, getId())); UserAccount user = _regionService.disableUser(this); if (user != null) { From 1b861dad48fe4987b691eab7ed102638d5c1f550 Mon Sep 17 00:00:00 2001 From: "Suyang(Dawson) Chen" Date: Fri, 9 Jan 2026 03:30:17 -0500 Subject: [PATCH 25/51] Cleanup: Standardize logger message formatting in ApiServer.java (#11188) --- .../src/main/java/com/cloud/api/ApiServer.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java index 5a3c8c2c717..95aca28b53f 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -461,14 +461,14 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer final Long snapshotLimit = ConcurrentSnapshotsThresholdPerHost.value(); if (snapshotLimit == null || snapshotLimit <= 0) { - logger.debug("Global concurrent snapshot config parameter " + ConcurrentSnapshotsThresholdPerHost.value() + " is less or equal 0; defaulting to unlimited"); + logger.debug("Global concurrent snapshot config parameter {} is less or equal 0; defaulting to unlimited", ConcurrentSnapshotsThresholdPerHost.value()); } else { dispatcher.setCreateSnapshotQueueSizeLimit(snapshotLimit); } final Long migrationLimit = VolumeApiService.ConcurrentMigrationsThresholdPerDatastore.value(); if (migrationLimit == null || migrationLimit <= 0) { - logger.debug("Global concurrent migration config parameter " + VolumeApiService.ConcurrentMigrationsThresholdPerDatastore.value() + " is less or equal 0; defaulting to unlimited"); + logger.debug("Global concurrent migration config parameter {} is less or equal 0; defaulting to unlimited", VolumeApiService.ConcurrentMigrationsThresholdPerDatastore.value()); } else { dispatcher.setMigrateQueueSizeLimit(migrationLimit); } @@ -647,7 +647,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer logValue = (value == null) ? "'null'" : value[0]; } - logger.trace(" key: " + keyStr + ", value: " + logValue); + logger.trace(" key: {}, value: {}", keyStr, logValue); } } throw new ServerApiException(ApiErrorCode.UNSUPPORTED_ACTION_ERROR, "Invalid request, no command sent"); @@ -707,7 +707,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer buf.append(obj.getUuid()); buf.append(" "); } - logger.info("PermissionDenied: " + ex.getMessage() + " on objs: [" + buf + "]"); + logger.info("PermissionDenied: {} on objs: [{}]", ex.getMessage(), buf); } else { logger.info("PermissionDenied: {}", ex.getMessage()); } @@ -1035,7 +1035,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer // if api/secret key are passed to the parameters if ((signature == null) || (apiKey == null)) { - logger.debug("Expired session, missing signature, or missing apiKey -- ignoring request. Signature: " + signature + ", apiKey: " + apiKey); + logger.warn("Expired session, missing signature, or missing apiKey -- ignoring request. Signature: {}, apiKey: {}", signature, apiKey); return false; // no signature, bad request } @@ -1258,7 +1258,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer float offsetInHrs = 0f; if (timezone != null) { final TimeZone t = TimeZone.getTimeZone(timezone); - logger.info("Current user logged in under " + timezone + " timezone"); + logger.info("Current user logged in under {} timezone", timezone); final java.util.Date date = new java.util.Date(); final long longDate = date.getTime(); @@ -1410,9 +1410,9 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer final Boolean apiSourceCidrChecksEnabled = ApiServiceConfiguration.ApiSourceCidrChecksEnabled.value(); if (apiSourceCidrChecksEnabled) { - logger.debug("CIDRs from which account '" + account.toString() + "' is allowed to perform API calls: " + accessAllowedCidrs); + logger.debug("CIDRs from which account '{}' is allowed to perform API calls: {}", account.toString(), accessAllowedCidrs); if (!NetUtils.isIpInCidrList(remoteAddress, accessAllowedCidrs.split(","))) { - logger.warn("Request by account '" + account.toString() + "' was denied since " + remoteAddress + " does not match " + accessAllowedCidrs); + logger.warn("Request by account '{}' was denied since {} does not match {}", account.toString(), remoteAddress, accessAllowedCidrs); throw new OriginDeniedException("Calls from disallowed origin", account, remoteAddress); } } From 9a38e75abdffdc22568c57f4abcb7519b7933ea9 Mon Sep 17 00:00:00 2001 From: Nicolas Vazquez Date: Fri, 9 Jan 2026 05:48:04 -0300 Subject: [PATCH 26/51] Fix Linstor shrink qcow2 volumes (#12387) --- .../src/main/java/com/cloud/storage/VolumeApiServiceImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 0faf9f1e2c0..4f8b55d16fb 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -2420,7 +2420,8 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic } } - if (volume != null && ImageFormat.QCOW2.equals(volume.getFormat()) && !Volume.State.Allocated.equals(volume.getState()) && !StoragePoolType.StorPool.equals(volume.getPoolType())) { + if (volume != null && ImageFormat.QCOW2.equals(volume.getFormat()) && !Volume.State.Allocated.equals(volume.getState()) && + !Arrays.asList(StoragePoolType.StorPool, StoragePoolType.Linstor).contains(volume.getPoolType())) { String message = "Unable to shrink volumes of type QCOW2"; logger.warn(message); throw new InvalidParameterValueException(message); From c91e84c6d8b1b440d8c6af49f7b8f728c406e701 Mon Sep 17 00:00:00 2001 From: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> Date: Fri, 9 Jan 2026 18:00:24 +0530 Subject: [PATCH 27/51] Avoid double counting primary storage allocated capacity for storage pools having a parent (#12181) --- .../java/com/cloud/capacity/dao/CapacityDaoImpl.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/engine/schema/src/main/java/com/cloud/capacity/dao/CapacityDaoImpl.java b/engine/schema/src/main/java/com/cloud/capacity/dao/CapacityDaoImpl.java index 5e7eee4566c..7bd3df103b1 100644 --- a/engine/schema/src/main/java/com/cloud/capacity/dao/CapacityDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/capacity/dao/CapacityDaoImpl.java @@ -213,6 +213,8 @@ public class CapacityDaoImpl extends GenericDaoBase implements private static final String LEFT_JOIN_VM_TEMPLATE = "LEFT JOIN vm_template ON vm_template.id = vi.vm_template_id "; + private static final String STORAGE_POOLS_WITH_CHILDREN = "SELECT DISTINCT parent FROM storage_pool WHERE parent != 0 AND removed IS NULL"; + public CapacityDaoImpl() { _hostIdTypeSearch = createSearchBuilder(); _hostIdTypeSearch.and("hostId", _hostIdTypeSearch.entity().getHostOrPoolId(), SearchCriteria.Op.EQ); @@ -379,6 +381,11 @@ public class CapacityDaoImpl extends GenericDaoBase implements finalQuery.append(" AND capacity_type = ?"); resourceIdList.add(capacityType.longValue()); } + + // Exclude storage pools with children from capacity calculations to avoid double counting + finalQuery.append(" AND NOT (capacity.capacity_type = ").append(Capacity.CAPACITY_TYPE_STORAGE_ALLOCATED) + .append(" AND capacity.host_id IN (").append(STORAGE_POOLS_WITH_CHILDREN).append("))"); + if (CollectionUtils.isNotEmpty(hostIds)) { finalQuery.append(String.format(" AND capacity.host_id IN (%s)", StringUtils.join(hostIds, ","))); if (capacityType == null) { @@ -541,6 +548,10 @@ public class CapacityDaoImpl extends GenericDaoBase implements StringBuilder sql = new StringBuilder(LIST_CAPACITY_GROUP_BY_CAPACITY_PART1); List resourceIdList = new ArrayList(); + // Exclude storage pools with children from capacity calculations to avoid double counting + sql.append(" AND NOT (capacity.capacity_type = ").append(Capacity.CAPACITY_TYPE_STORAGE_ALLOCATED) + .append(" AND capacity.host_id IN (").append(STORAGE_POOLS_WITH_CHILDREN).append("))"); + if (zoneId != null) { sql.append(" AND capacity.data_center_id = ?"); resourceIdList.add(zoneId); From ef1aaa0551d3d372e3ac7295b7f87dc203212a80 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 9 Jan 2026 18:26:39 +0530 Subject: [PATCH 28/51] kvm: allow skip forcing disk controller (#11750) --- .../java/com/cloud/vm/VmDetailConstants.java | 3 + .../resource/LibvirtComputingResource.java | 52 +++++++++++---- .../LibvirtComputingResourceTest.java | 65 +++++++++++++++++-- .../com/cloud/api/query/QueryManagerImpl.java | 1 + 4 files changed, 106 insertions(+), 15 deletions(-) diff --git a/api/src/main/java/com/cloud/vm/VmDetailConstants.java b/api/src/main/java/com/cloud/vm/VmDetailConstants.java index a6c9b6eba16..3d0152b0e43 100644 --- a/api/src/main/java/com/cloud/vm/VmDetailConstants.java +++ b/api/src/main/java/com/cloud/vm/VmDetailConstants.java @@ -54,6 +54,9 @@ public interface VmDetailConstants { String NIC_MULTIQUEUE_NUMBER = "nic.multiqueue.number"; String NIC_PACKED_VIRTQUEUES_ENABLED = "nic.packed.virtqueues.enabled"; + // KVM specific, disk controllers + String KVM_SKIP_FORCE_DISK_CONTROLLER = "skip.force.disk.controller"; + // Mac OSX guest specific (internal) String SMC_PRESENT = "smc.present"; String FIRMWARE = "firmware"; diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index 87ea55ca766..b66a838a3a5 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -107,7 +107,6 @@ import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; - import com.cloud.agent.api.Answer; import com.cloud.agent.api.Command; import com.cloud.agent.api.HostVmStateReportEntry; @@ -188,8 +187,8 @@ import com.cloud.network.Networks.IsolationType; import com.cloud.network.Networks.RouterPrivateIpStrategy; import com.cloud.network.Networks.TrafficType; import com.cloud.resource.AgentStatusUpdater; -import com.cloud.resource.ResourceStatusUpdater; import com.cloud.resource.RequestWrapper; +import com.cloud.resource.ResourceStatusUpdater; import com.cloud.resource.ServerResource; import com.cloud.resource.ServerResourceBase; import com.cloud.storage.JavaStorageLayer; @@ -3083,6 +3082,44 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv return useBLOCKDiskType(physicalDisk) ? DiskDef.DiskType.BLOCK : DiskDef.DiskType.FILE; } + /** + * Defines the disk configuration for the default pool type based on the provided parameters. + * It determines the appropriate disk settings depending on whether the disk is a data disk, whether + * it's a Windows template, whether UEFI is enabled, and whether secure boot is active. + * + * @param disk The disk definition object that will be configured with the disk settings. + * @param volume The volume (disk) object, containing information about the type of disk. + * @param isWindowsTemplate Flag indicating whether the template is a Windows template. + * @param isUefiEnabled Flag indicating whether UEFI is enabled. + * @param isSecureBoot Flag indicating whether secure boot is enabled. + * @param physicalDisk The physical disk object that contains the path to the disk. + * @param devId The device ID for the disk. + * @param diskBusType The disk bus type to use if not skipping force disk controller. + * @param diskBusTypeData The disk bus type to use for data disks, if applicable. + * @param details A map of VM details containing additional configuration values, such as whether to skip force + * disk controller. + */ + protected void defineDiskForDefaultPoolType(DiskDef disk, DiskTO volume, boolean isWindowsTemplate, + boolean isUefiEnabled, boolean isSecureBoot, KVMPhysicalDisk physicalDisk, int devId, + DiskDef.DiskBus diskBusType, DiskDef.DiskBus diskBusTypeData, Map details) { + boolean skipForceDiskController = MapUtils.getBoolean(details, VmDetailConstants.KVM_SKIP_FORCE_DISK_CONTROLLER, + false); + if (skipForceDiskController) { + disk.defFileBasedDisk(physicalDisk.getPath(), devId, Volume.Type.DATADISK.equals(volume.getType()) ? + diskBusTypeData : diskBusType, DiskDef.DiskFmtType.QCOW2); + return; + } + if (volume.getType() == Volume.Type.DATADISK && !(isWindowsTemplate && isUefiEnabled)) { + disk.defFileBasedDisk(physicalDisk.getPath(), devId, diskBusTypeData, DiskDef.DiskFmtType.QCOW2); + } else { + if (isSecureBoot) { + disk.defFileBasedDisk(physicalDisk.getPath(), devId, DiskDef.DiskFmtType.QCOW2, isWindowsTemplate); + } else { + disk.defFileBasedDisk(physicalDisk.getPath(), devId, diskBusType, DiskDef.DiskFmtType.QCOW2); + } + } + } + public void createVbd(final Connect conn, final VirtualMachineTO vmSpec, final String vmName, final LibvirtVMDef vm) throws InternalErrorException, LibvirtException, URISyntaxException { final Map details = vmSpec.getDetails(); final List disks = Arrays.asList(vmSpec.getDisks()); @@ -3244,15 +3281,8 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv disk.setDiscard(DiscardType.UNMAP); } } else { - if (volume.getType() == Volume.Type.DATADISK && !(isWindowsTemplate && isUefiEnabled)) { - disk.defFileBasedDisk(physicalDisk.getPath(), devId, diskBusTypeData, DiskDef.DiskFmtType.QCOW2); - } else { - if (isSecureBoot) { - disk.defFileBasedDisk(physicalDisk.getPath(), devId, DiskDef.DiskFmtType.QCOW2, isWindowsTemplate); - } else { - disk.defFileBasedDisk(physicalDisk.getPath(), devId, diskBusType, DiskDef.DiskFmtType.QCOW2); - } - } + defineDiskForDefaultPoolType(disk, volume, isWindowsTemplate, isUefiEnabled, isSecureBoot, + physicalDisk, devId, diskBusType, diskBusTypeData, details); } pool.customizeLibvirtDiskDef(disk); } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java index 88e0983b63e..ed163787b11 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java @@ -56,9 +56,6 @@ import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; -import com.cloud.utils.net.NetUtils; - -import com.cloud.vm.VmDetailConstants; import org.apache.cloudstack.api.ApiConstants.IoDriverPolicy; import org.apache.cloudstack.storage.command.AttachAnswer; import org.apache.cloudstack.storage.command.AttachCommand; @@ -217,13 +214,15 @@ import com.cloud.storage.template.TemplateLocation; import com.cloud.template.VirtualMachineTemplate.BootloaderType; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.utils.script.Script; +import com.cloud.utils.net.NetUtils; import com.cloud.utils.script.OutputInterpreter.OneLineParser; +import com.cloud.utils.script.Script; import com.cloud.utils.ssh.SshHelper; import com.cloud.vm.DiskProfile; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.PowerState; import com.cloud.vm.VirtualMachine.Type; +import com.cloud.vm.VmDetailConstants; @RunWith(MockitoJUnitRunner.class) public class LibvirtComputingResourceTest { @@ -240,6 +239,19 @@ public class LibvirtComputingResourceTest { Connect connMock; @Mock LibvirtDomainXMLParser parserMock; + @Mock + private DiskDef diskDef; + @Mock + private DiskTO volume; + @Mock + private KVMPhysicalDisk physicalDisk; + @Mock + private Map details; + + private static final String PHYSICAL_DISK_PATH = "/path/to/disk"; + private static final int DEV_ID = 1; + private static final DiskDef.DiskBus DISK_BUS_TYPE = DiskDef.DiskBus.VIRTIO; + private static final DiskDef.DiskBus DISK_BUS_TYPE_DATA = DiskDef.DiskBus.SCSI; @Spy private LibvirtComputingResource libvirtComputingResourceSpy = Mockito.spy(new LibvirtComputingResource()); @@ -6565,4 +6577,49 @@ public class LibvirtComputingResourceTest { assertEquals(LibvirtVMDef.TpmDef.TpmModel.CRB, tpmDef.getModel()); assertEquals(LibvirtVMDef.TpmDef.TpmVersion.V2_0, tpmDef.getVersion()); } + + @Test + public void defineDiskForDefaultPoolTypeSkipsForceDiskController() { + Map details = new HashMap<>(); + details.put(VmDetailConstants.KVM_SKIP_FORCE_DISK_CONTROLLER, "true"); + Mockito.when(volume.getType()).thenReturn(Volume.Type.DATADISK); + Mockito.when(physicalDisk.getPath()).thenReturn(PHYSICAL_DISK_PATH); + libvirtComputingResourceSpy.defineDiskForDefaultPoolType(diskDef, volume, false, false, false, physicalDisk, DEV_ID, DISK_BUS_TYPE, DISK_BUS_TYPE_DATA, details); + Mockito.verify(diskDef).defFileBasedDisk(PHYSICAL_DISK_PATH, DEV_ID, DISK_BUS_TYPE_DATA, DiskDef.DiskFmtType.QCOW2); + } + + @Test + public void defineDiskForDefaultPoolTypeUsesDiskBusTypeDataForDataDiskWithoutWindowsAndUefi() { + Map details = new HashMap<>(); + Mockito.when(volume.getType()).thenReturn(Volume.Type.DATADISK); + Mockito.when(physicalDisk.getPath()).thenReturn(PHYSICAL_DISK_PATH); + libvirtComputingResourceSpy.defineDiskForDefaultPoolType(diskDef, volume, false, false, false, physicalDisk, DEV_ID, DISK_BUS_TYPE, DISK_BUS_TYPE_DATA, details); + Mockito.verify(diskDef).defFileBasedDisk(PHYSICAL_DISK_PATH, DEV_ID, DISK_BUS_TYPE_DATA, DiskDef.DiskFmtType.QCOW2); + } + + @Test + public void defineDiskForDefaultPoolTypeUsesDiskBusTypeForRootDisk() { + Map details = new HashMap<>(); + Mockito.when(volume.getType()).thenReturn(Volume.Type.ROOT); + Mockito.when(physicalDisk.getPath()).thenReturn(PHYSICAL_DISK_PATH); + libvirtComputingResourceSpy.defineDiskForDefaultPoolType(diskDef, volume, false, false, false, physicalDisk, DEV_ID, DISK_BUS_TYPE, DISK_BUS_TYPE_DATA, details); + Mockito.verify(diskDef).defFileBasedDisk(PHYSICAL_DISK_PATH, DEV_ID, DISK_BUS_TYPE, DiskDef.DiskFmtType.QCOW2); + } + + @Test + public void defineDiskForDefaultPoolTypeUsesSecureBootConfiguration() { + Map details = new HashMap<>(); + Mockito.when(volume.getType()).thenReturn(Volume.Type.ROOT); + Mockito.when(physicalDisk.getPath()).thenReturn(PHYSICAL_DISK_PATH); + libvirtComputingResourceSpy.defineDiskForDefaultPoolType(diskDef, volume, true, true, true, physicalDisk, DEV_ID, DISK_BUS_TYPE, DISK_BUS_TYPE_DATA, details); + Mockito.verify(diskDef).defFileBasedDisk(PHYSICAL_DISK_PATH, DEV_ID, DiskDef.DiskFmtType.QCOW2, true); + } + + @Test + public void defineDiskForDefaultPoolTypeHandlesNullDetails() { + Mockito.when(volume.getType()).thenReturn(Volume.Type.DATADISK); + Mockito.when(physicalDisk.getPath()).thenReturn(PHYSICAL_DISK_PATH); + libvirtComputingResourceSpy.defineDiskForDefaultPoolType(diskDef, volume, false, false, false, physicalDisk, DEV_ID, DISK_BUS_TYPE, DISK_BUS_TYPE_DATA, null); + Mockito.verify(diskDef).defFileBasedDisk(PHYSICAL_DISK_PATH, DEV_ID, DISK_BUS_TYPE_DATA, DiskDef.DiskFmtType.QCOW2); + } } 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 8641eb7ffc3..5833ede550e 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -5072,6 +5072,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q options.put(VmDetailConstants.VIRTUAL_TPM_VERSION, Arrays.asList("1.2", "2.0")); options.put(VmDetailConstants.GUEST_CPU_MODE, Arrays.asList("custom", "host-model", "host-passthrough")); options.put(VmDetailConstants.GUEST_CPU_MODEL, Collections.emptyList()); + options.put(VmDetailConstants.KVM_SKIP_FORCE_DISK_CONTROLLER, Arrays.asList("true", "false")); } if (HypervisorType.VMware.equals(hypervisorType)) { From 04875f151771b6b6fdf97d4a4f2690fbeda025d2 Mon Sep 17 00:00:00 2001 From: Nicolas Vazquez Date: Fri, 9 Jan 2026 13:50:27 -0300 Subject: [PATCH 29/51] Improve logs for VM migrations (#12332) --- .../cloud/vm/VirtualMachineManagerImpl.java | 29 ++++++++++++++----- .../wrapper/LibvirtMigrateCommandWrapper.java | 9 +++++- ...virtPrepareForMigrationCommandWrapper.java | 4 +++ .../java/com/cloud/vm/UserVmManagerImpl.java | 2 ++ 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index f9238fa0e71..86f45630611 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -3053,7 +3053,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } protected void migrate(final VMInstanceVO vm, final long srcHostId, final DeployDestination dest) throws ResourceUnavailableException, ConcurrentOperationException { - logger.info("Migrating {} to {}", vm, dest); + logger.info("Start preparing migration of the VM: {} to {}", vm, dest); final long dstHostId = dest.getHost().getId(); final Host fromHost = _hostDao.findById(srcHostId); if (fromHost == null) { @@ -3118,9 +3118,11 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac if (pfma == null || !pfma.getResult()) { final String details = pfma != null ? pfma.getDetails() : "null answer returned"; final String msg = "Unable to prepare for migration due to " + details; + logger.error("Failed to prepare destination host {} for migration of VM {} : {}", dstHostId, vm.getInstanceName(), details); pfma = null; throw new AgentUnavailableException(msg, dstHostId); } + logger.debug("Successfully prepared destination host {} for migration of VM {} ", dstHostId, vm.getInstanceName()); } catch (final OperationTimedoutException e1) { throw new AgentUnavailableException("Operation timed out", dstHostId); } finally { @@ -3141,18 +3143,23 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac volumeMgr.release(vm.getId(), dstHostId); } - logger.info("Migration cancelled because state has changed: {}", vm); - throw new ConcurrentOperationException("Migration cancelled because state has changed: " + vm); + String msg = "Migration cancelled because state has changed: " + vm; + logger.warn(msg); + throw new ConcurrentOperationException(msg); } } catch (final NoTransitionException e1) { _networkMgr.rollbackNicForMigration(vmSrc, profile); volumeMgr.release(vm.getId(), dstHostId); - logger.info("Migration cancelled because {}", e1.getMessage()); + String msg = String.format("Migration cancelled for VM %s due to state transition failure: %s", + vm.getInstanceName(), e1.getMessage()); + logger.warn(msg, e1); throw new ConcurrentOperationException("Migration cancelled because " + e1.getMessage()); } catch (final CloudRuntimeException e2) { _networkMgr.rollbackNicForMigration(vmSrc, profile); volumeMgr.release(vm.getId(), dstHostId); - logger.info("Migration cancelled because {}", e2.getMessage()); + String msg = String.format("Migration cancelled for VM %s due to runtime exception: %s", + vm.getInstanceName(), e2.getMessage()); + logger.error(msg, e2); work.setStep(Step.Done); _workDao.update(work.getId(), work); try { @@ -3172,8 +3179,12 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac final Answer ma = _agentMgr.send(vm.getLastHostId(), mc); if (ma == null || !ma.getResult()) { final String details = ma != null ? ma.getDetails() : "null answer returned"; + String msg = String.format("Migration command failed for VM %s on source host id=%s to destination host %s: %s", + vm.getInstanceName(), vm.getLastHostId(), dstHostId, details); + logger.error(msg); throw new CloudRuntimeException(details); } + logger.info("Migration command successful for VM {}", vm.getInstanceName()); } catch (final OperationTimedoutException e) { boolean success = false; if (HypervisorType.KVM.equals(vm.getHypervisorType())) { @@ -3210,7 +3221,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac try { if (!checkVmOnHost(vm, dstHostId)) { - logger.error("Unable to complete migration for {}", vm); + logger.error("Migration verification failed for VM {} : VM not found on destination host {} ", vm.getInstanceName(), dstHostId); try { _agentMgr.send(srcHostId, new Commands(cleanup(vm, dpdkInterfaceMapping)), null); } catch (final AgentUnavailableException e) { @@ -3225,7 +3236,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac migrated = true; } finally { if (!migrated) { - logger.info("Migration was unsuccessful. Cleaning up: {}", vm); + logger.info("Migration was unsuccessful. Cleaning up: {}", vm); _networkMgr.rollbackNicForMigration(vmSrc, profile); volumeMgr.release(vm.getId(), dstHostId); // deallocate GPU devices for the VM on the destination host @@ -3237,7 +3248,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac try { _agentMgr.send(dstHostId, new Commands(cleanup(vm, dpdkInterfaceMapping)), null); } catch (final AgentUnavailableException ae) { - logger.warn("Looks like the destination Host is unavailable for cleanup", ae); + logger.warn("Destination host {} unavailable for cleanup after failed migration of VM {}", dstHostId, vm.getInstanceName(), ae); } _networkMgr.setHypervisorHostname(profile, dest, false); try { @@ -3246,6 +3257,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac logger.warn(e.getMessage()); } } else { + logger.info("Migration completed successfully for VM %s" + vm); _networkMgr.commitNicForMigration(vmSrc, profile); volumeMgr.release(vm.getId(), srcHostId); // deallocate GPU devices for the VM on the src host after migration is complete @@ -3276,6 +3288,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac migrateCommand.setVlanToPersistenceMap(vlanToPersistenceMap); } + logger.debug("Setting auto convergence to: {}", StorageManager.KvmAutoConvergence.value()); migrateCommand.setAutoConvergence(StorageManager.KvmAutoConvergence.value()); migrateCommand.setHostGuid(destination.getHost().getGuid()); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java index 859de5143f9..81328d6ffb9 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java @@ -278,17 +278,20 @@ public final class LibvirtMigrateCommandWrapper extends CommandWrapper 0 && sleeptime > migrateWait * 1000) { DomainState state = null; try { state = dm.getInfo().state; + logger.info("VM domain state when trying to abort migration : {}", state); } catch (final LibvirtException e) { logger.info("Couldn't get VM domain state after " + sleeptime + "ms: " + e.getMessage()); } if (state != null && state == DomainState.VIR_DOMAIN_RUNNING) { try { DomainJobInfo job = dm.getJobInfo(); - logger.info(String.format("Aborting migration of VM [%s] with domain job [%s] due to time out after %d seconds.", vmName, job, migrateWait)); + logger.warn("Aborting migration of VM {} with domain job [{}] due to timeout after {} seconds. " + + "Job stats: data processed={} bytes, data remaining={} bytes", vmName, job, migrateWait, job.getDataProcessed(), job.getDataRemaining()); dm.abortJob(); result = String.format("Migration of VM [%s] was cancelled by CloudStack due to time out after %d seconds.", vmName, migrateWait); commandState = Command.State.FAILED; @@ -303,10 +306,12 @@ public final class LibvirtMigrateCommandWrapper extends CommandWrapper 0 && sleeptime > migratePauseAfter) { DomainState state = null; try { state = dm.getInfo().state; + logger.info("VM domain state when trying to pause VM for migration: {}", state); } catch (final LibvirtException e) { logger.info("Couldn't get VM domain state after " + sleeptime + "ms: " + e.getMessage()); } @@ -381,6 +386,7 @@ public final class LibvirtMigrateCommandWrapper extends CommandWrapper Date: Mon, 12 Jan 2026 12:11:45 +0530 Subject: [PATCH 30/51] [UI] Fix for the login url with nested redirect parameters (#12356) --- ui/src/utils/request.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/utils/request.js b/ui/src/utils/request.js index 42b26c9785b..2317aac0446 100644 --- a/ui/src/utils/request.js +++ b/ui/src/utils/request.js @@ -54,7 +54,7 @@ const err = (error) => { if (response.config && response.config.params && ['forgotPassword', 'listIdps', 'cloudianIsEnabled'].includes(response.config.params.command)) { return } - const originalPath = router.currentRoute.value.fullPath + const originalPath = router.currentRoute.value.path for (const key in response.data) { if (key.includes('response')) { if (response.data[key].errortext.includes('not available for user')) { From 2358632253a0a0da74b81028e6c63aeb49df7e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20B=C3=B6ck?= <89930804+erikbocks@users.noreply.github.com> Date: Mon, 12 Jan 2026 04:20:31 -0300 Subject: [PATCH 31/51] Fixed User type accounts being able to change resource limits of their own domain and account (#12046) Co-authored-by: Lucas Martins <56271185+lucas-a-martins@users.noreply.github.com> --- .../com/cloud/resourcelimit/ResourceLimitManagerImpl.java | 5 +++++ .../cloud/resourcelimit/ResourceLimitManagerImplTest.java | 1 + 2 files changed, 6 insertions(+) diff --git a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java index 9a6c8a85f18..648abf0d938 100644 --- a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java +++ b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java @@ -903,6 +903,11 @@ public class ResourceLimitManagerImpl extends ManagerBase implements ResourceLim public ResourceLimitVO updateResourceLimit(Long accountId, Long domainId, Integer typeId, Long max, String tag) { Account caller = CallContext.current().getCallingAccount(); + if (caller.getType().equals(Account.Type.NORMAL)) { + logger.info("Throwing exception because only root admins and domain admins are allowed to update resource limits."); + throw new PermissionDeniedException("Your account does not have the permission to update resource limits."); + } + if (max == null) { max = (long)Resource.RESOURCE_UNLIMITED; } else if (max < Resource.RESOURCE_UNLIMITED) { diff --git a/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java b/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java index a968a2da0b7..0b0b8c5e43f 100644 --- a/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java +++ b/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java @@ -147,6 +147,7 @@ public class ResourceLimitManagerImplTest { overrideDefaultConfigValue(ResourceLimitService.ResourceLimitStorageTags, "_defaultValue", StringUtils.join(storageTags, ",")); Account account = mock(Account.class); + when(account.getType()).thenReturn(Account.Type.ADMIN); User user = mock(User.class); CallContext.register(user, account); } From db1c7d678cc5505a926193e119991eb87d86cf33 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Mon, 12 Jan 2026 12:51:19 +0530 Subject: [PATCH 32/51] Updated protobuf version to 3.25.5, and protobuf & jackson maven dependencies (#12389) --- plugins/hypervisors/kvm/pom.xml | 25 +++++++++++++++++++++++++ pom.xml | 12 ++++++++++++ utils/pom.xml | 5 +++++ 3 files changed, 42 insertions(+) diff --git a/plugins/hypervisors/kvm/pom.xml b/plugins/hypervisors/kvm/pom.xml index 096c0362ee4..e2e1721b3a7 100644 --- a/plugins/hypervisors/kvm/pom.xml +++ b/plugins/hypervisors/kvm/pom.xml @@ -67,6 +67,31 @@ java-linstor ${cs.java-linstor.version} + + com.fasterxml.jackson.core + jackson-core + ${cs.jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${cs.jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + ${cs.jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${cs.jackson.version} + + + com.fasterxml.jackson.module + jackson-module-jaxb-annotations + ${cs.jackson.version} + net.java.dev.jna jna diff --git a/pom.xml b/pom.xml index b51ed12fded..6985108302d 100644 --- a/pom.xml +++ b/pom.xml @@ -190,6 +190,7 @@ 5.3.26 0.5.4 3.1.7 + 3.25.5 @@ -727,6 +728,17 @@ xml-apis 2.0.2 + + + com.google.protobuf + protobuf-java + ${cs.protobuf.version} + + + com.google.protobuf + protobuf-java-util + ${cs.protobuf.version} + com.linbit.linstor.api java-linstor diff --git a/utils/pom.xml b/utils/pom.xml index 6c367364600..6b8b1249423 100755 --- a/utils/pom.xml +++ b/utils/pom.xml @@ -196,6 +196,11 @@ jackson-databind ${cs.jackson.version} + + com.fasterxml.jackson.dataformat + jackson-dataformat-cbor + ${cs.jackson.version} + org.apache.commons commons-compress From 0e6d2d986b7022648bdb5550060aeb7b7fe76bf4 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 12 Jan 2026 13:23:37 +0530 Subject: [PATCH 33/51] ui: prevent calling listConfigurations when not allowed (#11704) By default, normal users won't have access to listConfigurations API, therefore, UI should not call it when access is not there. Signed-off-by: Abhishek Kumar --- ui/src/store/modules/user.js | 18 ++++++++++-------- .../views/image/RegisterOrUploadTemplate.vue | 3 +++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/ui/src/store/modules/user.js b/ui/src/store/modules/user.js index fc1b0dc25a9..21cd603e378 100644 --- a/ui/src/store/modules/user.js +++ b/ui/src/store/modules/user.js @@ -539,14 +539,16 @@ const user = { reject(error) }) - api('listConfigurations', { name: 'hypervisor.custom.display.name' }).then(json => { - if (json.listconfigurationsresponse.configuration !== null) { - const config = json.listconfigurationsresponse.configuration[0] - commit('SET_CUSTOM_HYPERVISOR_NAME', config.value) - } - }).catch(error => { - reject(error) - }) + if ('listConfigurations' in store.getters.apis) { + api('listConfigurations', { name: 'hypervisor.custom.display.name' }).then(json => { + if (json.listconfigurationsresponse.configuration !== null) { + const config = json.listconfigurationsresponse.configuration[0] + commit('SET_CUSTOM_HYPERVISOR_NAME', config.value) + } + }).catch(error => { + reject(error) + }) + } }) }, UpdateConfiguration ({ commit }) { diff --git a/ui/src/views/image/RegisterOrUploadTemplate.vue b/ui/src/views/image/RegisterOrUploadTemplate.vue index c3f812773be..76df7b246aa 100644 --- a/ui/src/views/image/RegisterOrUploadTemplate.vue +++ b/ui/src/views/image/RegisterOrUploadTemplate.vue @@ -646,6 +646,9 @@ export default { }) }, fetchCustomHypervisorName () { + if (!('listConfigurations' in this.$store.getters.apis)) { + return + } const params = { name: 'hypervisor.custom.display.name' } From c7cfeb5caa1a5d40864ae7f7530bf6563cea6f31 Mon Sep 17 00:00:00 2001 From: Abhisar Sinha <63767682+abh1sar@users.noreply.github.com> Date: Mon, 12 Jan 2026 13:43:12 +0530 Subject: [PATCH 34/51] fix location constraint ceph error (#12285) --- .../storage/datastore/driver/CephObjectStoreDriverImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/storage/object/ceph/src/main/java/org/apache/cloudstack/storage/datastore/driver/CephObjectStoreDriverImpl.java b/plugins/storage/object/ceph/src/main/java/org/apache/cloudstack/storage/datastore/driver/CephObjectStoreDriverImpl.java index 12920e37907..9af558cf6e3 100644 --- a/plugins/storage/object/ceph/src/main/java/org/apache/cloudstack/storage/datastore/driver/CephObjectStoreDriverImpl.java +++ b/plugins/storage/object/ceph/src/main/java/org/apache/cloudstack/storage/datastore/driver/CephObjectStoreDriverImpl.java @@ -350,7 +350,7 @@ public class CephObjectStoreDriverImpl extends BaseObjectStoreDriverImpl { new AWSStaticCredentialsProvider( new BasicAWSCredentials(accessKey, secretKey))) .withEndpointConfiguration( - new AwsClientBuilder.EndpointConfiguration(url, null)) + new AwsClientBuilder.EndpointConfiguration(url, "us-east-1")) .build(); if (client == null) { From 2b373a4659526a43db0ed30ca39927b0183c95de Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Mon, 12 Jan 2026 14:18:35 +0530 Subject: [PATCH 35/51] [UI] Fix primary storage details display when the uuid has divergent pattern (#12307) * [UI] Fix primary storage details display when the uuid has different pattern (eg. for pools with SolidFireShared provider) * Fix on refresh --------- Co-authored-by: vishesh92 --- ui/src/components/view/InfoCard.vue | 2 +- ui/src/components/view/ListView.vue | 4 ++-- ui/src/components/view/VolumesTab.vue | 2 +- ui/src/components/widgets/Breadcrumb.vue | 2 +- ui/src/config/router.js | 4 ++-- ui/src/views/image/IsoZones.vue | 2 +- ui/src/views/image/TemplateZones.vue | 2 +- ui/src/views/storage/SnapshotZones.vue | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue index f1efcaef281..0272df028a3 100644 --- a/ui/src/components/view/InfoCard.vue +++ b/ui/src/components/view/InfoCard.vue @@ -622,7 +622,7 @@
{{ $t('label.storagepool') }}
- {{ resource.storage || resource.storageid }} + {{ resource.storage || resource.storageid }} {{ resource.storage || resource.storageid }} {{ resource.storagetype }} diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index 0109784047a..a02fb5569ed 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -94,7 +94,7 @@ {{ $t(text.toLowerCase()) }} - {{ text }} + {{ text }} {{ text }}   @@ -306,7 +306,7 @@ {{ text }} diff --git a/ui/src/components/widgets/Breadcrumb.vue b/ui/src/components/widgets/Breadcrumb.vue index 147e779502b..4723417f539 100644 --- a/ui/src/components/widgets/Breadcrumb.vue +++ b/ui/src/components/widgets/Breadcrumb.vue @@ -100,7 +100,7 @@ export default { this.breadList = [] this.$route.matched.forEach((item, idx) => { const parent = this.$route.matched[idx - 1] - if (item && parent && parent.name !== 'index' && !item.path.endsWith(':id')) { + if (item && parent && parent.name !== 'index' && !item.path.endsWith(':id') && !item.path.endsWith(':id(.*)')) { this.breadList.pop() } this.breadList.push(item) diff --git a/ui/src/config/router.js b/ui/src/config/router.js index aa85f452b73..f8ff3e00138 100644 --- a/ui/src/config/router.js +++ b/ui/src/config/router.js @@ -90,7 +90,7 @@ function generateRouterMap (section) { hideChildrenInMenu: true, children: [ { - path: '/' + child.name + '/:id', + path: '/' + child.name + '/:id(.*)', hidden: child.hidden, meta: { title: child.title, @@ -145,7 +145,7 @@ function generateRouterMap (section) { map.meta.tabs = section.tabs map.children = [{ - path: '/' + section.name + '/:id', + path: '/' + section.name + '/:id(.*)', actions: section.actions ? section.actions : [], meta: { title: section.title, diff --git a/ui/src/views/image/IsoZones.vue b/ui/src/views/image/IsoZones.vue index 75eac8fd97f..f14ec92b3f6 100644 --- a/ui/src/views/image/IsoZones.vue +++ b/ui/src/views/image/IsoZones.vue @@ -90,7 +90,7 @@ :rowKey="record => record.zoneid">