From ed7bd5e5804c2d834b061f64bfd2d711e1ae5715 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 3 Jul 2025 19:00:42 +0530 Subject: [PATCH 01/94] ui: fix handler for deploy button menu (#11116) Signed-off-by: Abhishek Kumar --- ui/src/views/compute/DeployVM.vue | 4 +- ui/src/views/compute/DeployVnfAppliance.vue | 4 +- ui/src/views/compute/wizard/DeployButtons.vue | 47 +++++++++---------- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/ui/src/views/compute/DeployVM.vue b/ui/src/views/compute/DeployVM.vue index 73ea3fcc59d..d61c9e08ea9 100644 --- a/ui/src/views/compute/DeployVM.vue +++ b/ui/src/views/compute/DeployVM.vue @@ -845,7 +845,7 @@ :deployButtonMenuOptions="deployMenuOptions" @handle-cancel="() => $router.back()" @handle-deploy="handleSubmit" - @handle-deploy-menu="handleSubmitAndStay" /> + @handle-deploy-menu="(index, e) => handleSubmitAndStay(e)" /> @@ -860,7 +860,7 @@ :deployButtonMenuOptions="deployMenuOptions" @handle-cancel="() => $router.back()" @handle-deploy="handleSubmit" - @handle-deploy-menu="handleSubmitAndStay" /> + @handle-deploy-menu="(index, e) => handleSubmitAndStay(e)" /> diff --git a/ui/src/views/compute/DeployVnfAppliance.vue b/ui/src/views/compute/DeployVnfAppliance.vue index 49a2a9ef8f3..b7e797ee57c 100644 --- a/ui/src/views/compute/DeployVnfAppliance.vue +++ b/ui/src/views/compute/DeployVnfAppliance.vue @@ -825,7 +825,7 @@ :deployButtonMenuOptions="deployMenuOptions" @handle-cancel="() => $router.back()" @handle-deploy="handleSubmit" - @handle-deploy-menu="handleSubmitAndStay" /> + @handle-deploy-menu="(index, e) => handleSubmitAndStay(e)" /> @@ -840,7 +840,7 @@ :deployButtonMenuOptions="deployMenuOptions" @handle-cancel="() => $router.back()" @handle-deploy="handleSubmit" - @handle-deploy-menu="handleSubmitAndStay" /> + @handle-deploy-menu="(index, e) => handleSubmitAndStay(e)" /> diff --git a/ui/src/views/compute/wizard/DeployButtons.vue b/ui/src/views/compute/wizard/DeployButtons.vue index 2bd2408e4ed..6fc9aee096d 100644 --- a/ui/src/views/compute/wizard/DeployButtons.vue +++ b/ui/src/views/compute/wizard/DeployButtons.vue @@ -86,7 +86,7 @@ export default { this.$emit('handle-deploy', e) }, handleMenu (e) { - this.$emit('handle-deploy-menu', e.key - 1) + this.$emit('handle-deploy-menu', e.key - 1, e) } } } @@ -94,37 +94,36 @@ export default { From 80f46ad55d63bea88da0d1d38b00b045499aa105 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Fri, 4 Jul 2025 13:54:54 +0530 Subject: [PATCH 02/94] [VMware to KVM Migration] Fix for converted instance npe issue when source vmware instance ovf is exported from management server (#11003) --- .../com/cloud/storage/VolumeApiService.java | 7 + .../agent/api/ConvertInstanceAnswer.java | 16 -- .../agent/api/ConvertInstanceCommand.java | 11 +- .../LibvirtConvertInstanceCommandWrapper.java | 205 +----------------- ...virtConvertInstanceCommandWrapperTest.java | 70 ------ .../vm/UnmanagedVMsManagerImpl.java | 59 +---- .../vm/UnmanagedVMsManagerImplTest.java | 2 - 7 files changed, 20 insertions(+), 350 deletions(-) diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java b/api/src/main/java/com/cloud/storage/VolumeApiService.java index bb69b5b6650..4182728c204 100644 --- a/api/src/main/java/com/cloud/storage/VolumeApiService.java +++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java @@ -171,6 +171,13 @@ public interface VolumeApiService { * */ boolean doesStoragePoolSupportDiskOffering(StoragePool destPool, DiskOffering diskOffering); + + /** + * Checks if the storage pool supports the required disk offering tags + * destPool the storage pool to check the disk offering tags + * diskOfferingTags the tags that should be supported + * return whether the tags are supported in the storage pool + */ boolean doesStoragePoolSupportDiskOfferingTags(StoragePool destPool, String diskOfferingTags); Volume destroyVolume(long volumeId, Account caller, boolean expunge, boolean forceExpunge); diff --git a/core/src/main/java/com/cloud/agent/api/ConvertInstanceAnswer.java b/core/src/main/java/com/cloud/agent/api/ConvertInstanceAnswer.java index 174348f4f18..8092ab9b43f 100644 --- a/core/src/main/java/com/cloud/agent/api/ConvertInstanceAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/ConvertInstanceAnswer.java @@ -16,8 +16,6 @@ // under the License. package com.cloud.agent.api; -import org.apache.cloudstack.vm.UnmanagedInstanceTO; - public class ConvertInstanceAnswer extends Answer { private String temporaryConvertUuid; @@ -25,16 +23,6 @@ public class ConvertInstanceAnswer extends Answer { public ConvertInstanceAnswer() { super(); } - private UnmanagedInstanceTO convertedInstance; - - public ConvertInstanceAnswer(Command command, boolean success, String details) { - super(command, success, details); - } - - public ConvertInstanceAnswer(Command command, UnmanagedInstanceTO convertedInstance) { - super(command, true, ""); - this.convertedInstance = convertedInstance; - } public ConvertInstanceAnswer(Command command, String temporaryConvertUuid) { super(command, true, ""); @@ -44,8 +32,4 @@ public class ConvertInstanceAnswer extends Answer { public String getTemporaryConvertUuid() { return temporaryConvertUuid; } - - public UnmanagedInstanceTO getConvertedInstance() { - return convertedInstance; - } } diff --git a/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java b/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java index b8250903f85..f938d0ac55d 100644 --- a/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java +++ b/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java @@ -20,13 +20,10 @@ import com.cloud.agent.api.to.DataStoreTO; import com.cloud.agent.api.to.RemoteInstanceTO; import com.cloud.hypervisor.Hypervisor; -import java.util.List; - public class ConvertInstanceCommand extends Command { private RemoteInstanceTO sourceInstance; private Hypervisor.HypervisorType destinationHypervisorType; - private List destinationStoragePools; private DataStoreTO conversionTemporaryLocation; private String templateDirOnConversionLocation; private boolean checkConversionSupport; @@ -36,12 +33,10 @@ public class ConvertInstanceCommand extends Command { public ConvertInstanceCommand() { } - public ConvertInstanceCommand(RemoteInstanceTO sourceInstance, Hypervisor.HypervisorType destinationHypervisorType, - List destinationStoragePools, DataStoreTO conversionTemporaryLocation, + public ConvertInstanceCommand(RemoteInstanceTO sourceInstance, Hypervisor.HypervisorType destinationHypervisorType, DataStoreTO conversionTemporaryLocation, String templateDirOnConversionLocation, boolean checkConversionSupport, boolean exportOvfToConversionLocation) { this.sourceInstance = sourceInstance; this.destinationHypervisorType = destinationHypervisorType; - this.destinationStoragePools = destinationStoragePools; this.conversionTemporaryLocation = conversionTemporaryLocation; this.templateDirOnConversionLocation = templateDirOnConversionLocation; this.checkConversionSupport = checkConversionSupport; @@ -56,10 +51,6 @@ public class ConvertInstanceCommand extends Command { return destinationHypervisorType; } - public List getDestinationStoragePools() { - return destinationStoragePools; - } - public DataStoreTO getConversionTemporaryLocation() { return conversionTemporaryLocation; } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapper.java index a11730a1240..e79a8da9dda 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapper.java @@ -18,22 +18,12 @@ // package com.cloud.hypervisor.kvm.resource.wrapper; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; import java.net.URLEncoder; import java.nio.charset.Charset; -import java.util.ArrayList; import java.util.List; import java.util.UUID; -import java.util.stream.Collectors; import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; -import org.apache.cloudstack.vm.UnmanagedInstanceTO; -import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import com.cloud.agent.api.Answer; @@ -44,17 +34,12 @@ import com.cloud.agent.api.to.NfsTO; import com.cloud.agent.api.to.RemoteInstanceTO; import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; -import com.cloud.hypervisor.kvm.resource.LibvirtDomainXMLParser; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef; -import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; import com.cloud.hypervisor.kvm.storage.KVMStoragePool; import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; -import com.cloud.storage.Storage; import com.cloud.utils.FileUtil; -import com.cloud.utils.Pair; -import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.script.OutputInterpreter; import com.cloud.utils.script.Script; @@ -77,7 +62,7 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper getTemporaryDisksFromParsedXml(KVMStoragePool pool, LibvirtDomainXMLParser xmlParser, String convertedBasePath) { - List disksDefs = xmlParser.getDisks(); - disksDefs = disksDefs.stream().filter(x -> x.getDiskType() == LibvirtVMDef.DiskDef.DiskType.FILE && - x.getDeviceType() == LibvirtVMDef.DiskDef.DeviceType.DISK).collect(Collectors.toList()); - if (CollectionUtils.isEmpty(disksDefs)) { - String err = String.format("Cannot find any disk defined on the converted XML domain %s.xml", convertedBasePath); - logger.error(err); - throw new CloudRuntimeException(err); - } - sanitizeDisksPath(disksDefs); - return getPhysicalDisksFromDefPaths(disksDefs, pool); - } - - private List getPhysicalDisksFromDefPaths(List disksDefs, KVMStoragePool pool) { - List disks = new ArrayList<>(); - for (LibvirtVMDef.DiskDef diskDef : disksDefs) { - KVMPhysicalDisk physicalDisk = pool.getPhysicalDisk(diskDef.getDiskPath()); - disks.add(physicalDisk); - } - return disks; - } - - protected List getTemporaryDisksWithPrefixFromTemporaryPool(KVMStoragePool pool, String path, String prefix) { - String msg = String.format("Could not parse correctly the converted XML domain, checking for disks on %s with prefix %s", path, prefix); - logger.info(msg); - pool.refresh(); - List disksWithPrefix = pool.listPhysicalDisks() - .stream() - .filter(x -> x.getName().startsWith(prefix) && !x.getName().endsWith(".xml")) - .collect(Collectors.toList()); - if (CollectionUtils.isEmpty(disksWithPrefix)) { - msg = String.format("Could not find any converted disk with prefix %s on temporary location %s", prefix, path); - logger.error(msg); - throw new CloudRuntimeException(msg); - } - return disksWithPrefix; - } - - private void cleanupDisksAndDomainFromTemporaryLocation(List disks, - KVMStoragePool temporaryStoragePool, - String temporaryConvertUuid) { - for (KVMPhysicalDisk disk : disks) { - logger.info(String.format("Cleaning up temporary disk %s after conversion from temporary location", disk.getName())); - temporaryStoragePool.deletePhysicalDisk(disk.getName(), Storage.ImageFormat.QCOW2); - } - logger.info(String.format("Cleaning up temporary domain %s after conversion from temporary location", temporaryConvertUuid)); - FileUtil.deleteFiles(temporaryStoragePool.getLocalPath(), temporaryConvertUuid, ".xml"); - } - protected void sanitizeDisksPath(List disks) { for (LibvirtVMDef.DiskDef disk : disks) { String[] diskPathParts = disk.getDiskPath().split("/"); @@ -262,114 +198,6 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper moveTemporaryDisksToDestination(List temporaryDisks, - List destinationStoragePools, - KVMStoragePoolManager storagePoolMgr) { - List targetDisks = new ArrayList<>(); - if (temporaryDisks.size() != destinationStoragePools.size()) { - String warn = String.format("Discrepancy between the converted instance disks (%s) " + - "and the expected number of disks (%s)", temporaryDisks.size(), destinationStoragePools.size()); - logger.warn(warn); - } - for (int i = 0; i < temporaryDisks.size(); i++) { - String poolPath = destinationStoragePools.get(i); - KVMStoragePool destinationPool = storagePoolMgr.getStoragePool(Storage.StoragePoolType.NetworkFilesystem, poolPath); - if (destinationPool == null) { - String err = String.format("Could not find a storage pool by URI: %s", poolPath); - logger.error(err); - continue; - } - if (destinationPool.getType() != Storage.StoragePoolType.NetworkFilesystem) { - String err = String.format("Storage pool by URI: %s is not an NFS storage", poolPath); - logger.error(err); - continue; - } - KVMPhysicalDisk sourceDisk = temporaryDisks.get(i); - if (logger.isDebugEnabled()) { - String msg = String.format("Trying to copy converted instance disk number %s from the temporary location %s" + - " to destination storage pool %s", i, sourceDisk.getPool().getLocalPath(), destinationPool.getUuid()); - logger.debug(msg); - } - - String destinationName = UUID.randomUUID().toString(); - - KVMPhysicalDisk destinationDisk = storagePoolMgr.copyPhysicalDisk(sourceDisk, destinationName, destinationPool, 7200 * 1000); - targetDisks.add(destinationDisk); - } - return targetDisks; - } - - private UnmanagedInstanceTO getConvertedUnmanagedInstance(String baseName, - List vmDisks, - LibvirtDomainXMLParser xmlParser) { - UnmanagedInstanceTO instanceTO = new UnmanagedInstanceTO(); - instanceTO.setName(baseName); - instanceTO.setDisks(getUnmanagedInstanceDisks(vmDisks, xmlParser)); - instanceTO.setNics(getUnmanagedInstanceNics(xmlParser)); - return instanceTO; - } - - private List getUnmanagedInstanceNics(LibvirtDomainXMLParser xmlParser) { - List nics = new ArrayList<>(); - if (xmlParser != null) { - List interfaces = xmlParser.getInterfaces(); - for (LibvirtVMDef.InterfaceDef interfaceDef : interfaces) { - UnmanagedInstanceTO.Nic nic = new UnmanagedInstanceTO.Nic(); - nic.setMacAddress(interfaceDef.getMacAddress()); - nic.setNicId(interfaceDef.getBrName()); - nic.setAdapterType(interfaceDef.getModel().toString()); - nics.add(nic); - } - } - return nics; - } - - protected List getUnmanagedInstanceDisks(List vmDisks, LibvirtDomainXMLParser xmlParser) { - List instanceDisks = new ArrayList<>(); - List diskDefs = xmlParser != null ? xmlParser.getDisks() : null; - for (int i = 0; i< vmDisks.size(); i++) { - KVMPhysicalDisk physicalDisk = vmDisks.get(i); - KVMStoragePool storagePool = physicalDisk.getPool(); - UnmanagedInstanceTO.Disk disk = new UnmanagedInstanceTO.Disk(); - disk.setPosition(i); - Pair storagePoolHostAndPath = getNfsStoragePoolHostAndPath(storagePool); - disk.setDatastoreHost(storagePoolHostAndPath.first()); - disk.setDatastorePath(storagePoolHostAndPath.second()); - disk.setDatastoreName(storagePool.getUuid()); - disk.setDatastoreType(storagePool.getType().name()); - disk.setCapacity(physicalDisk.getVirtualSize()); - disk.setFileBaseName(physicalDisk.getName()); - if (CollectionUtils.isNotEmpty(diskDefs)) { - LibvirtVMDef.DiskDef diskDef = diskDefs.get(i); - disk.setController(diskDef.getBusType() != null ? diskDef.getBusType().toString() : LibvirtVMDef.DiskDef.DiskBus.VIRTIO.toString()); - } else { - // If the job is finished but we cannot parse the XML, the guest VM can use the virtio driver - disk.setController(LibvirtVMDef.DiskDef.DiskBus.VIRTIO.toString()); - } - instanceDisks.add(disk); - } - return instanceDisks; - } - - protected Pair getNfsStoragePoolHostAndPath(KVMStoragePool storagePool) { - String sourceHostIp = null; - String sourcePath = null; - List commands = new ArrayList<>(); - commands.add(new String[]{Script.getExecutableAbsolutePath("mount")}); - commands.add(new String[]{Script.getExecutableAbsolutePath("grep"), storagePool.getLocalPath()}); - String storagePoolMountPoint = Script.executePipedCommands(commands, 0).second(); - logger.debug(String.format("NFS Storage pool: %s - local path: %s, mount point: %s", storagePool.getUuid(), storagePool.getLocalPath(), storagePoolMountPoint)); - if (StringUtils.isNotEmpty(storagePoolMountPoint)) { - String[] res = storagePoolMountPoint.strip().split(" "); - res = res[0].split(":"); - if (res.length > 1) { - sourceHostIp = res[0].strip(); - sourcePath = res[1].strip(); - } - } - return new Pair<>(sourceHostIp, sourcePath); - } - private boolean exportOVAFromVMOnVcenter(String vmExportUrl, String targetOvfDir, int noOfThreads, @@ -412,27 +240,6 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper disks = List.of(sourceDisk); - String destinationPoolUuid = UUID.randomUUID().toString(); - List destinationPools = List.of(destinationPoolUuid); - - KVMPhysicalDisk destDisk = Mockito.mock(KVMPhysicalDisk.class); - Mockito.when(destDisk.getPath()).thenReturn("xyz"); - Mockito.when(storagePoolManager.getStoragePool(Storage.StoragePoolType.NetworkFilesystem, destinationPoolUuid)) - .thenReturn(destinationPool); - Mockito.when(destinationPool.getType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); - Mockito.when(storagePoolManager.copyPhysicalDisk(Mockito.eq(sourceDisk), Mockito.anyString(), Mockito.eq(destinationPool), Mockito.anyInt())) - .thenReturn(destDisk); - - List movedDisks = convertInstanceCommandWrapper.moveTemporaryDisksToDestination(disks, destinationPools, storagePoolManager); - Assert.assertEquals(1, movedDisks.size()); - Assert.assertEquals("xyz", movedDisks.get(0).getPath()); - } - - @Test - public void testGetUnmanagedInstanceDisks() { - try (MockedStatic diff --git a/ui/src/api/index.js b/ui/src/api/index.js index 85c46c483b2..45938df6cab 100644 --- a/ui/src/api/index.js +++ b/ui/src/api/index.js @@ -88,10 +88,13 @@ export function login (arg) { }) } -export function logout () { - message.destroy() - notification.destroy() - return postAPI('logout') +export async function logout () { + const result = await postAPI('logout').finally(() => { + sourceToken.cancel() + message.destroy() + notification.destroy() + }) + return result } export function oauthlogin (arg) { diff --git a/ui/src/components/header/UserMenu.vue b/ui/src/components/header/UserMenu.vue index 34d38d8327b..a67fe06096a 100644 --- a/ui/src/components/header/UserMenu.vue +++ b/ui/src/components/header/UserMenu.vue @@ -80,6 +80,8 @@ import { mapActions, mapGetters } from 'vuex' import ResourceIcon from '@/components/view/ResourceIcon' import eventBus from '@/config/eventBus' import { SERVER_MANAGER } from '@/store/mutation-types' +import { sourceToken } from '@/utils/request' +import { applyCustomGuiTheme } from '@/utils/guiTheme' export default { name: 'UserMenu', @@ -178,7 +180,9 @@ export default { } }, handleLogout () { - return this.Logout({}).then(() => { + this.Logout({}).finally(async () => { + sourceToken.init() + await applyCustomGuiTheme(null, null) this.$router.push('/user/login') }).catch(err => { this.$message.error({ diff --git a/ui/src/main.js b/ui/src/main.js index d7f32ff503d..c25ab1066d4 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -43,6 +43,9 @@ import { } from './utils/plugins' import { VueAxios } from './utils/request' import directives from './utils/directives' +import Cookies from 'js-cookie' +import { api } from '@/api' +import { applyCustomGuiTheme } from './utils/guiTheme' vueApp.use(VueAxios, router) vueApp.use(pollJobPlugin) @@ -89,7 +92,7 @@ fetch('config.json?ts=' + Date.now()) } return response.json() }) - .then(config => { + .then(async config => { vueProps.$config = config let baseUrl = config.apiBase if (config.multipleServer) { @@ -98,6 +101,19 @@ fetch('config.json?ts=' + Date.now()) vueProps.axios.defaults.baseURL = baseUrl + const userid = Cookies.get('userid') + let accountid = null + let domainid = null + + if (userid !== undefined && Cookies.get('sessionkey')) { + await api('listUsers', { userid: userid }).then(response => { + accountid = response.listusersresponse.user[0].accountid + domainid = response.listusersresponse.user[0].domainid + }) + } + + await applyCustomGuiTheme(accountid, domainid) + loadLanguageAsync().then(() => { vueApp.use(store) .use(router) diff --git a/ui/src/store/modules/user.js b/ui/src/store/modules/user.js index ffa0c2b136e..0fbdc5788c0 100644 --- a/ui/src/store/modules/user.js +++ b/ui/src/store/modules/user.js @@ -47,6 +47,10 @@ import { LATEST_CS_VERSION } from '@/store/mutation-types' +import { + applyCustomGuiTheme +} from '@/utils/guiTheme' + const user = { state: { token: '', @@ -243,7 +247,6 @@ const user = { const latestVersion = vueProps.$localStorage.get(LATEST_CS_VERSION, { version: '', fetchedTs: 0 }) commit('SET_LATEST_VERSION', latestVersion) notification.destroy() - resolve() }).catch(error => { reject(error) @@ -406,6 +409,7 @@ const user = { getAPI('listUsers', { id: Cookies.get('userid'), showicon: true }).then(response => { const result = response.listusersresponse.user[0] + applyCustomGuiTheme(result.accountid, result.domainid) commit('SET_INFO', result) commit('SET_NAME', result.firstname + ' ' + result.lastname) commit('SET_AVATAR', result.icon?.base64image || '') diff --git a/ui/src/utils/guiTheme.js b/ui/src/utils/guiTheme.js new file mode 100644 index 00000000000..7cc3dc0cc03 --- /dev/null +++ b/ui/src/utils/guiTheme.js @@ -0,0 +1,100 @@ +// 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. + +import { vueProps } from '@/vue-app' +import { api } from '@/api' + +export async function applyCustomGuiTheme (accountid, domainid) { + await fetch('config.json').then(response => response.json()).then(config => { + vueProps.$config = config + }) + + let guiTheme + + if (accountid != null) { + guiTheme = await fetchGuiTheme({ accountid: accountid }) + } + + if (guiTheme === undefined && domainid != null) { + guiTheme = await fetchGuiTheme({ domainid: domainid }) + } + + if (guiTheme === undefined) { + guiTheme = await fetchGuiTheme({ commonname: window.location.hostname }) + } + + if (guiTheme === undefined) { + guiTheme = await fetchGuiTheme({ listonlydefaulttheme: true }) + } + + await applyDynamicCustomization(guiTheme) +} + +async function fetchGuiTheme (params) { + return await api('listGuiThemes', params).then(response => { + if (response.listguithemesresponse.guiThemes) { + return response.listguithemesresponse.guiThemes[0] + } + }) +} + +async function applyDynamicCustomization (response) { + let jsonConfig + + if (response?.jsonconfiguration) { + jsonConfig = JSON.parse(response?.jsonconfiguration) + } + + // Sets custom GUI fields only if is not nullish. + vueProps.$config.appTitle = jsonConfig?.appTitle ?? vueProps.$config.appTitle + vueProps.$config.footer = jsonConfig?.footer ?? vueProps.$config.footer + vueProps.$config.loginFooter = jsonConfig?.loginFooter ?? vueProps.$config.loginFooter + vueProps.$config.logo = jsonConfig?.logo ?? vueProps.$config.logo + vueProps.$config.minilogo = jsonConfig?.minilogo ?? vueProps.$config.minilogo + vueProps.$config.banner = jsonConfig?.banner ?? vueProps.$config.banner + + if (jsonConfig?.error) { + vueProps.$config.error[403] = jsonConfig?.error[403] ?? vueProps.$config.error[403] + vueProps.$config.error[404] = jsonConfig?.error[404] ?? vueProps.$config.error[404] + vueProps.$config.error[500] = jsonConfig?.error[500] ?? vueProps.$config.error[500] + } + + if (jsonConfig?.plugins) { + jsonConfig.plugins.forEach(plugin => { + vueProps.$config.plugins.push(plugin) + }) + } + + vueProps.$config.favicon = jsonConfig?.favicon ?? vueProps.$config.favicon + vueProps.$config.css = response?.css ?? null + + await applyStaticCustomization(vueProps.$config.favicon, vueProps.$config.css) +} + +async function applyStaticCustomization (favicon, css) { + document.getElementById('favicon').href = favicon + + let style = document.getElementById('guiThemeCSS') + if (style != null) { + style.innerHTML = css + } else { + style = document.createElement('style') + style.setAttribute('id', 'guiThemeCSS') + style.innerHTML = css + document.body.appendChild(style) + } +} From e8ab0ae70a72e9b59479841a96e06700fb185fea Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Tue, 15 Jul 2025 16:40:53 +0530 Subject: [PATCH 17/94] CPU to Memory weight based algorithm to order cluster (#10997) * CPU to Memory weight based algorithm to order cluster host.capacityType.to.order.clusters config will support new algorithm: COMBINED which will work with host.capacityType.to.order.clusters.cputomemoryweight and capacity will be computed based on CPU and memory both and using weight factor * minor changes * add unit tests * update desc and add validation * handle copilot review comments * add log indicating chosen capacityType for ordering --------- Co-authored-by: Rohit Yadav --- .../apache/cloudstack/api/ApiConstants.java | 5 + .../configuration/ConfigurationManager.java | 6 + .../com/cloud/capacity/dao/CapacityDao.java | 10 +- .../cloud/capacity/dao/CapacityDaoImpl.java | 62 ++++- .../META-INF/db/schema-42010to42100.sql | 6 + .../capacity/dao/CapacityDaoImplTest.java | 226 +++++++++++++++++- .../implicitplanner/ImplicitPlannerTest.java | 2 +- .../allocator/impl/FirstFitAllocator.java | 45 +++- .../java/com/cloud/configuration/Config.java | 5 +- .../ConfigurationManagerImpl.java | 3 +- .../com/cloud/deploy/FirstFitPlanner.java | 125 ++++++++-- .../allocator/impl/FirstFitAllocatorTest.java | 62 +++++ .../com/cloud/vm/FirstFitPlannerTest.java | 142 ++++++++++- 13 files changed, 661 insertions(+), 38 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 882634ec6c8..f5a1636c30e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -90,9 +90,11 @@ public class ApiConstants { public static final String CONVERT_INSTANCE_HOST_ID = "convertinstancehostid"; public static final String CONVERT_INSTANCE_STORAGE_POOL_ID = "convertinstancepoolid"; public static final String ENABLED_REVOCATION_CHECK = "enabledrevocationcheck"; + public static final String COMBINED_CAPACITY_ORDERING = "COMBINED"; public static final String CONTROLLER = "controller"; public static final String CONTROLLER_UNIT = "controllerunit"; public static final String COPY_IMAGE_TAGS = "copyimagetags"; + public static final String CPU_OVERCOMMIT_RATIO = "cpuOvercommitRatio"; public static final String CSR = "csr"; public static final String PRIVATE_KEY = "privatekey"; public static final String DATASTORE_HOST = "datastorehost"; @@ -124,6 +126,7 @@ public class ApiConstants { public static final String CNI_CONFIG_DETAILS = "cniconfigdetails"; public static final String CNI_CONFIG_NAME = "cniconfigname"; public static final String COMPONENT = "component"; + public static final String CPU = "CPU"; public static final String CPU_CORE_PER_SOCKET = "cpucorepersocket"; public static final String CPU_NUMBER = "cpunumber"; public static final String CPU_SPEED = "cpuspeed"; @@ -344,6 +347,7 @@ public class ApiConstants { public static final String MAX_BACKUPS = "maxbackups"; public static final String MAX_CPU_NUMBER = "maxcpunumber"; public static final String MAX_MEMORY = "maxmemory"; + public static final String MEMORY_OVERCOMMIT_RATIO = "memoryOvercommitRatio"; public static final String MIN_CPU_NUMBER = "mincpunumber"; public static final String MIN_MEMORY = "minmemory"; public static final String MIGRATION_TYPE = "migrationtype"; @@ -441,6 +445,7 @@ public class ApiConstants { public static final String PUBLIC_END_PORT = "publicendport"; public static final String PUBLIC_ZONE = "publiczone"; public static final String PURGE_RESOURCES = "purgeresources"; + public static final String RAM = "RAM"; public static final String REBALANCE = "rebalance"; public static final String RECEIVED_BYTES = "receivedbytes"; public static final String RECONNECT = "reconnect"; diff --git a/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java b/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java index 859d1176213..5655a0593c8 100644 --- a/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java +++ b/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java @@ -65,6 +65,12 @@ public interface ConfigurationManager { "allow.non.rfc1918.compliant.ips", "Advanced", "false", "Allows non-compliant RFC 1918 IPs for Shared, Isolated networks and VPCs", true, null); + ConfigKey HostCapacityTypeCpuMemoryWeight = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Float.class, + "host.capacityType.to.order.clusters.cputomemoryweight", + "0.5", + "Weight for CPU (as a value between 0 and 1) applied to compute capacity for Pods, Clusters and Hosts for COMBINED capacityType for ordering. Weight for RAM will be (1 - weight of CPU)", + true, ConfigKey.Scope.Global); + /** * @param offering * @return diff --git a/engine/schema/src/main/java/com/cloud/capacity/dao/CapacityDao.java b/engine/schema/src/main/java/com/cloud/capacity/dao/CapacityDao.java index 7a4c96c6e1f..42313efa512 100644 --- a/engine/schema/src/main/java/com/cloud/capacity/dao/CapacityDao.java +++ b/engine/schema/src/main/java/com/cloud/capacity/dao/CapacityDao.java @@ -30,7 +30,7 @@ public interface CapacityDao extends GenericDao { List listByHostIdTypes(Long hostId, List capacityTypes); - List listClustersInZoneOrPodByHostCapacities(long id, long vmId, int requiredCpu, long requiredRam, short capacityTypeForOrdering, boolean isZone); + List listClustersInZoneOrPodByHostCapacities(long id, long vmId, int requiredCpu, long requiredRam, boolean isZone); List listHostsWithEnoughCapacity(int requiredCpu, long requiredRam, Long clusterId, String hostType); @@ -48,7 +48,7 @@ public interface CapacityDao extends GenericDao { List findFilteredCapacityBy(Integer capacityType, Long zoneId, Long podId, Long clusterId, List hostIds, List poolIds); - List listPodsByHostCapacities(long zoneId, int requiredCpu, long requiredRam, short capacityType); + List listPodsByHostCapacities(long zoneId, int requiredCpu, long requiredRam); Pair, Map> orderPodsByAggregateCapacity(long zoneId, short capacityType); @@ -65,4 +65,10 @@ public interface CapacityDao extends GenericDao { float findClusterConsumption(Long clusterId, short capacityType, long computeRequested); Pair, Map> orderHostsByFreeCapacity(Long zoneId, Long clusterId, short capacityType); + + List listHostCapacityByCapacityTypes(Long zoneId, Long clusterId, List capacityTypes); + + List listPodCapacityByCapacityTypes(Long zoneId, List capacityTypes); + + List listClusterCapacityByCapacityTypes(Long zoneId, Long podId, List capacityTypes); } 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 0860f14518f..d54d1632bd0 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 @@ -684,7 +684,7 @@ public class CapacityDaoImpl extends GenericDaoBase implements } @Override - public List listClustersInZoneOrPodByHostCapacities(long id, long vmId, int requiredCpu, long requiredRam, short capacityTypeForOrdering, boolean isZone) { + public List listClustersInZoneOrPodByHostCapacities(long id, long vmId, int requiredCpu, long requiredRam, boolean isZone) { TransactionLegacy txn = TransactionLegacy.currentTxn(); PreparedStatement pstmt = null; List result = new ArrayList(); @@ -1068,7 +1068,65 @@ public class CapacityDaoImpl extends GenericDaoBase implements } @Override - public List listPodsByHostCapacities(long zoneId, int requiredCpu, long requiredRam, short capacityType) { + public List listHostCapacityByCapacityTypes(Long zoneId, Long clusterId, List capacityTypes) { + SearchBuilder sb = createSearchBuilder(); + sb.and("zoneId", sb.entity().getDataCenterId(), SearchCriteria.Op.EQ); + sb.and("clusterId", sb.entity().getClusterId(), SearchCriteria.Op.EQ); + sb.and("capacityTypes", sb.entity().getCapacityType(), SearchCriteria.Op.IN); + sb.and("capacityState", sb.entity().getCapacityState(), Op.EQ); + sb.done(); + + SearchCriteria sc = sb.create(); + sc.setParameters("capacityState", "Enabled"); + if (zoneId != null) { + sc.setParameters("zoneId", zoneId); + } + if (clusterId != null) { + sc.setParameters("clusterId", clusterId); + } + sc.setParameters("capacityTypes", capacityTypes.toArray()); + return listBy(sc); + } + + @Override + public List listPodCapacityByCapacityTypes(Long zoneId, List capacityTypes) { + SearchBuilder sb = createSearchBuilder(); + sb.and("zoneId", sb.entity().getDataCenterId(), SearchCriteria.Op.EQ); + sb.and("capacityTypes", sb.entity().getCapacityType(), SearchCriteria.Op.IN); + sb.and("capacityState", sb.entity().getCapacityState(), Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("capacityState", "Enabled"); + if (zoneId != null) { + sc.setParameters("zoneId", zoneId); + } + sc.setParameters("capacityTypes", capacityTypes.toArray()); + return listBy(sc); + } + + @Override + public List listClusterCapacityByCapacityTypes(Long zoneId, Long podId, List capacityTypes) { + SearchBuilder sb = createSearchBuilder(); + sb.and("zoneId", sb.entity().getDataCenterId(), SearchCriteria.Op.EQ); + sb.and("podId", sb.entity().getPodId(), SearchCriteria.Op.EQ); + sb.and("capacityTypes", sb.entity().getCapacityType(), SearchCriteria.Op.IN); + sb.and("capacityState", sb.entity().getCapacityState(), Op.EQ); + sb.done(); + + SearchCriteria sc = sb.create(); + sc.setParameters("capacityState", "Enabled"); + if (zoneId != null) { + sc.setParameters("zoneId", zoneId); + } + if (podId != null) { + sc.setParameters("podId", podId); + } + sc.setParameters("capacityTypes", capacityTypes.toArray()); + return listBy(sc); + } + + @Override + public List listPodsByHostCapacities(long zoneId, int requiredCpu, long requiredRam) { TransactionLegacy txn = TransactionLegacy.currentTxn(); PreparedStatement pstmt = null; List result = new ArrayList(); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql index 53c2bbf60cc..3fd4914b2e2 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql @@ -204,6 +204,12 @@ SET `sort_key` = CASE END; -- End: Changes for Guest OS category cleanup +-- Update description for configuration: host.capacityType.to.order.clusters +UPDATE `cloud`.`configuration` SET + `description` = 'The host capacity type (CPU, RAM or COMBINED) is used by deployment planner to order clusters during VM resource allocation' +WHERE `name` = 'host.capacityType.to.order.clusters' + AND `description` = 'The host capacity type (CPU or RAM) is used by deployment planner to order clusters during VM resource allocation'; + -- Whitelabel GUI CREATE TABLE IF NOT EXISTS `cloud`.`gui_themes` ( `id` bigint(20) unsigned NOT NULL auto_increment, diff --git a/engine/schema/src/test/java/com/cloud/capacity/dao/CapacityDaoImplTest.java b/engine/schema/src/test/java/com/cloud/capacity/dao/CapacityDaoImplTest.java index 8e9a0bd34c7..f9528d5d57f 100644 --- a/engine/schema/src/test/java/com/cloud/capacity/dao/CapacityDaoImplTest.java +++ b/engine/schema/src/test/java/com/cloud/capacity/dao/CapacityDaoImplTest.java @@ -51,6 +51,7 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; 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; @@ -61,6 +62,9 @@ public class CapacityDaoImplTest { @InjectMocks CapacityDaoImpl capacityDao = new CapacityDaoImpl(); + @Mock + private CapacityVO mockEntity; + @Mock private TransactionLegacy txn; @Mock @@ -71,6 +75,8 @@ public class CapacityDaoImplTest { private SearchBuilder searchBuilder; private SearchCriteria searchCriteria; + private List capacityTypes; + private List expectedCapacities; @Before public void setUp() { @@ -83,6 +89,17 @@ public class CapacityDaoImplTest { mockedTransactionLegacy = Mockito.mockStatic(TransactionLegacy.class); mockedTransactionLegacy.when(TransactionLegacy::currentTxn).thenReturn(txn); + + // Setup common test data + capacityTypes = Arrays.asList((short) 1, (short) 2, (short) 3); + expectedCapacities = Arrays.asList(mock(CapacityVO.class), mock(CapacityVO.class)); + doReturn(expectedCapacities).when(capacityDao).listBy(searchCriteria); + } + + private CapacityVO createMockCapacityVO(Long id) { + CapacityVO capacity = mock(CapacityVO.class); + when(capacity.getId()).thenReturn(id); + return capacity; } @After @@ -205,11 +222,11 @@ public class CapacityDaoImplTest { when(pstmt.executeQuery()).thenReturn(resultSet); when(resultSet.next()).thenReturn(false); - List resultZone = capacityDao.listClustersInZoneOrPodByHostCapacities(1L, 123L, 2, 2048L, (short)0, true); + List resultZone = capacityDao.listClustersInZoneOrPodByHostCapacities(1L, 123L, 2, 2048L, true); assertNotNull(resultZone); assertTrue(resultZone.isEmpty()); - List resultPod = capacityDao.listClustersInZoneOrPodByHostCapacities(1L, 123L, 2, 2048L, (short)0, false); + List resultPod = capacityDao.listClustersInZoneOrPodByHostCapacities(1L, 123L, 2, 2048L, false); assertNotNull(resultPod); assertTrue(resultPod.isEmpty()); } @@ -281,7 +298,7 @@ public class CapacityDaoImplTest { when(pstmt.executeQuery()).thenReturn(resultSet); when(resultSet.next()).thenReturn(false); - List result = capacityDao.listPodsByHostCapacities(1L, 2, 1024L, (short)0); + List result = capacityDao.listPodsByHostCapacities(1L, 2, 1024L); assertNotNull(result); assertTrue(result.isEmpty()); } @@ -330,4 +347,207 @@ public class CapacityDaoImplTest { assertNotNull(result); assertTrue(result.isEmpty()); } + + @Test + public void testListHostCapacityByCapacityTypes_WithAllParameters() { + // Given + Long zoneId = 100L; + Long clusterId = 200L; + + // When + List result = capacityDao.listHostCapacityByCapacityTypes(zoneId, clusterId, capacityTypes); + + // Then + verify(searchBuilder).and("zoneId", mockEntity.getDataCenterId(), SearchCriteria.Op.EQ); + verify(searchBuilder).and("clusterId", mockEntity.getClusterId(), SearchCriteria.Op.EQ); + verify(searchBuilder).and("capacityTypes", mockEntity.getCapacityType(), SearchCriteria.Op.IN); + verify(searchBuilder).and("capacityState", mockEntity.getCapacityState(), SearchCriteria.Op.EQ); + + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria).setParameters("zoneId", zoneId); + verify(searchCriteria).setParameters("clusterId", clusterId); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + verify(capacityDao).listBy(searchCriteria); + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListHostCapacityByCapacityTypes_WithNullZoneId() { + // Given + Long clusterId = 200L; + + // When + List result = capacityDao.listHostCapacityByCapacityTypes(null, clusterId, capacityTypes); + + // Then + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria, Mockito.times(0)).setParameters(eq("zoneId"), any()); + verify(searchCriteria).setParameters("clusterId", clusterId); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListHostCapacityByCapacityTypes_WithNullClusterId() { + // Given + Long zoneId = 100L; + + // When + List result = capacityDao.listHostCapacityByCapacityTypes(zoneId, null, capacityTypes); + + // Then + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria).setParameters("zoneId", zoneId); + verify(searchCriteria, never()).setParameters(eq("clusterId"), any()); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListHostCapacityByCapacityTypes_WithEmptyCapacityTypes() { + // Given + Long zoneId = 100L; + Long clusterId = 200L; + List emptyCapacityTypes = Collections.emptyList(); + + // When + List result = capacityDao.listHostCapacityByCapacityTypes(zoneId, clusterId, emptyCapacityTypes); + + // Then + verify(searchCriteria).setParameters("capacityTypes", emptyCapacityTypes.toArray()); + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListPodCapacityByCapacityTypes_WithAllParameters() { + // Given + Long zoneId = 100L; + + // When + List result = capacityDao.listPodCapacityByCapacityTypes(zoneId, capacityTypes); + + // Then + verify(searchBuilder).and("zoneId", mockEntity.getDataCenterId(), SearchCriteria.Op.EQ); + verify(searchBuilder).and("capacityTypes", mockEntity.getCapacityType(), SearchCriteria.Op.IN); + verify(searchBuilder).and("capacityState", mockEntity.getCapacityState(), SearchCriteria.Op.EQ); + + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria).setParameters("zoneId", zoneId); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListPodCapacityByCapacityTypes_WithNullZoneId() { + // When + List result = capacityDao.listPodCapacityByCapacityTypes(null, capacityTypes); + + // Then + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria, never()).setParameters(eq("zoneId"), any()); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListClusterCapacityByCapacityTypes_WithAllParameters() { + // Given + Long zoneId = 100L; + Long podId = 300L; + + // When + List result = capacityDao.listClusterCapacityByCapacityTypes(zoneId, podId, capacityTypes); + + // Then + verify(searchBuilder).and("zoneId", mockEntity.getDataCenterId(), SearchCriteria.Op.EQ); + verify(searchBuilder).and("podId", mockEntity.getPodId(), SearchCriteria.Op.EQ); + verify(searchBuilder).and("capacityTypes", mockEntity.getCapacityType(), SearchCriteria.Op.IN); + verify(searchBuilder).and("capacityState", mockEntity.getCapacityState(), SearchCriteria.Op.EQ); + + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria).setParameters("zoneId", zoneId); + verify(searchCriteria).setParameters("podId", podId); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListClusterCapacityByCapacityTypes_WithNullZoneId() { + // Given + Long podId = 300L; + + // When + List result = capacityDao.listClusterCapacityByCapacityTypes(null, podId, capacityTypes); + + // Then + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria, never()).setParameters(eq("zoneId"), any()); + verify(searchCriteria).setParameters("podId", podId); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListClusterCapacityByCapacityTypes_WithNullPodId() { + // Given + Long zoneId = 100L; + + // When + List result = capacityDao.listClusterCapacityByCapacityTypes(zoneId, null, capacityTypes); + + // Then + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria).setParameters("zoneId", zoneId); + verify(searchCriteria, never()).setParameters(eq("podId"), any()); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListClusterCapacityByCapacityTypes_WithBothIdsNull() { + // When + List result = capacityDao.listClusterCapacityByCapacityTypes(null, null, capacityTypes); + + // Then + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria, never()).setParameters(eq("zoneId"), any()); + verify(searchCriteria, never()).setParameters(eq("podId"), any()); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testAllMethods_VerifySearchBuilderSetup() { + // Test that all methods properly set up the search builder + Long zoneId = 100L; + Long clusterId = 200L; + Long podId = 300L; + + // Test host capacity method + capacityDao.listHostCapacityByCapacityTypes(zoneId, clusterId, capacityTypes); + + // Test pod capacity method + capacityDao.listPodCapacityByCapacityTypes(zoneId, capacityTypes); + + // Test cluster capacity method + capacityDao.listClusterCapacityByCapacityTypes(zoneId, podId, capacityTypes); + + // Verify createSearchBuilder was called 3 times + verify(capacityDao, times(3)).createSearchBuilder(); + + // Verify done() was called 3 times + verify(searchBuilder, times(3)).done(); + + // Verify listBy was called 3 times + verify(capacityDao, times(3)).listBy(searchCriteria); + } } diff --git a/plugins/deployment-planners/implicit-dedication/src/test/java/org/apache/cloudstack/implicitplanner/ImplicitPlannerTest.java b/plugins/deployment-planners/implicit-dedication/src/test/java/org/apache/cloudstack/implicitplanner/ImplicitPlannerTest.java index 2d2b4c78261..a00fb49bb3f 100644 --- a/plugins/deployment-planners/implicit-dedication/src/test/java/org/apache/cloudstack/implicitplanner/ImplicitPlannerTest.java +++ b/plugins/deployment-planners/implicit-dedication/src/test/java/org/apache/cloudstack/implicitplanner/ImplicitPlannerTest.java @@ -359,7 +359,7 @@ public class ImplicitPlannerTest { clustersWithEnoughCapacity.add(3L); when( capacityDao.listClustersInZoneOrPodByHostCapacities(dataCenterId, 12L, noOfCpusInOffering * cpuSpeedInOffering, ramInOffering * 1024L * 1024L, - Capacity.CAPACITY_TYPE_CPU, true)).thenReturn(clustersWithEnoughCapacity); + true)).thenReturn(clustersWithEnoughCapacity); Map clusterCapacityMap = new HashMap(); clusterCapacityMap.put(1L, 2048D); diff --git a/server/src/main/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java b/server/src/main/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java index 68a901a68a2..3ca47caf8af 100644 --- a/server/src/main/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java +++ b/server/src/main/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocator.java @@ -30,15 +30,18 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; import com.cloud.agent.manager.allocator.HostAllocator; +import com.cloud.capacity.Capacity; import com.cloud.capacity.CapacityManager; import com.cloud.capacity.CapacityVO; import com.cloud.capacity.dao.CapacityDao; import com.cloud.configuration.Config; +import com.cloud.configuration.ConfigurationManager; import com.cloud.dc.ClusterDetailsDao; import com.cloud.dc.dao.ClusterDao; import com.cloud.deploy.DeploymentPlan; import com.cloud.deploy.DeploymentClusterPlanner; import com.cloud.deploy.DeploymentPlanner.ExcludeList; +import com.cloud.deploy.FirstFitPlanner; import com.cloud.gpu.GPU; import com.cloud.host.DetailVO; import com.cloud.host.Host; @@ -67,6 +70,7 @@ import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; +import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; /** @@ -295,7 +299,7 @@ public class FirstFitAllocator extends AdapterBase implements HostAllocator { Collections.shuffle(hosts); } else if (vmAllocationAlgorithm.equals("userdispersing")) { hosts = reorderHostsByNumberOfVms(plan, hosts, account); - }else if(vmAllocationAlgorithm.equals("firstfitleastconsumed")){ + } else if(vmAllocationAlgorithm.equals("firstfitleastconsumed")){ hosts = reorderHostsByCapacity(plan, hosts); } @@ -372,13 +376,7 @@ public class FirstFitAllocator extends AdapterBase implements HostAllocator { private List reorderHostsByCapacity(DeploymentPlan plan, List hosts) { Long zoneId = plan.getDataCenterId(); Long clusterId = plan.getClusterId(); - //Get capacity by which we should reorder - String capacityTypeToOrder = _configDao.getValue(Config.HostCapacityTypeToOrderClusters.key()); - short capacityType = CapacityVO.CAPACITY_TYPE_CPU; - if("RAM".equalsIgnoreCase(capacityTypeToOrder)){ - capacityType = CapacityVO.CAPACITY_TYPE_MEMORY; - } - Pair, Map> result = _capacityDao.orderHostsByFreeCapacity(zoneId, clusterId, capacityType); + Pair, Map> result = getOrderedHostsByCapacity(zoneId, clusterId); List hostIdsByFreeCapacity = result.first(); Map sortedHostByCapacity = result.second().entrySet() .stream() @@ -407,6 +405,37 @@ public class FirstFitAllocator extends AdapterBase implements HostAllocator { return reorderedHosts; } + private Pair, Map> getOrderedHostsByCapacity(Long zoneId, Long clusterId) { + double cpuToMemoryWeight = ConfigurationManager.HostCapacityTypeCpuMemoryWeight.value(); + // Get capacity by which we should reorder + short capacityType = FirstFitPlanner.getHostCapacityTypeToOrderCluster( + _configDao.getValue(Config.HostCapacityTypeToOrderClusters.key()), cpuToMemoryWeight); + logger.debug("CapacityType: {} is used for Host ordering", FirstFitPlanner.getCapacityTypeName(capacityType)); + if (capacityType >= 0) { // for CPU or RAM + return _capacityDao.orderHostsByFreeCapacity(zoneId, clusterId, capacityType); + } + List capacities = _capacityDao.listHostCapacityByCapacityTypes(zoneId, clusterId, + List.of(Capacity.CAPACITY_TYPE_CPU, Capacity.CAPACITY_TYPE_MEMORY)); + Map hostByComputedCapacity = getHostByCombinedCapacities(capacities, cpuToMemoryWeight); + return new Pair<>(new ArrayList<>(hostByComputedCapacity.keySet()), hostByComputedCapacity); + } + + @NotNull + public static Map getHostByCombinedCapacities(List capacities, double cpuToMemoryWeight) { + Map hostByComputedCapacity = new HashMap<>(); + for (CapacityVO capacityVO : capacities) { + long hostId = capacityVO.getHostOrPoolId(); + double applicableWeight = capacityVO.getCapacityType() == Capacity.CAPACITY_TYPE_CPU ? cpuToMemoryWeight : 1 - cpuToMemoryWeight; + double capacityMetric = applicableWeight * (capacityVO.getTotalCapacity() - (capacityVO.getUsedCapacity() + capacityVO.getReservedCapacity()))/capacityVO.getTotalCapacity(); + hostByComputedCapacity.merge(hostId, capacityMetric, Double::sum); + } + + return hostByComputedCapacity.entrySet() + .stream() + .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); + } + private List reorderHostsByNumberOfVms(DeploymentPlan plan, List hosts, Account account) { if (account == null) { return hosts; diff --git a/server/src/main/java/com/cloud/configuration/Config.java b/server/src/main/java/com/cloud/configuration/Config.java index a6efaffcb56..d35bfac501f 100644 --- a/server/src/main/java/com/cloud/configuration/Config.java +++ b/server/src/main/java/com/cloud/configuration/Config.java @@ -897,8 +897,9 @@ public enum Config { String.class, "host.capacityType.to.order.clusters", "CPU", - "The host capacity type (CPU or RAM) is used by deployment planner to order clusters during VM resource allocation", - "CPU,RAM"), + "The host capacity type (CPU, RAM, COMBINED) is used by deployment planner to order clusters during VM resource allocation", + "CPU,RAM,COMBINED"), + ApplyAllocationAlgorithmToPods( "Advanced", ManagementServer.class, diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 2452ac96cfc..8cee3dba527 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -601,6 +601,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati weightBasedParametersForValidation.add(CapacityManager.SecondaryStorageCapacityThreshold.key()); weightBasedParametersForValidation.add(ClusterDrsService.ClusterDrsImbalanceThreshold.key()); weightBasedParametersForValidation.add(ClusterDrsService.ClusterDrsImbalanceSkipThreshold.key()); + weightBasedParametersForValidation.add(ConfigurationManager.HostCapacityTypeCpuMemoryWeight.key()); } protected void overProvisioningFactorsForValidation() { @@ -8274,7 +8275,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati BYTES_MAX_READ_LENGTH, BYTES_MAX_WRITE_LENGTH, ADD_HOST_ON_SERVICE_RESTART_KVM, SET_HOST_DOWN_TO_MAINTENANCE, VM_SERVICE_OFFERING_MAX_CPU_CORES, VM_SERVICE_OFFERING_MAX_RAM_SIZE, MIGRATE_VM_ACROSS_CLUSTERS, ENABLE_ACCOUNT_SETTINGS_FOR_DOMAIN, ENABLE_DOMAIN_SETTINGS_FOR_CHILD_DOMAIN, - ALLOW_DOMAIN_ADMINS_TO_CREATE_TAGGED_OFFERINGS, DELETE_QUERY_BATCH_SIZE, AllowNonRFC1918CompliantIPs + ALLOW_DOMAIN_ADMINS_TO_CREATE_TAGGED_OFFERINGS, DELETE_QUERY_BATCH_SIZE, AllowNonRFC1918CompliantIPs, HostCapacityTypeCpuMemoryWeight }; } diff --git a/server/src/main/java/com/cloud/deploy/FirstFitPlanner.java b/server/src/main/java/com/cloud/deploy/FirstFitPlanner.java index abaf48400e2..d01e43622c6 100644 --- a/server/src/main/java/com/cloud/deploy/FirstFitPlanner.java +++ b/server/src/main/java/com/cloud/deploy/FirstFitPlanner.java @@ -20,14 +20,19 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.naming.ConfigurationException; import com.cloud.capacity.CapacityVO; +import com.cloud.configuration.ConfigurationManager; +import com.cloud.dc.ClusterDetailsVO; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; @@ -457,17 +462,14 @@ public class FirstFitPlanner extends AdapterBase implements DeploymentClusterPla logger.debug("Listing clusters in order of aggregate capacity, that have (at least one host with) enough CPU and RAM capacity under this " + (isZone ? "Zone: " : "Pod: ") + id); } - String capacityTypeToOrder = configDao.getValue(Config.HostCapacityTypeToOrderClusters.key()); - short capacityType = Capacity.CAPACITY_TYPE_CPU; - if ("RAM".equalsIgnoreCase(capacityTypeToOrder)) { - capacityType = Capacity.CAPACITY_TYPE_MEMORY; - } - List clusterIdswithEnoughCapacity = capacityDao.listClustersInZoneOrPodByHostCapacities(id, vmId, requiredCpu, requiredRam, capacityType, isZone); + List clusterIdswithEnoughCapacity = capacityDao.listClustersInZoneOrPodByHostCapacities(id, vmId, requiredCpu, requiredRam, isZone); if (logger.isTraceEnabled()) { logger.trace("ClusterId List having enough CPU and RAM capacity: " + clusterIdswithEnoughCapacity); } - Pair, Map> result = capacityDao.orderClustersByAggregateCapacity(id, vmId, capacityType, isZone); + + + Pair, Map> result = getOrderedClustersByCapacity(id, vmId, isZone); List clusterIdsOrderedByAggregateCapacity = result.first(); //only keep the clusters that have enough capacity to host this VM if (logger.isTraceEnabled()) { @@ -491,17 +493,12 @@ public class FirstFitPlanner extends AdapterBase implements DeploymentClusterPla if (logger.isDebugEnabled()) { logger.debug("Listing pods in order of aggregate capacity, that have (at least one host with) enough CPU and RAM capacity under this Zone: " + zoneId); } - String capacityTypeToOrder = configDao.getValue(Config.HostCapacityTypeToOrderClusters.key()); - short capacityType = Capacity.CAPACITY_TYPE_CPU; - if ("RAM".equalsIgnoreCase(capacityTypeToOrder)) { - capacityType = Capacity.CAPACITY_TYPE_MEMORY; - } - - List podIdswithEnoughCapacity = capacityDao.listPodsByHostCapacities(zoneId, requiredCpu, requiredRam, capacityType); + List podIdswithEnoughCapacity = capacityDao.listPodsByHostCapacities(zoneId, requiredCpu, requiredRam); if (logger.isTraceEnabled()) { logger.trace("PodId List having enough CPU and RAM capacity: " + podIdswithEnoughCapacity); } - Pair, Map> result = capacityDao.orderPodsByAggregateCapacity(zoneId, capacityType); + + Pair, Map> result = getOrderedPodsByCapacity(zoneId); List podIdsOrderedByAggregateCapacity = result.first(); //only keep the clusters that have enough capacity to host this VM if (logger.isTraceEnabled()) { @@ -517,6 +514,104 @@ public class FirstFitPlanner extends AdapterBase implements DeploymentClusterPla } + private Pair, Map> getOrderedPodsByCapacity(long zoneId) { + double cpuToMemoryWeight = ConfigurationManager.HostCapacityTypeCpuMemoryWeight.value(); + short capacityType = getHostCapacityTypeToOrderCluster( + configDao.getValue(Config.HostCapacityTypeToOrderClusters.key()), cpuToMemoryWeight); + + logger.debug("CapacityType: {} is used for Pod ordering", getCapacityTypeName(capacityType)); + if (capacityType >= 0) { // for capacityType other than COMBINED + return capacityDao.orderPodsByAggregateCapacity(zoneId, capacityType); + } + List capacities = capacityDao.listPodCapacityByCapacityTypes(zoneId, List.of(Capacity.CAPACITY_TYPE_CPU, Capacity.CAPACITY_TYPE_MEMORY)); + Map podsByCombinedCapacities = getPodByCombinedCapacities(capacities, cpuToMemoryWeight); + return new Pair<>(new ArrayList<>(podsByCombinedCapacities.keySet()), podsByCombinedCapacities); + } + + // order pods by combining cpu and memory capacity considering cpuToMemoeryWeight + public Map getPodByCombinedCapacities(List capacities, double cpuToMemoryWeight) { + Map podByCombinedCapacity = new HashMap<>(); + for (CapacityVO capacityVO : capacities) { + boolean isCPUCapacity = capacityVO.getCapacityType() == Capacity.CAPACITY_TYPE_CPU; + long podId = capacityVO.getPodId(); + double applicableWeight = isCPUCapacity ? cpuToMemoryWeight : 1 - cpuToMemoryWeight; + String overCommitRatioParam = isCPUCapacity ? ApiConstants.CPU_OVERCOMMIT_RATIO : ApiConstants.MEMORY_OVERCOMMIT_RATIO; + ClusterDetailsVO overCommitRatioVO = clusterDetailsDao.findDetail(capacityVO.getClusterId(), overCommitRatioParam); + float overCommitRatio = Float.parseFloat(overCommitRatioVO.getValue()); + double capacityMetric = applicableWeight * + (capacityVO.getUsedCapacity() + capacityVO.getReservedCapacity())/(capacityVO.getTotalCapacity() * overCommitRatio); + podByCombinedCapacity.merge(podId, capacityMetric, Double::sum); + } + return podByCombinedCapacity.entrySet() + .stream() + .sorted(Map.Entry.comparingByValue()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); + } + + + private Pair, Map> getOrderedClustersByCapacity(long id, long vmId, boolean isZone) { + double cpuToMemoryWeight = ConfigurationManager.HostCapacityTypeCpuMemoryWeight.value(); + short capacityType = getHostCapacityTypeToOrderCluster( + configDao.getValue(Config.HostCapacityTypeToOrderClusters.key()), cpuToMemoryWeight); + + logger.debug("CapacityType: {} is used for Cluster ordering", getCapacityTypeName(capacityType)); + if (capacityType >= 0) { // for capacityType other than COMBINED + return capacityDao.orderClustersByAggregateCapacity(id, vmId, capacityType, isZone); + } + + Long zoneId = isZone ? id : null; + Long podId = isZone ? null : id; + List capacities = capacityDao.listClusterCapacityByCapacityTypes(zoneId, podId, + List.of(Capacity.CAPACITY_TYPE_CPU, Capacity.CAPACITY_TYPE_MEMORY)); + + Map clusterByCombinedCapacities = getClusterByCombinedCapacities(capacities, cpuToMemoryWeight); + return new Pair<>(new ArrayList<>(clusterByCombinedCapacities.keySet()), clusterByCombinedCapacities); + } + + public static String getCapacityTypeName(short capacityType) { + switch (capacityType) { + case 0: return ApiConstants.RAM; + case 1: return ApiConstants.CPU; + case -1: return ApiConstants.COMBINED_CAPACITY_ORDERING; + default: return "UNKNOWN"; + } + } + + public Map getClusterByCombinedCapacities(List capacities, double cpuToMemoryWeight) { + Map clusterByCombinedCapacity = new HashMap<>(); + for (CapacityVO capacityVO : capacities) { + boolean isCPUCapacity = capacityVO.getCapacityType() == Capacity.CAPACITY_TYPE_CPU; + long clusterId = capacityVO.getClusterId(); + double applicableWeight = isCPUCapacity ? cpuToMemoryWeight : 1 - cpuToMemoryWeight; + String overCommitRatioParam = isCPUCapacity ? ApiConstants.CPU_OVERCOMMIT_RATIO : ApiConstants.MEMORY_OVERCOMMIT_RATIO; + ClusterDetailsVO overCommitRatioVO = clusterDetailsDao.findDetail(clusterId, overCommitRatioParam); + float overCommitRatio = Float.parseFloat(overCommitRatioVO.getValue()); + double capacityMetric = applicableWeight * + (capacityVO.getUsedCapacity() + capacityVO.getReservedCapacity())/(capacityVO.getTotalCapacity() * overCommitRatio); + clusterByCombinedCapacity.merge(clusterId, capacityMetric, Double::sum); + } + return clusterByCombinedCapacity.entrySet() + .stream() + .sorted(Map.Entry.comparingByValue()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); + } + + public static short getHostCapacityTypeToOrderCluster(String capacityTypeToOrder, double cpuToMemoryWeight) { + if (ApiConstants.RAM.equalsIgnoreCase(capacityTypeToOrder)) { + return CapacityVO.CAPACITY_TYPE_MEMORY; + } + if (ApiConstants.COMBINED_CAPACITY_ORDERING.equalsIgnoreCase(capacityTypeToOrder)) { + if (cpuToMemoryWeight == 1.0) { + return CapacityVO.CAPACITY_TYPE_CPU; + } + if (cpuToMemoryWeight == 0.0) { + return CapacityVO.CAPACITY_TYPE_MEMORY; + } + return -1; // represents COMBINED + } + return CapacityVO.CAPACITY_TYPE_CPU; + } + private void removeClustersWithoutMatchingTag(List clusterListForVmAllocation, String hostTagOnOffering) { List matchingClusters = hostDao.listClustersByHostTag(hostTagOnOffering); diff --git a/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java b/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java index 4524943db38..83498fbbe76 100644 --- a/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java +++ b/server/src/test/java/com/cloud/agent/manager/allocator/impl/FirstFitAllocatorTest.java @@ -19,6 +19,7 @@ package com.cloud.agent.manager.allocator.impl; import com.cloud.capacity.CapacityManager; +import com.cloud.capacity.CapacityVO; import com.cloud.deploy.DeploymentPlan; import com.cloud.deploy.DeploymentPlanner; import com.cloud.host.Host; @@ -28,12 +29,14 @@ import com.cloud.service.dao.ServiceOfferingDetailsDao; import com.cloud.user.Account; import com.cloud.utils.Pair; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -47,6 +50,7 @@ import static org.mockito.Mockito.when; public class FirstFitAllocatorTest { + private static final double TOLERANCE = 0.0001; private FirstFitAllocator allocator; private CapacityManager capacityMgr; private ServiceOfferingDetailsDao offeringDetailsDao; @@ -156,4 +160,62 @@ public class FirstFitAllocatorTest { assertTrue(result.isEmpty()); } + + @Test + public void testHostByCombinedCapacityOrder() { + // Test scenario 1: Default capacity usage (0.5 weight) + List mockCapacity = getHostCapacities(); + Map hostByCombinedCapacity = FirstFitAllocator.getHostByCombinedCapacities(mockCapacity, 0.5); + + // Verify host ordering and capacity values + Long firstHostId = hostByCombinedCapacity.keySet().iterator().next(); + Assert.assertEquals("Host with ID 1 should be first in ordering", Long.valueOf(1L), firstHostId); + Assert.assertEquals("Host 1 combined capacity should match expected value", + 0.9609375, hostByCombinedCapacity.get(1L), TOLERANCE); + Assert.assertEquals("Host 2 combined capacity should match expected value", + 0.9296875, hostByCombinedCapacity.get(2L), TOLERANCE); + + // Test scenario 2: Modified capacity usage (0.7 weight) + when(mockCapacity.get(0).getUsedCapacity()).thenReturn(1500L); + hostByCombinedCapacity = FirstFitAllocator.getHostByCombinedCapacities(mockCapacity, 0.7); + + // Verify new ordering after capacity change + firstHostId = hostByCombinedCapacity.keySet().iterator().next(); + Assert.assertEquals("Host with ID 2 should be first after capacity change", Long.valueOf(2L), firstHostId); + Assert.assertEquals("Host 2 combined capacity should match expected value after change", + 0.9515625, hostByCombinedCapacity.get(2L), TOLERANCE); + Assert.assertEquals("Host 1 combined capacity should match expected value after change", + 0.9484375, hostByCombinedCapacity.get(1L), TOLERANCE); + } + + List getHostCapacities() { + CapacityVO cpuCapacity1 = mock(CapacityVO.class); + when(cpuCapacity1.getHostOrPoolId()).thenReturn(1L); + when(cpuCapacity1.getTotalCapacity()).thenReturn(32000L); + when(cpuCapacity1.getReservedCapacity()).thenReturn(0L); + when(cpuCapacity1.getUsedCapacity()).thenReturn(500L); + when(cpuCapacity1.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_CPU); + + CapacityVO cpuCapacity2 = mock(CapacityVO.class); + when(cpuCapacity2.getHostOrPoolId()).thenReturn(2L); + when(cpuCapacity2.getTotalCapacity()).thenReturn(32000L); + when(cpuCapacity2.getReservedCapacity()).thenReturn(0L); + when(cpuCapacity2.getUsedCapacity()).thenReturn(500L); + when(cpuCapacity2.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_CPU); + + CapacityVO memCapacity1 = mock(CapacityVO.class); + when(memCapacity1.getHostOrPoolId()).thenReturn(1L); + when(memCapacity1.getTotalCapacity()).thenReturn(8589934592L); + when(memCapacity1.getReservedCapacity()).thenReturn(0L); + when(memCapacity1.getUsedCapacity()).thenReturn(536870912L); + when(memCapacity1.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_MEMORY); + + CapacityVO memCapacity2 = mock(CapacityVO.class); + when(memCapacity2.getHostOrPoolId()).thenReturn(2L); + when(memCapacity2.getTotalCapacity()).thenReturn(8589934592L); + when(memCapacity2.getReservedCapacity()).thenReturn(0L); + when(memCapacity2.getUsedCapacity()).thenReturn(1073741824L); + when(memCapacity1.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_MEMORY); + return Arrays.asList(cpuCapacity1, memCapacity1, cpuCapacity2, memCapacity2); + } } diff --git a/server/src/test/java/com/cloud/vm/FirstFitPlannerTest.java b/server/src/test/java/com/cloud/vm/FirstFitPlannerTest.java index 7df4857f4fd..517c2d2287b 100644 --- a/server/src/test/java/com/cloud/vm/FirstFitPlannerTest.java +++ b/server/src/test/java/com/cloud/vm/FirstFitPlannerTest.java @@ -17,6 +17,7 @@ package com.cloud.vm; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -30,6 +31,8 @@ import java.util.Map; import javax.inject.Inject; +import com.cloud.capacity.CapacityVO; +import com.cloud.dc.ClusterDetailsVO; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.framework.config.ConfigDepot; @@ -42,10 +45,10 @@ import org.apache.cloudstack.framework.config.impl.ConfigDepotImpl; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.test.utils.SpringUtils; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentMatchers; import org.mockito.Mockito; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -138,7 +141,9 @@ public class FirstFitPlannerTest { ScopedConfigStorage scopedStorage; @Inject HostDao hostDao; - + @Inject + private ClusterDetailsDao clusterDetailsDao; + private static final double TOLERANCE = 0.0001; private static long domainId = 1L; long dataCenterId = 1L; long accountId = 1L; @@ -241,6 +246,69 @@ public class FirstFitPlannerTest { assertTrue("Reordered cluster list does not have clusters exceeding threshold", (clusterList.containsAll(clustersCrossingThreshold))); } + + @Test + public void testGetClusterOrderCapacityType() { + Assert.assertEquals(1, FirstFitPlanner.getHostCapacityTypeToOrderCluster("CPU", 0.5)); + Assert.assertEquals(0, FirstFitPlanner.getHostCapacityTypeToOrderCluster("RAM", 0.5)); + String combinedOrder = "COMBINED"; + Assert.assertEquals(1, FirstFitPlanner.getHostCapacityTypeToOrderCluster(combinedOrder, 1)); // cputomemoryweight:1 -> CPU + Assert.assertEquals(0, FirstFitPlanner.getHostCapacityTypeToOrderCluster(combinedOrder, 0)); // cputomemoryweight: 0 -> RAM + Assert.assertEquals(-1, FirstFitPlanner.getHostCapacityTypeToOrderCluster(combinedOrder, 0.5)); + } + + @Test + public void testGetPodByCombinedCapacities() { + List mockCapacity = getPodCapacities(); + ClusterDetailsVO clusterDetailsOverCommitRatio = mock(ClusterDetailsVO.class); + when(clusterDetailsOverCommitRatio.getValue()).thenReturn("1.0"); + when(clusterDetailsDao.findDetail(anyLong(), anyString())).thenReturn(clusterDetailsOverCommitRatio); + + Map podByCombinedCapacity = planner.getPodByCombinedCapacities(mockCapacity, 0.5); + Long firstPodId = podByCombinedCapacity.keySet().iterator().next(); + Assert.assertEquals("Pod with ID 1 should be first in ordering", Long.valueOf(1L), firstPodId); + Assert.assertEquals("Pod 1 combined capacity should match expected value", + 0.0390625, podByCombinedCapacity.get(1L), TOLERANCE); + Assert.assertEquals("Pod 2 combined capacity should match expected value", + 0.0703125, podByCombinedCapacity.get(2L), TOLERANCE); + + // Test scenario 2: Modified capacity usage (0.7 weight) + when(mockCapacity.get(0).getUsedCapacity()).thenReturn(1500L); + podByCombinedCapacity = planner.getPodByCombinedCapacities(mockCapacity, 0.7); + firstPodId = podByCombinedCapacity.keySet().iterator().next(); + Assert.assertEquals("Pod with ID 2 should be first in ordering", Long.valueOf(2L), firstPodId); + Assert.assertEquals("Pod 2 combined capacity should match expected value", + 0.04843750, podByCombinedCapacity.get(2L), TOLERANCE); + Assert.assertEquals("Pod 1 combined capacity should match expected value", + 0.05156250, podByCombinedCapacity.get(1L), TOLERANCE); + } + + @Test + public void testGetClusterByCombinedCapacities() { + List mockCapacity = getClusterCapacities(); + ClusterDetailsVO clusterDetailsOverCommitRatio = mock(ClusterDetailsVO.class); + when(clusterDetailsOverCommitRatio.getValue()).thenReturn("1.0"); + when(clusterDetailsDao.findDetail(anyLong(), anyString())).thenReturn(clusterDetailsOverCommitRatio); + + Map clusterByCombinedCapacity = planner.getClusterByCombinedCapacities(mockCapacity, 0.5); + Long firstClusterId = clusterByCombinedCapacity.keySet().iterator().next(); + Assert.assertEquals("Cluster with ID 1 should be first in ordering", Long.valueOf(1L), firstClusterId); + Assert.assertEquals("Cluster 1 combined capacity should match expected value", + 0.046875, clusterByCombinedCapacity.get(1L), TOLERANCE); + Assert.assertEquals("Cluster 2 combined capacity should match expected value", + 0.07421875, clusterByCombinedCapacity.get(2L), TOLERANCE); + + // Test scenario 2: Modified capacity usage (0.7 weight) + when(mockCapacity.get(0).getUsedCapacity()).thenReturn(2000L); + clusterByCombinedCapacity = planner.getClusterByCombinedCapacities(mockCapacity, 0.7); + firstClusterId = clusterByCombinedCapacity.keySet().iterator().next(); + Assert.assertEquals("Cluster with ID 2 should be first in ordering", Long.valueOf(2L), firstClusterId); + Assert.assertEquals("Cluster 2 combined capacity should match expected value", + 0.05390625, clusterByCombinedCapacity.get(2L), TOLERANCE); + Assert.assertEquals("Cluster 1 combined capacity should match expected value", + 0.0625, clusterByCombinedCapacity.get(1L), TOLERANCE); + } + private List initializeForClusterThresholdDisabled() { when(configDepot.getConfigStringValue(DeploymentClusterPlanner.ClusterThresholdEnabled.key(), ConfigKey.Scope.Global, null)).thenReturn(Boolean.FALSE.toString()); @@ -293,7 +361,7 @@ public class FirstFitPlannerTest { when( capacityDao.listClustersInZoneOrPodByHostCapacities(dataCenterId, 12L, noOfCpusInOffering * cpuSpeedInOffering, ramInOffering * 1024L * 1024L, - Capacity.CAPACITY_TYPE_CPU, true)).thenReturn(clustersWithEnoughCapacity); + true)).thenReturn(clustersWithEnoughCapacity); Map clusterCapacityMap = new HashMap(); clusterCapacityMap.put(1L, 2048D); @@ -327,7 +395,7 @@ public class FirstFitPlannerTest { hostList6.add(new Long(15)); String[] implicitHostTags = {"GPU"}; int ramInBytes = ramInOffering * 1024 * 1024; - when(serviceOfferingDetailsDao.findDetail(ArgumentMatchers.anyLong(), anyString())).thenReturn(null); + when(serviceOfferingDetailsDao.findDetail(anyLong(), anyString())).thenReturn(null); when(hostGpuGroupsDao.listHostIds()).thenReturn(hostList0); when(capacityDao.listHostsWithEnoughCapacity(noOfCpusInOffering * cpuSpeedInOffering, ramInBytes, new Long(1), Host.Type.Routing.toString())).thenReturn(hostList1); when(capacityDao.listHostsWithEnoughCapacity(noOfCpusInOffering * cpuSpeedInOffering, ramInBytes, new Long(2), Host.Type.Routing.toString())).thenReturn(hostList2); @@ -505,4 +573,70 @@ public class FirstFitPlannerTest { } } } + + List getClusterCapacities() { + CapacityVO cpuCapacity1 = mock(CapacityVO.class); + when(cpuCapacity1.getClusterId()).thenReturn(1L); + when(cpuCapacity1.getTotalCapacity()).thenReturn(32000L); + when(cpuCapacity1.getReservedCapacity()).thenReturn(0L); + when(cpuCapacity1.getUsedCapacity()).thenReturn(1000L); + when(cpuCapacity1.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_CPU); + + CapacityVO cpuCapacity2 = mock(CapacityVO.class); + when(cpuCapacity2.getClusterId()).thenReturn(2L); + when(cpuCapacity2.getTotalCapacity()).thenReturn(32000L); + when(cpuCapacity2.getReservedCapacity()).thenReturn(0L); + when(cpuCapacity2.getUsedCapacity()).thenReturn(750L); + when(cpuCapacity2.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_CPU); + + CapacityVO memCapacity1 = mock(CapacityVO.class); + when(memCapacity1.getClusterId()).thenReturn(1L); + when(memCapacity1.getTotalCapacity()).thenReturn(8589934592L); + when(memCapacity1.getReservedCapacity()).thenReturn(0L); + when(memCapacity1.getUsedCapacity()).thenReturn(536870912L); + when(memCapacity1.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_MEMORY); + + CapacityVO memCapacity2 = mock(CapacityVO.class); + when(memCapacity2.getClusterId()).thenReturn(2L); + when(memCapacity2.getTotalCapacity()).thenReturn(8589934592L); + when(memCapacity2.getReservedCapacity()).thenReturn(0L); + when(memCapacity2.getUsedCapacity()).thenReturn(1073741824L); + when(memCapacity2.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_MEMORY); + return Arrays.asList(cpuCapacity1, memCapacity1, cpuCapacity2, memCapacity2); + } + + List getPodCapacities() { + CapacityVO cpuCapacity1 = mock(CapacityVO.class); + when(cpuCapacity1.getPodId()).thenReturn(1L); + when(cpuCapacity1.getClusterId()).thenReturn(1L); + when(cpuCapacity1.getTotalCapacity()).thenReturn(32000L); + when(cpuCapacity1.getReservedCapacity()).thenReturn(0L); + when(cpuCapacity1.getUsedCapacity()).thenReturn(500L); + when(cpuCapacity1.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_CPU); + + CapacityVO cpuCapacity2 = mock(CapacityVO.class); + when(cpuCapacity2.getPodId()).thenReturn(2L); + when(cpuCapacity2.getClusterId()).thenReturn(1L); + when(cpuCapacity2.getTotalCapacity()).thenReturn(32000L); + when(cpuCapacity2.getReservedCapacity()).thenReturn(0L); + when(cpuCapacity2.getUsedCapacity()).thenReturn(500L); + when(cpuCapacity2.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_CPU); + + CapacityVO memCapacity1 = mock(CapacityVO.class); + when(memCapacity1.getPodId()).thenReturn(1L); + when(memCapacity1.getClusterId()).thenReturn(1L); + when(memCapacity1.getTotalCapacity()).thenReturn(8589934592L); + when(memCapacity1.getReservedCapacity()).thenReturn(0L); + when(memCapacity1.getUsedCapacity()).thenReturn(536870912L); + when(memCapacity1.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_MEMORY); + + CapacityVO memCapacity2 = mock(CapacityVO.class); + when(memCapacity2.getPodId()).thenReturn(2L); + when(memCapacity2.getClusterId()).thenReturn(1L); + when(memCapacity2.getTotalCapacity()).thenReturn(8589934592L); + when(memCapacity2.getReservedCapacity()).thenReturn(0L); + when(memCapacity2.getUsedCapacity()).thenReturn(1073741824L); + when(memCapacity2.getCapacityType()).thenReturn(CapacityVO.CAPACITY_TYPE_MEMORY); + return Arrays.asList(cpuCapacity1, memCapacity1, cpuCapacity2, memCapacity2); + } } From 9688cbb0953494346a519a661c7bb4374688cb7d Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Tue, 15 Jul 2025 20:09:41 +0800 Subject: [PATCH 18/94] systemvm: build 4.20.2 template with 'depmod -a' (#11128) --- pom.xml | 2 +- tools/appliance/systemvmtemplate/scripts/finalize.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index af0ac94a1f2..17385ab5a8c 100644 --- a/pom.xml +++ b/pom.xml @@ -50,7 +50,7 @@ UTF-8 UTF-8 https://download.cloudstack.org/systemvm - 4.20.1.0 + 4.20.2.0 apache https://sonarcloud.io diff --git a/tools/appliance/systemvmtemplate/scripts/finalize.sh b/tools/appliance/systemvmtemplate/scripts/finalize.sh index e5d15ecb61c..507d4a4133a 100644 --- a/tools/appliance/systemvmtemplate/scripts/finalize.sh +++ b/tools/appliance/systemvmtemplate/scripts/finalize.sh @@ -68,6 +68,7 @@ function zero_disk() { } function finalize() { + depmod -a configure_misc configure_rundisk_size configure_sudoers From f52e05863e608f53f9e97e93fe47577b8dfe189b Mon Sep 17 00:00:00 2001 From: Vishesh Date: Tue, 15 Jul 2025 18:19:46 +0530 Subject: [PATCH 19/94] UI fix api in project view (#11191) * Add project id for post requests as well in the params * Replace leftover api calls to getAPI calls * ui: don't remove values from request if the value is null or empty string * Address comments * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Suresh Kumar Anaparti * fixup * Return null if guiTheme requests fails --------- Co-authored-by: Suresh Kumar Anaparti --- ui/src/api/index.js | 2 +- .../view/ImageDeployInstanceButton.vue | 6 ++-- ui/src/main.js | 4 +-- ui/src/utils/guiTheme.js | 7 ++-- ui/src/utils/request.js | 34 +++++++++++++++++-- ui/src/views/compute/KubernetesAddNodes.vue | 6 ++-- .../views/compute/KubernetesRemoveNodes.vue | 4 +-- 7 files changed, 47 insertions(+), 16 deletions(-) diff --git a/ui/src/api/index.js b/ui/src/api/index.js index 45938df6cab..1f532c36336 100644 --- a/ui/src/api/index.js +++ b/ui/src/api/index.js @@ -47,7 +47,7 @@ export function postAPI (command, data = {}) { params.append('response', 'json') if (data) { Object.entries(data).forEach(([key, value]) => { - if (value !== undefined && value !== null && value !== '') { + if (value !== undefined && value !== null) { params.append(key, value) } }) diff --git a/ui/src/components/view/ImageDeployInstanceButton.vue b/ui/src/components/view/ImageDeployInstanceButton.vue index 4f632cc0383..b2d4b55bc6a 100644 --- a/ui/src/components/view/ImageDeployInstanceButton.vue +++ b/ui/src/components/view/ImageDeployInstanceButton.vue @@ -41,7 +41,7 @@ + + From bf464585785b6c0848fb4b07953e12d1039af4ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernardo=20De=20Marco=20Gon=C3=A7alves?= Date: Thu, 17 Jul 2025 04:01:49 -0300 Subject: [PATCH 26/94] List templates and ISOs by domain (#11179) --- .../com/cloud/api/query/QueryManagerImpl.java | 28 +++++++++++++------ ui/src/components/view/InfoCard.vue | 3 ++ ui/src/config/section/domain.js | 5 ++++ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index d0f6fc0b16d..1d8e8687051 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -29,6 +29,7 @@ import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -4443,6 +4444,8 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q boolean showRemovedTmpl = cmd.getShowRemoved(); Account caller = CallContext.current().getCallingAccount(); Long parentTemplateId = cmd.getParentTemplateId(); + Long domainId = cmd.getDomainId(); + boolean isRecursive = cmd.isRecursive(); boolean listAll = false; if (templateFilter != null && templateFilter == TemplateFilter.all) { @@ -4453,7 +4456,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q } List permittedAccountIds = new ArrayList(); - Ternary domainIdRecursiveListProject = new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null); + Ternary domainIdRecursiveListProject = new Ternary<>(domainId, isRecursive, null); accountMgr.buildACLSearchParameters( caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccountIds, domainIdRecursiveListProject, listAll, false @@ -4481,7 +4484,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q null, cmd.getPageSizeVal(), cmd.getStartIndex(), cmd.getZoneId(), cmd.getStoragePoolId(), cmd.getImageStoreId(), hypervisorType, showDomr, cmd.listInReadyState(), permittedAccounts, caller, listProjectResourcesCriteria, tags, showRemovedTmpl, cmd.getIds(), parentTemplateId, cmd.getShowUnique(), - templateType, isVnf); + templateType, isVnf, domainId, isRecursive); } private Pair, Integer> searchForTemplatesInternal(Long templateId, String name, String keyword, @@ -4490,7 +4493,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q boolean showDomr, boolean onlyReady, List permittedAccounts, Account caller, ListProjectResourcesCriteria listProjectResourcesCriteria, Map tags, boolean showRemovedTmpl, List ids, Long parentTemplateId, Boolean showUnique, String templateType, - Boolean isVnf) { + Boolean isVnf, Long domainId, boolean isRecursive) { // check if zone is configured, if not, just return empty list List hypers = null; @@ -4572,7 +4575,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q if (!permittedAccounts.isEmpty()) { domain = _domainDao.findById(permittedAccounts.get(0).getDomainId()); } else { - domain = _domainDao.findById(caller.getDomainId()); + domain = _domainDao.findById(Objects.requireNonNullElse(domainId, caller.getDomainId())); } setIdsListToSearchCriteria(sc, ids); @@ -4584,10 +4587,14 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q sc.addAnd("accountType", SearchCriteria.Op.EQ, Account.Type.PROJECT); } - // add criteria for domain path in case of domain admin + // add criteria for domain path in case of admins if ((templateFilter == TemplateFilter.self || templateFilter == TemplateFilter.selfexecutable) - && (caller.getType() == Account.Type.DOMAIN_ADMIN || caller.getType() == Account.Type.RESOURCE_DOMAIN_ADMIN)) { - sc.addAnd("domainPath", SearchCriteria.Op.LIKE, domain.getPath() + "%"); + && (accountMgr.isAdmin(caller.getAccountId()))) { + if (isRecursive) { + sc.addAnd("domainPath", SearchCriteria.Op.LIKE, domain.getPath() + "%"); + } else { + sc.addAnd("domainPath", SearchCriteria.Op.EQ, domain.getPath()); + } } List relatedDomainIds = new ArrayList(); @@ -4893,6 +4900,8 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q Map tags = cmd.getTags(); boolean showRemovedISO = cmd.getShowRemoved(); Account caller = CallContext.current().getCallingAccount(); + Long domainId = cmd.getDomainId(); + boolean isRecursive = cmd.isRecursive(); boolean listAll = false; if (isoFilter != null && isoFilter == TemplateFilter.all) { @@ -4904,7 +4913,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q List permittedAccountIds = new ArrayList<>(); - Ternary domainIdRecursiveListProject = new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null); + Ternary domainIdRecursiveListProject = new Ternary<>(domainId, isRecursive, null); accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccountIds, domainIdRecursiveListProject, listAll, false); ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); List permittedAccounts = new ArrayList<>(); @@ -4917,7 +4926,8 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q return searchForTemplatesInternal(cmd.getId(), cmd.getIsoName(), cmd.getKeyword(), isoFilter, true, cmd.isBootable(), cmd.getPageSizeVal(), cmd.getStartIndex(), cmd.getZoneId(), cmd.getStoragePoolId(), cmd.getImageStoreId(), hypervisorType, true, cmd.listInReadyState(), permittedAccounts, caller, listProjectResourcesCriteria, - tags, showRemovedISO, null, null, cmd.getShowUnique(), null, null); + tags, showRemovedISO, null, null, cmd.getShowUnique(), null, null, + domainId, isRecursive); } @Override diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue index 00cb4748a88..e59735c51ea 100644 --- a/ui/src/components/view/InfoCard.vue +++ b/ui/src/components/view/InfoCard.vue @@ -1162,6 +1162,9 @@ export default { if (item.name === 'template') { query.templatefilter = 'self' query.filter = 'self' + } else if (item.name === 'iso') { + query.isofilter = 'self' + query.filter = 'self' } if (item.param === 'account') { diff --git a/ui/src/config/section/domain.js b/ui/src/config/section/domain.js index e6807f06278..fbe20ef8891 100644 --- a/ui/src/config/section/domain.js +++ b/ui/src/config/section/domain.js @@ -48,6 +48,11 @@ export default { name: 'template', title: 'label.templates', param: 'domainid' + }, + { + name: 'iso', + title: 'label.isos', + param: 'domainid' }], tabs: [ { From 15c09af5cc7b4fbfee2e295ae2bd4e8b51fb57d3 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 17 Jul 2025 03:31:28 -0400 Subject: [PATCH 27/94] UI: Fix traffic Label on Zone creation wizard for VMware (#11101) --- .../views/infra/zone/ZoneWizardPhysicalNetworkSetupStep.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/src/views/infra/zone/ZoneWizardPhysicalNetworkSetupStep.vue b/ui/src/views/infra/zone/ZoneWizardPhysicalNetworkSetupStep.vue index 1117cb6ec01..e4354a6d3b0 100644 --- a/ui/src/views/infra/zone/ZoneWizardPhysicalNetworkSetupStep.vue +++ b/ui/src/views/infra/zone/ZoneWizardPhysicalNetworkSetupStep.vue @@ -79,14 +79,14 @@