This is an automated email from the ASF dual-hosted git repository. sureshanaparti pushed a commit to branch 4.22 in repository https://gitbox.apache.org/repos/asf/cloudstack.git
commit d700e2db643c6d1adbda82957f21a6a8b696f704 Merge: a8f1e4a5bac 26b57655ece Author: Suresh Kumar Anaparti <[email protected]> AuthorDate: Thu Jan 29 15:51:58 2026 +0530 Merge branch '4.20' into 4.22 .../org/apache/cloudstack/alert/AlertService.java | 4 +- .../command/admin/host/AddSecondaryStorageCmd.java | 24 ++- .../command/user/snapshot/CreateSnapshotCmd.java | 3 +- .../service/StorageOrchestrationService.java | 3 +- .../subsystem/api/storage/TemplateService.java | 4 +- .../main/java/com/cloud/alert/AlertManager.java | 1 - .../java/com/cloud/resource/ResourceManager.java | 4 +- .../java/com/cloud/storage/StorageManager.java | 5 +- .../engine/orchestration/StorageOrchestrator.java | 45 +++-- .../main/java/com/cloud/usage/dao/UsageDao.java | 2 +- .../java/com/cloud/usage/dao/UsageDaoImpl.java | 37 ++-- .../main/java/com/cloud/usage/dao/UsageJobDao.java | 2 + .../java/com/cloud/usage/dao/UsageJobDaoImpl.java | 3 +- .../storage/datastore/db/SnapshotDataStoreDao.java | 14 ++ .../datastore/db/SnapshotDataStoreDaoImpl.java | 42 +++- .../resources/META-INF/db/schema-42020to42030.sql | 6 + .../resources/META-INF/db/schema-42200to42210.sql | 6 + .../storage/image/TemplateServiceImpl.java | 157 ++++++++++++-- .../storage/image/TemplateServiceImplTest.java | 171 +++++++++++++++- .../src/main/java/com/cloud/utils/db/Filter.java | 3 +- .../java/com/cloud/utils/db/GenericDaoBase.java | 4 +- .../LibvirtUpdateHostPasswordCommandWrapper.java | 3 +- .../CitrixUpdateHostPasswordCommandWrapper.java | 2 +- .../configuration/ConfigurationManagerImpl.java | 2 +- .../deploy/DeploymentPlanningManagerImpl.java | 82 ++++---- .../main/java/com/cloud/event/AlertGenerator.java | 5 +- .../com/cloud/ha/HighAvailabilityManagerImpl.java | 4 +- .../com/cloud/resource/ResourceManagerImpl.java | 5 +- .../resourcelimit/ResourceLimitManagerImpl.java | 10 +- .../com/cloud/storage/ImageStoreDetailsUtil.java | 11 + .../java/com/cloud/storage/StorageManagerImpl.java | 2 +- .../storage/snapshot/SnapshotManagerImpl.java | 30 +-- .../com/cloud/template/TemplateManagerImpl.java | 14 +- .../java/com/cloud/usage/UsageServiceImpl.java | 3 +- .../cloudstack/backup/BackupManagerImpl.java | 8 +- .../cloud/resource/ResourceManagerImplTest.java | 14 +- .../ResourceLimitManagerImplTest.java | 10 +- ui/public/locales/en.json | 4 +- ui/src/components/view/DedicateDomain.vue | 129 +++++------- ui/src/components/widgets/InfiniteScrollSelect.vue | 91 ++++++++- ui/src/views/iam/AddUser.vue | 121 ++++------- ui/src/views/infra/AddSecondaryStorage.vue | 82 +++++++- ui/src/views/infra/UsageRecords.vue | 112 +++++----- ui/src/views/infra/zone/ZoneWizardAddResources.vue | 25 ++- ui/src/views/infra/zone/ZoneWizardLaunchZone.vue | 5 + ui/src/views/storage/CreateTemplate.vue | 111 ++++------ ui/src/views/storage/UploadLocalVolume.vue | 225 ++++++++------------- ui/src/views/storage/UploadVolume.vue | 218 ++++++++------------ ui/src/views/tools/CreateWebhook.vue | 126 ++++-------- ui/src/views/tools/ManageVolumes.vue | 157 ++++++-------- .../java/com/cloud/usage/UsageManagerImpl.java | 5 + .../main/java/com/cloud/utils/script/Script.java | 99 +++++---- .../main/java/com/cloud/utils/ssh/SshHelper.java | 73 ++++++- .../java/com/cloud/utils/script/ScriptTest.java | 30 +++ .../java/com/cloud/utils/ssh/SshHelperTest.java | 60 ++++++ 55 files changed, 1471 insertions(+), 947 deletions(-) diff --cc api/src/main/java/org/apache/cloudstack/alert/AlertService.java index d8e471756a0,4ae6288efce..14223227c34 --- a/api/src/main/java/org/apache/cloudstack/alert/AlertService.java +++ b/api/src/main/java/org/apache/cloudstack/alert/AlertService.java @@@ -71,11 -71,8 +71,11 @@@ public interface AlertService public static final AlertType ALERT_TYPE_HA_ACTION = new AlertType((short)30, "ALERT.HA.ACTION", true); public static final AlertType ALERT_TYPE_CA_CERT = new AlertType((short)31, "ALERT.CA.CERT", true); public static final AlertType ALERT_TYPE_VM_SNAPSHOT = new AlertType((short)32, "ALERT.VM.SNAPSHOT", true); - public static final AlertType ALERT_TYPE_VR_PUBLIC_IFACE_MTU = new AlertType((short)32, "ALERT.VR.PUBLIC.IFACE.MTU", true); - public static final AlertType ALERT_TYPE_VR_PRIVATE_IFACE_MTU = new AlertType((short)32, "ALERT.VR.PRIVATE.IFACE.MTU", true); + public static final AlertType ALERT_TYPE_VR_PUBLIC_IFACE_MTU = new AlertType((short)33, "ALERT.VR.PUBLIC.IFACE.MTU", true); + public static final AlertType ALERT_TYPE_VR_PRIVATE_IFACE_MTU = new AlertType((short)34, "ALERT.VR.PRIVATE.IFACE.MTU", true); + public static final AlertType ALERT_TYPE_EXTENSION_PATH_NOT_READY = new AlertType((short)33, "ALERT.TYPE.EXTENSION.PATH.NOT.READY", true); + public static final AlertType ALERT_TYPE_BACKUP_STORAGE = new AlertType(Capacity.CAPACITY_TYPE_BACKUP_STORAGE, "ALERT.STORAGE.BACKUP", true); + public static final AlertType ALERT_TYPE_OBJECT_STORAGE = new AlertType(Capacity.CAPACITY_TYPE_OBJECT_STORAGE, "ALERT.STORAGE.OBJECT", true); public short getType() { return type; diff --cc api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java index 3a49bad8fcb,078d4517f95..1b9d3c59e5a --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java @@@ -270,9 -243,8 +270,8 @@@ public class CreateSnapshotCmd extends } } - private Snapshot.LocationType getLocationType() { + public Snapshot.LocationType getLocationType() { - - if (Snapshot.LocationType.values() == null || Snapshot.LocationType.values().length == 0 || locationType == null) { + if (locationType == null) { return null; } diff --cc engine/components-api/src/main/java/com/cloud/storage/StorageManager.java index 3e3901cb293,4ce1f4a9638..5c7348cbe6c --- a/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java +++ b/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java @@@ -220,16 -220,9 +220,17 @@@ public interface StorageManager extend "storage.pool.host.connect.workers", "1", "Number of worker threads to be used to connect hosts to a primary storage", true); + ConfigKey<Float> ObjectStorageCapacityThreshold = new ConfigKey<>("Alert", Float.class, + "objectStorage.capacity.notificationthreshold", + "0.75", + "Percentage (as a value between 0 and 1) of object storage utilization above which alerts will be sent about low storage available.", + true, + ConfigKey.Scope.Global, + null); + - ConfigKey<Boolean> COPY_PUBLIC_TEMPLATES_FROM_OTHER_STORAGES = new ConfigKey<>(Boolean.class, "copy.public.templates.from.other.storages", - "Storage", "true", "Allow SSVMs to try copying public templates from one secondary storage to another instead of downloading them from the source.", + ConfigKey<Boolean> COPY_TEMPLATES_FROM_OTHER_SECONDARY_STORAGES = new ConfigKey<>(Boolean.class, "copy.templates.from.other.secondary.storages", + "Storage", "true", "When enabled, this feature allows templates to be copied from existing Secondary Storage servers (within the same zone or across zones) " + + "while adding a new Secondary Storage. If the copy operation fails, the system falls back to downloading the template from the source URL.", true, ConfigKey.Scope.Zone, null); /** diff --cc engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java index ac9601389bd,c68316dd1fe..cdf903407c1 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java @@@ -92,10 -78,15 +92,19 @@@ public class SnapshotDataStoreDaoImpl e " order by created %s " + " limit 1"; + private static final String FIND_SNAPSHOT_IN_ZONE = "SELECT ssr.* FROM " + + "snapshot_store_ref ssr, snapshots s " + + "WHERE ssr.snapshot_id=? AND ssr.snapshot_id = s.id AND s.data_center_id=?;"; + + private static final String GET_PHYSICAL_SIZE_OF_SNAPSHOTS_ON_PRIMARY_BY_ACCOUNT = "SELECT SUM(s.physical_size) " + + "FROM cloud.snapshot_store_ref s " + + "LEFT JOIN cloud.snapshots ON s.snapshot_id = snapshots.id " + + "WHERE snapshots.account_id = ? " + + "AND snapshots.removed IS NULL " + + "AND s.state = 'Ready' " + + "AND s.store_role = 'Primary' " + + "AND NOT EXISTS (SELECT 1 FROM cloud.snapshot_store_ref i WHERE i.snapshot_id = s.snapshot_id AND i.store_role = 'Image')"; + @Override public boolean configure(String name, Map<String, Object> params) throws ConfigurationException { super.configure(name, params); @@@ -136,11 -127,10 +145,11 @@@ stateSearch.and(STATE, stateSearch.entity().getState(), SearchCriteria.Op.IN); stateSearch.done(); + - idStateNinSearch = createSearchBuilder(); - idStateNinSearch.and(SNAPSHOT_ID, idStateNinSearch.entity().getSnapshotId(), SearchCriteria.Op.EQ); - idStateNinSearch.and(STATE, idStateNinSearch.entity().getState(), SearchCriteria.Op.NOTIN); - idStateNinSearch.done(); + idStateNeqSearch = createSearchBuilder(); + idStateNeqSearch.and(SNAPSHOT_ID, idStateNeqSearch.entity().getSnapshotId(), SearchCriteria.Op.EQ); + idStateNeqSearch.and(STATE, idStateNeqSearch.entity().getState(), SearchCriteria.Op.NEQ); + idStateNeqSearch.done(); snapshotVOSearch = snapshotDao.createSearchBuilder(); snapshotVOSearch.and(VOLUME_ID, snapshotVOSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); @@@ -471,17 -355,9 +480,17 @@@ @Override public List<SnapshotDataStoreVO> findBySnapshotIdWithNonDestroyedState(long snapshotId) { - SearchCriteria<SnapshotDataStoreVO> sc = idStateNinSearch.create(); + SearchCriteria<SnapshotDataStoreVO> sc = idStateNeqSearch.create(); sc.setParameters(SNAPSHOT_ID, snapshotId); - sc.setParameters(STATE, State.Destroyed); + sc.setParameters(STATE, State.Destroyed.name()); + return listBy(sc); + } + + @Override + public List<SnapshotDataStoreVO> findBySnapshotIdAndNotInDestroyedHiddenState(long snapshotId) { - SearchCriteria<SnapshotDataStoreVO> sc = idStateNinSearch.create(); ++ SearchCriteria<SnapshotDataStoreVO> sc = idStateNeqSearch.create(); + sc.setParameters(SNAPSHOT_ID, snapshotId); + sc.setParameters(STATE, State.Destroyed.name(), State.Hidden.name()); return listBy(sc); } diff --cc engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql index f5543756ed6,00000000000..a8a3d3f7bd4 mode 100644,000000..100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql @@@ -1,29 -1,0 +1,35 @@@ +-- 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. + +--; +-- Schema upgrade from 4.22.0.0 to 4.22.1.0 +--; + +-- Add vm_id column to usage_event table for volume usage events +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.usage_event','vm_id', 'bigint UNSIGNED NULL COMMENT "VM ID associated with volume usage events"'); +CALL `cloud_usage`.`IDEMPOTENT_ADD_COLUMN`('cloud_usage.usage_event','vm_id', 'bigint UNSIGNED NULL COMMENT "VM ID associated with volume usage events"'); + +-- Add vm_id column to cloud_usage.usage_volume table +CALL `cloud_usage`.`IDEMPOTENT_ADD_COLUMN`('cloud_usage.usage_volume','vm_id', 'bigint UNSIGNED NULL COMMENT "VM ID associated with the volume usage"'); + +ALTER TABLE `cloud`.`template_store_ref` MODIFY COLUMN `download_url` varchar(2048); ++ ++UPDATE `cloud`.`alert` SET type = 33 WHERE name = 'ALERT.VR.PUBLIC.IFACE.MTU'; ++UPDATE `cloud`.`alert` SET type = 34 WHERE name = 'ALERT.VR.PRIVATE.IFACE.MTU'; ++ ++-- Update configuration 'kvm.ssh.to.agent' description and is_dynamic fields ++UPDATE `cloud`.`configuration` SET description = 'True if the management server will restart the agent service via SSH into the KVM hosts after or during maintenance operations', is_dynamic = 1 WHERE name = 'kvm.ssh.to.agent'; diff --cc server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java index bb165e0529d,6881fbab98c..230f97f6fd3 --- a/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java +++ b/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java @@@ -36,7 -36,7 +36,8 @@@ import java.util.stream.Collectors import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.gpu.dao.VgpuProfileDao; + import com.cloud.resource.ResourceState; import org.apache.cloudstack.affinity.AffinityGroupDomainMapVO; import org.apache.cloudstack.affinity.AffinityGroupProcessor; import org.apache.cloudstack.affinity.AffinityGroupService; diff --cc server/src/main/java/com/cloud/storage/StorageManagerImpl.java index 6f289f8e6bb,d1dca0fa901..9f9928bfb66 --- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java @@@ -4613,8 -4206,7 +4613,8 @@@ public class StorageManagerImpl extend DataStoreDownloadFollowRedirects, AllowVolumeReSizeBeyondAllocation, StoragePoolHostConnectWorkers, + ObjectStorageCapacityThreshold, - COPY_PUBLIC_TEMPLATES_FROM_OTHER_STORAGES + COPY_TEMPLATES_FROM_OTHER_SECONDARY_STORAGES }; } diff --cc server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java index 886feea19f2,19cde4da0f1..900c59abb66 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@@ -1688,16 -1479,12 +1695,17 @@@ public class SnapshotManagerImpl extend UsageEventUtils.publishUsageEvent(EventTypes.EVENT_SNAPSHOT_CREATE, snapshot.getAccountId(), snapshot.getDataCenterId(), snapshotId, snapshot.getName(), null, null, snapshotStoreRef.getPhysicalSize(), volume.getSize(), snapshot.getClass().getName(), snapshot.getUuid()); + ResourceType storeResourceType = dataStoreRole == DataStoreRole.Image ? ResourceType.secondary_storage : ResourceType.primary_storage; // Correct the resource count of snapshot in case of delta snapshots. - _resourceLimitMgr.decrementResourceCount(snapshotOwner.getId(), ResourceType.secondary_storage, new Long(volume.getSize() - snapshotStoreRef.getPhysicalSize())); + _resourceLimitMgr.decrementResourceCount(snapshotOwner.getId(), storeResourceType, new Long(volume.getSize() - snapshotStoreRef.getPhysicalSize())); - if (!payload.getAsyncBackup() && backupSnapToSecondary) { - copyNewSnapshotToZones(snapshotId, snapshot.getDataCenterId(), payload.getZoneIds()); + if (!payload.getAsyncBackup()) { + if (backupSnapToSecondary) { + copyNewSnapshotToZones(snapshotId, snapshot.getDataCenterId(), payload.getZoneIds()); + } + if (CollectionUtils.isNotEmpty(payload.getStoragePoolIds())) { + copyNewSnapshotToZonesOnPrimary(payload, snapshot); + } } } catch (Exception e) { logger.debug("post process snapshot failed", e); diff --cc server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java index 7e60c111ab2,1669d7a47d9..47472408404 --- a/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java +++ b/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java @@@ -55,11 -45,8 +55,12 @@@ import com.cloud.vm.dao.VMInstanceDao import com.trilead.ssh2.Connection; import org.apache.cloudstack.api.command.admin.host.CancelHostAsDegradedCmd; import org.apache.cloudstack.api.command.admin.host.DeclareHostAsDegradedCmd; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo; + import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.junit.After; import org.junit.Assert; import org.junit.Before; diff --cc server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java index a968a2da0b7,53ccc830dd2..e04ccc0ca13 --- a/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java +++ b/server/src/test/java/com/cloud/resourcelimit/ResourceLimitManagerImplTest.java @@@ -31,9 -26,9 +31,10 @@@ import org.apache.cloudstack.api.ApiCom import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.TaggedResourceLimitAndCountResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.reservation.dao.ReservationDao; + import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@@ -128,9 -120,8 +129,11 @@@ public class ResourceLimitManagerImplTe @Mock UserVmDao userVmDao; @Mock + EntityManager entityManager; ++ @Mock + SnapshotDataStoreDao snapshotDataStoreDao; + private CallContext callContext; private List<String> hostTags = List.of("htag1", "htag2", "htag3"); private List<String> storageTags = List.of("stag1", "stag2"); diff --cc ui/public/locales/en.json index bf5ea84b343,99873820d53..0a9539abff3 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@@ -649,9 -591,10 +649,11 @@@ "label.copy.consoleurl": "Copy console URL to clipboard", "label.copyid": "Copy ID", "label.copy.password": "Copy password", + "label.copy.templates.from.other.secondary.storages": "Copy Templates from other storages instead of fetching from URLs", + "label.copy.templates.from.other.secondary.storages.add.zone": "Copy Templates from other storages", "label.core": "Core", "label.core.zone.type": "Core Zone type", +"label.count": "Count", "label.counter": "Counter", "label.counter.name": "Name of the counter for which the policy will be evaluated", "label.cpu": "CPU", @@@ -3312,17 -3018,12 +3314,17 @@@ "message.desc.import.shared.kvm.wizard": "Import QCOW2 image from selected Primary Storage Pool", "message.desc.import.unmanage.volume": "Please choose a storage pool that you want to import or unmanage volumes. The storage pool should be in Up status. <br>This feature only supports KVM.", "message.desc.importexportinstancewizard": "By choosing to manage an Instance, CloudStack takes over the orchestration of that Instance. Unmanaging an Instance removes CloudStack ability to manage it. In both cases, the Instance is left running and no changes are done to the VM on the hypervisor.<br><br>For KVM, managing a VM is an experimental feature.", +"message.desc.importingestinstancewizard": "This feature only applies to libvirt based KVM instances. Only Stopped instances can be ingested", "message.desc.importmigratefromvmwarewizard": "By selecting an existing or external VMware Datacenter and an instance to import, CloudStack migrates the selected instance from VMware to KVM on a conversion host using virt-v2v and imports it into a KVM Cluster", "message.desc.primary.storage": "Each Cluster must contain one or more primary storage servers. We will add the first one now. Primary storage contains the disk volumes for all the Instances running on hosts in the cluster. Use any standards-compliant protocol that is supported by the underlying hypervisor.", -"message.desc.reset.ssh.key.pair": "Please specify a ssh key pair that you would like to add to this Instance.", -"message.desc.secondary.storage": "Each Zone must have at least one NFS or secondary storage server. We will add the first one now. Secondary storage stores Instance Templates, ISO images, and Instance disk volume Snapshots. This server must be available to all hosts in the zone.<br/><br/>Provide the IP address and exported path.<br/><br/> \"Copy templates from other secondary storages\" switch can be used to automatically copy existing templates from secondary storages in other zones i [...] +"message.desc.register.template": "Hosted on download.cloudstack.org, these templates can be easily registered directly within CloudStack. Simply click <strong>Register Template</strong> for the templates you wish to use.", +"message.desc.register.cni.config": "Please fill in the following data to register CNI Configuration as user data.", "message.desc.register.user.data": "Please fill in the following to register new User Data.", "message.desc.registered.user.data": "Registered a User Data.", +"message.desc.reset.ssh.key.pair": "Please specify a ssh key pair that you would like to add to this Instance.", - "message.desc.secondary.storage": "Each Zone must have at least one NFS or secondary storage server. We will add the first one now. Secondary storage stores Instance Templates, ISO images, and Instance disk volume Snapshots. This server must be available to all hosts in the zone.<br/><br/>Provide the IP address and exported path.", ++"message.desc.secondary.storage": "Each Zone must have at least one NFS or secondary storage server. We will add the first one now. Secondary storage stores Instance Templates, ISO images, and Instance disk volume Snapshots. This server must be available to all hosts in the zone.<br/><br/>Provide the IP address and exported path.<br/><br/> \"Copy templates from other secondary storages\" switch can be used to automatically copy existing templates from secondary storages in other zones i [...] +"message.desc.validationformat": "Specifies the format used to validate the parameter value, such as EMAIL, URL, UUID, DECIMAL, etc.", +"message.desc.valueoptions": "Provide a comma-separated list of values that will appear as selectable options for this parameter", "message.desc.zone": "A Zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. A zone consists of one or more Pods (each of which contains hosts and primary storage servers) and a secondary storage server which is shared by all pods in the zone.", "message.desc.zone.edge": "A Zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. An edge zone consists of one or more hosts (each of which provides local storage as primary storage servers). Only shared and L2 Networks can be deployed in such zones and functionalities that require secondary storages are not supported.", "message.drs.plan.description": "The maximum number of live migrations allowed for DRS. Configure DRS under the settings tab before generating a plan or to enable automatic DRS for the Cluster.", diff --cc ui/src/components/widgets/InfiniteScrollSelect.vue index 122feafb2a0,da780b66b80..8db4d8d523a --- a/ui/src/components/widgets/InfiniteScrollSelect.vue +++ b/ui/src/components/widgets/InfiniteScrollSelect.vue @@@ -41,9 -41,10 +41,11 @@@ - optionValueKey (String, optional): Property to use as the value for options (e.g., 'name'). Default is 'id' - optionLabelKey (String, optional): Property to use as the label for options (e.g., 'name'). Default is 'name' - defaultOption (Object, optional): Preselected object to include initially + - allowClear (Boolean, optional): Whether to allow clearing the selection. Default is false - showIcon (Boolean, optional): Whether to show icon for the options. Default is true - defaultIcon (String, optional): Icon to be shown when there is no resource icon for the option. Default is 'cloud-outlined' + - autoSelectFirstOption (Boolean, optional): Whether to automatically select the first option when options are loaded. Default is false + - selectFirstOption (Boolean, optional): Whether to automatically select the first option when options are loaded. Default is false Events: - @change-option-value (Function): Emits the selected option value(s) when value(s) changes. Do not use @change as it will give warnings and may not work @@@ -76,9 -78,9 +79,9 @@@ </div> </div> </template> - <a-select-option v-for="option in options" :key="option.id" :value="option[optionValueKey]"> + <a-select-option v-for="option in selectableOptions" :key="option.id" :value="option[optionValueKey]"> <span> - <span v-if="showIcon && option.showicon !== false"> - <span v-if="showIcon && option.id !== null && option.id !== undefined"> ++ <span v-if="showIcon && option.showicon !== false && option.id !== null && option.id !== undefined"> <resource-icon v-if="option.icon && option.icon.base64image" :image="option.icon.base64image" size="1x" style="margin-right: 5px"/> <render-icon v-else :icon="defaultIcon" style="margin-right: 5px" /> </span> @@@ -141,9 -143,9 +148,13 @@@ export default type: Number, default: null }, + autoSelectFirstOption: { + type: Boolean, + default: false ++ }, + selectFirstOption: { + type: Boolean, + default: false } }, data () { @@@ -157,7 -159,7 +168,8 @@@ scrollHandlerAttached: false, preselectedOptionValue: null, successiveFetches: 0, - canSelectFirstOption: false ++ canSelectFirstOption: false, + hasAutoSelectedFirst: false } }, created () { @@@ -218,9 -250,9 +260,10 @@@ }).catch(error => { this.$notifyError(error) }).finally(() => { + this.canSelectFirstOption = true if (this.successiveFetches === 0) { this.loading = false + this.autoSelectFirstOptionIfNeeded() } }) }, @@@ -229,19 -261,12 +272,18 @@@ (Array.isArray(this.preselectedOptionValue) && this.preselectedOptionValue.length === 0) || this.successiveFetches >= this.maxSuccessiveFetches) { this.resetPreselectedOptionValue() + if (!this.canSelectFirstOption && this.autoSelectFirstOption && this.options.length > 0) { + this.$nextTick(() => { + this.preselectedOptionValue = this.options[0][this.optionValueKey] + this.onChange(this.preselectedOptionValue) + }) + } return } - const matchValue = Array.isArray(this.preselectedOptionValue) ? this.preselectedOptionValue[0] : this.preselectedOptionValue - const match = this.options.find(entry => entry[this.optionValueKey] === matchValue) - if (!match) { + if (!this.preselectedMatch) { this.successiveFetches++ - if (this.options.length < this.totalCount) { + // Exclude defaultOption from count when comparing with totalCount + if (this.apiOptionsCount < this.totalCount) { this.fetchItems() } else { this.resetPreselectedOptionValue() diff --cc ui/src/views/iam/AddUser.vue index 374d81c51d1,3f0bd018050..acde7583887 --- a/ui/src/views/iam/AddUser.vue +++ b/ui/src/views/iam/AddUser.vue @@@ -313,79 -274,77 +274,81 @@@ export default isValidValueForKey (obj, key) { return key in obj && obj[key] != null }, - handleSubmit (e) { + async handleSubmit (e) { e.preventDefault() if (this.loading) return - this.formRef.value.validate().then(() => { - const values = toRaw(this.form) - this.loading = true - const params = { - username: values.username, - password: values.password, - email: values.email, - firstname: values.firstname, - lastname: values.lastname, - accounttype: 0 - } - - // Account: use route query account if available, otherwise use form value (which is the account name) - if (this.account) { - params.account = this.account - } else if (values.account) { - params.account = values.account - } - // Domain: use route query domainid if available, otherwise use form value - if (this.domainid) { - params.domainid = this.domainid - } else if (values.domainid) { - params.domainid = values.domainid - } + await this.formRef.value.validate() + .catch(error => this.formRef.value.scrollToField(error.errorFields[0].name)) - if (this.isValidValueForKey(values, 'timezone') && values.timezone.length > 0) { - params.timezone = values.timezone - } + this.loading = true + const values = toRaw(this.form) + try { + const userCreationResponse = await this.createUser(values) + this.$notification.success({ + message: this.$t('label.create.user'), + description: `${this.$t('message.success.create.user')} ${values.username}` + }) - api('createUser', {}, 'POST', params).then(response => { - this.$emit('refresh-data') + const user = userCreationResponse?.createuserresponse?.user + if (values.samlenable && user) { + await postAPI('authorizeSamlSso', { + enable: values.samlenable, + entityid: values.samlentity, + userid: user.id + }) this.$notification.success({ - message: this.$t('label.create.user'), - description: `${this.$t('message.success.create.user')} ${params.username}` + message: this.$t('label.samlenable'), + description: this.$t('message.success.enable.saml.auth') }) - const user = response.createuserresponse.user - if (values.samlenable && user) { - api('authorizeSamlSso', { - enable: values.samlenable, - entityid: values.samlentity, - userid: user.id - }).then(response => { - this.$notification.success({ - message: this.$t('label.samlenable'), - description: this.$t('message.success.enable.saml.auth') - }) - }).catch(error => { - this.$notification.error({ - message: this.$t('message.request.failed'), - description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message, - duration: 0 - }) - }) - } + } + + this.closeAction() + this.$emit('refresh-data') + } catch (error) { + if (error?.config?.params?.command === 'authorizeSamlSso') { this.closeAction() - }).catch(error => { - this.$notification.error({ - message: this.$t('message.request.failed'), - description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message, - duration: 0 - }) - }).finally(() => { - this.loading = false + this.$emit('refresh-data') + } + + this.$notification.error({ + message: this.$t('message.request.failed'), + description: error?.response?.headers['x-description'] || error.message, + duration: 0 }) - }).catch(error => { - this.formRef.value.scrollToField(error.errorFields[0].name) - }) + } finally { + this.loading = false + } + }, + async createUser (rawParams) { + const params = { + username: rawParams.username, + password: rawParams.password, + email: rawParams.email, + firstname: rawParams.firstname, + lastname: rawParams.lastname, + accounttype: 0 + } + ++ // Account: use route query account if available, otherwise use form value (which is the account name) + if (this.account) { + params.account = this.account - } else if (this.accountList[rawParams.account]) { - params.account = this.accountList[rawParams.account].name ++ } else if (rawParams.account) { ++ params.account = rawParams.account + } + ++ // Domain: use route query domainid if available, otherwise use form value + if (this.domainid) { + params.domainid = this.domainid + } else if (rawParams.domainid) { + params.domainid = rawParams.domainid + } + + if (this.isValidValueForKey(rawParams, 'timezone') && rawParams.timezone.length > 0) { + params.timezone = rawParams.timezone + } + + return postAPI('createUser', params) }, async validateConfirmPassword (rule, value) { if (!value || value.length === 0) { diff --cc ui/src/views/infra/AddSecondaryStorage.vue index 0a02401e8e5,db4893115a6..603801b41fb --- a/ui/src/views/infra/AddSecondaryStorage.vue +++ b/ui/src/views/infra/AddSecondaryStorage.vue @@@ -229,16 -246,51 +246,51 @@@ export default closeModal () { this.$emit('close-action') }, + fetchCopyTemplatesConfig () { + if (!this.form.zone) { + return + } + - api('listConfigurations', { ++ getAPI('listConfigurations', { + name: 'copy.templates.from.other.secondary.storages', + zoneid: this.form.zone + }).then(json => { + const items = + json?.listconfigurationsresponse?.configuration || [] + + items.forEach(item => { + if (item.name === 'copy.templates.from.other.secondary.storages') { + this.form.copyTemplatesFromOtherSecondaryStorages = + item.value === 'true' + } + }) + }) + }, + onZoneChange (val) { + this.form.zone = val + this.copyTemplatesTouched = false + this.fetchCopyTemplatesConfig() + }, listZones () { - api('listZones', { showicon: true }).then(json => { + getAPI('listZones', { showicon: true }).then(json => { - if (json && json.listzonesresponse && json.listzonesresponse.zone) { - this.zones = json.listzonesresponse.zone - if (this.zones.length > 0) { - this.form.zone = this.zones[0].id || '' - } + this.zones = json.listzonesresponse.zone || [] + + if (this.zones.length > 0) { + this.form.zone = this.zones[0].id + this.fetchCopyTemplatesConfig() } }) }, + checkOtherSecondaryStorages () { - api('listImageStores', { listall: true }).then(json => { ++ getAPI('listImageStores', { listall: true }).then(json => { + const stores = json?.listimagestoresresponse?.imagestore || [] + + this.showCopyTemplatesToggle = stores.length > 0 + }) + }, + onCopyTemplatesToggleChanged (val) { + this.copyTemplatesTouched = true + }, nfsURL (server, path) { var url if (path.substring(0, 1) !== '/') { diff --cc ui/src/views/infra/zone/ZoneWizardAddResources.vue index b2a273f4c88,298cc7fec9d..32b7b10ad6d --- a/ui/src/views/infra/zone/ZoneWizardAddResources.vue +++ b/ui/src/views/infra/zone/ZoneWizardAddResources.vue @@@ -1116,9 -1117,23 +1125,23 @@@ export default this.storageProviders = storageProviders }) }, + applyCopyTemplatesOptionFromGlobalSettingDuringSecondaryStorageAddition () { - api('listConfigurations', { ++ getAPI('listConfigurations', { + name: 'copy.templates.from.other.secondary.storages' + }).then(json => { + const config = json?.listconfigurationsresponse?.configuration?.[0] + + if (!config || config.value === undefined) { + return + } + + const value = String(config.value).toLowerCase() === 'true' + this.copytemplate = value + }) + }, fetchPrimaryStorageProvider () { this.primaryStorageProviders = [] - api('listStorageProviders', { type: 'primary' }).then(json => { + getAPI('listStorageProviders', { type: 'primary' }).then(json => { this.primaryStorageProviders = json.liststorageprovidersresponse.dataStoreProvider || [] this.primaryStorageProviders.map((item, idx) => { this.primaryStorageProviders[idx].id = item.name }) }) diff --cc ui/src/views/storage/UploadVolume.vue index db8866caa0c,c2cbaabc225..12a39eae340 --- a/ui/src/views/storage/UploadVolume.vue +++ b/ui/src/views/storage/UploadVolume.vue @@@ -158,7 -142,7 +142,7 @@@ <script> import { ref, reactive, toRaw } from 'vue' - import { getAPI, postAPI } from '@/api' -import { api } from '@/api' ++import { postAPI } from '@/api' import { mixinForm } from '@/utils/mixin' import ResourceIcon from '@/components/view/ResourceIcon' import TooltipLabel from '@/components/widgets/TooltipLabel' diff --cc ui/src/views/tools/CreateWebhook.vue index 961acd20e18,ef07cc39ed0..5f7ed0204b4 --- a/ui/src/views/tools/CreateWebhook.vue +++ b/ui/src/views/tools/CreateWebhook.vue @@@ -155,8 -149,7 +149,7 @@@ <script> import { ref, reactive, toRaw } from 'vue' - import { getAPI, postAPI } from '@/api' - import _ from 'lodash' -import { api } from '@/api' ++import { postAPI } from '@/api' import { mixinForm } from '@/utils/mixin' import TooltipLabel from '@/components/widgets/TooltipLabel' import TestWebhookDeliveryView from '@/components/view/TestWebhookDeliveryView' @@@ -300,13 -263,11 +263,11 @@@ export default return } if (values.account) { - const accountItem = _.find(this.accounts, (option) => option.id === values.account) - if (accountItem) { - params.account = accountItem.name - } + // values.account is the account name (optionValueKey="name") + params.account = values.account } this.loading = true - api('createWebhook', params).then(json => { + postAPI('createWebhook', params).then(json => { this.$emit('refresh-data') this.$notification.success({ message: this.$t('label.create.webhook'),
