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'),


Reply via email to