This is an automated email from the ASF dual-hosted git repository.

harikrishna pushed a commit to branch 4.20
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/4.20 by this push:
     new 6932cacabc1 Allow copy of templates from secondary storages of other 
zone when adding a new secondary storage (#12296)
6932cacabc1 is described below

commit 6932cacabc187cf3d76e53c7979ed10067aff2f2
Author: Harikrishna <[email protected]>
AuthorDate: Wed Jan 28 16:00:30 2026 +0530

    Allow copy of templates from secondary storages of other zone when adding a 
new secondary storage (#12296)
    
    * Allow copy of templates from secondary storages of other zone when adding 
a new secondary storage
    
    * Add API param and UI changes on add secondary storage page
    
    * Make copy template across zones non blocking
    
    * Code fixes
    
    * unused imports
    
    * Add copy template flag in zone wizard and remove NFS checks
    
    * Fix UI
    
    * Label fixes
    
    * code optimizations
    
    * code refactoring
    
    * missing changes
    
    * Combine template copy and download into a single asynchronous operation
    
    * unused import and fixed conflicts
    
    * unused code
    
    * update config message
    
    * Fix configuration setting value on add secondary storage page
    
    * Removed unused code
    
    * Update unit tests
---
 .../command/admin/host/AddSecondaryStorageCmd.java |  24 ++-
 .../service/StorageOrchestrationService.java       |   3 +-
 .../subsystem/api/storage/TemplateService.java     |   4 +-
 .../java/com/cloud/storage/StorageManager.java     |   5 +-
 .../engine/orchestration/StorageOrchestrator.java  |  45 ++++--
 .../storage/image/TemplateServiceImpl.java         | 157 ++++++++++++++++---
 .../storage/image/TemplateServiceImplTest.java     | 171 ++++++++++++++++++++-
 .../com/cloud/storage/ImageStoreDetailsUtil.java   |  11 ++
 .../java/com/cloud/storage/StorageManagerImpl.java |   2 +-
 .../com/cloud/template/TemplateManagerImpl.java    |  14 +-
 ui/public/locales/en.json                          |   4 +-
 ui/src/views/infra/AddSecondaryStorage.vue         |  82 +++++++++-
 ui/src/views/infra/zone/ZoneWizardAddResources.vue |  25 ++-
 ui/src/views/infra/zone/ZoneWizardLaunchZone.vue   |   5 +
 14 files changed, 487 insertions(+), 65 deletions(-)

diff --git 
a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddSecondaryStorageCmd.java
 
b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddSecondaryStorageCmd.java
index 9a7eff7e2e5..585fd1b87a8 100644
--- 
a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddSecondaryStorageCmd.java
+++ 
b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddSecondaryStorageCmd.java
@@ -29,6 +29,11 @@ import org.apache.cloudstack.api.response.ZoneResponse;
 import com.cloud.exception.DiscoveryException;
 import com.cloud.storage.ImageStore;
 import com.cloud.user.Account;
+import org.apache.commons.collections.MapUtils;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
 
 @APICommand(name = "addSecondaryStorage", description = "Adds secondary 
storage.", responseObject = ImageStoreResponse.class,
         requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
@@ -44,6 +49,9 @@ public class AddSecondaryStorageCmd extends BaseCmd {
     @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, 
entityType = ZoneResponse.class, description = "The Zone ID for the secondary 
storage")
     protected Long zoneId;
 
+    @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, 
description = "Details in key/value pairs using format 
details[i].keyname=keyvalue. Example: 
details[0].copytemplatesfromothersecondarystorages=true")
+    protected Map details;
+
     /////////////////////////////////////////////////////
     /////////////////// Accessors ///////////////////////
     /////////////////////////////////////////////////////
@@ -56,6 +64,20 @@ public class AddSecondaryStorageCmd extends BaseCmd {
         return zoneId;
     }
 
+    public Map<String, String> getDetails() {
+        Map<String, String> detailsMap = new HashMap<>();
+        if (MapUtils.isNotEmpty(details)) {
+            Collection<?> props = details.values();
+            for (Object prop : props) {
+                HashMap<String, String> detail = (HashMap<String, String>) 
prop;
+                for (Map.Entry<String, String> entry: detail.entrySet()) {
+                    detailsMap.put(entry.getKey(),entry.getValue());
+                }
+            }
+        }
+        return detailsMap;
+    }
+
     /////////////////////////////////////////////////////
     /////////////// API Implementation///////////////////
     /////////////////////////////////////////////////////
@@ -68,7 +90,7 @@ public class AddSecondaryStorageCmd extends BaseCmd {
     @Override
     public void execute(){
         try{
-            ImageStore result = _storageService.discoverImageStore(null, 
getUrl(), "NFS", getZoneId(), null);
+            ImageStore result = _storageService.discoverImageStore(null, 
getUrl(), "NFS", getZoneId(), getDetails());
             ImageStoreResponse storeResponse = null;
             if (result != null ) {
                     storeResponse = 
_responseGenerator.createImageStoreResponse(result);
diff --git 
a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/StorageOrchestrationService.java
 
b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/StorageOrchestrationService.java
index 8be2015bfef..4af0c806060 100644
--- 
a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/StorageOrchestrationService.java
+++ 
b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/StorageOrchestrationService.java
@@ -22,7 +22,6 @@ import java.util.concurrent.Future;
 
 import org.apache.cloudstack.api.response.MigrationResponse;
 import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
-import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
 import 
org.apache.cloudstack.engine.subsystem.api.storage.TemplateService.TemplateApiResult;
 import org.apache.cloudstack.storage.ImageStoreService.MigrationPolicy;
 
@@ -31,5 +30,5 @@ public interface StorageOrchestrationService {
 
     MigrationResponse migrateResources(Long srcImgStoreId, Long 
destImgStoreId, List<Long> templateIdList, List<Long> snapshotIdList);
 
-    Future<TemplateApiResult> orchestrateTemplateCopyToImageStore(TemplateInfo 
source, DataStore destStore);
+    Future<TemplateApiResult> orchestrateTemplateCopyFromSecondaryStores(long 
templateId, DataStore destStore);
 }
diff --git 
a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateService.java
 
b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateService.java
index a8861d5acc6..269eb4f1c21 100644
--- 
a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateService.java
+++ 
b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateService.java
@@ -80,4 +80,6 @@ public interface TemplateService {
     List<DatadiskTO> getTemplateDatadisksOnImageStore(TemplateInfo 
templateInfo, String configurationId);
 
     AsyncCallFuture<TemplateApiResult> copyTemplateToImageStore(DataObject 
source, DataStore destStore);
-}
+
+    void handleTemplateCopyFromSecondaryStores(long templateId, DataStore 
destStore);
+ }
diff --git 
a/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java 
b/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java
index de0cb34d63e..4ce1f4a9638 100644
--- 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,8 +220,9 @@ public interface StorageManager extends StorageService {
             "storage.pool.host.connect.workers", "1",
             "Number of worker threads to be used to connect hosts to a primary 
storage", true);
 
-    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 --git 
a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/StorageOrchestrator.java
 
b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/StorageOrchestrator.java
index 37a1f8dc196..933b4e0c5ce 100644
--- 
a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/StorageOrchestrator.java
+++ 
b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/StorageOrchestrator.java
@@ -36,6 +36,9 @@ import java.util.stream.Collectors;
 import javax.inject.Inject;
 import javax.naming.ConfigurationException;
 
+import com.cloud.dc.dao.DataCenterDao;
+import com.cloud.storage.dao.VMTemplateDao;
+import com.cloud.template.TemplateManager;
 import org.apache.cloudstack.api.response.MigrationResponse;
 import 
org.apache.cloudstack.engine.orchestration.service.StorageOrchestrationService;
 import org.apache.cloudstack.engine.subsystem.api.storage.DataObject;
@@ -45,6 +48,7 @@ import 
org.apache.cloudstack.engine.subsystem.api.storage.SecondaryStorageServic
 import 
org.apache.cloudstack.engine.subsystem.api.storage.SecondaryStorageService.DataObjectResult;
 import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory;
 import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
+import org.apache.cloudstack.engine.subsystem.api.storage.TemplateDataFactory;
 import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
 import org.apache.cloudstack.engine.subsystem.api.storage.TemplateService;
 import 
org.apache.cloudstack.engine.subsystem.api.storage.TemplateService.TemplateApiResult;
@@ -103,6 +107,15 @@ public class StorageOrchestrator extends ManagerBase 
implements StorageOrchestra
     VolumeDataStoreDao volumeDataStoreDao;
     @Inject
     DataMigrationUtility migrationHelper;
+    @Inject
+    TemplateManager templateManager;
+    @Inject
+    VMTemplateDao templateDao;
+    @Inject
+    TemplateDataFactory templateDataFactory;
+    @Inject
+    DataCenterDao dcDao;
+
 
     ConfigKey<Double> ImageStoreImbalanceThreshold = new 
ConfigKey<>("Advanced", Double.class,
             "image.store.imbalance.threshold",
@@ -304,8 +317,9 @@ public class StorageOrchestrator extends ManagerBase 
implements StorageOrchestra
     }
 
     @Override
-    public Future<TemplateApiResult> 
orchestrateTemplateCopyToImageStore(TemplateInfo source, DataStore destStore) {
-        return submit(destStore.getScope().getScopeId(), new 
CopyTemplateTask(source, destStore));
+    public Future<TemplateApiResult> 
orchestrateTemplateCopyFromSecondaryStores(long srcTemplateId, DataStore 
destStore) {
+        Long dstZoneId = destStore.getScope().getScopeId();
+        return submit(dstZoneId, new 
CopyTemplateFromSecondaryStorageTask(srcTemplateId, destStore));
     }
 
     protected Pair<String, Boolean> migrateCompleted(Long destDatastoreId, 
DataStore srcDatastore, List<DataObject> files, MigrationPolicy 
migrationPolicy, int skipped) {
@@ -624,13 +638,13 @@ public class StorageOrchestrator extends ManagerBase 
implements StorageOrchestra
         }
     }
 
-    private class CopyTemplateTask implements Callable<TemplateApiResult> {
-        private TemplateInfo sourceTmpl;
-        private DataStore destStore;
-        private String logid;
+    private class CopyTemplateFromSecondaryStorageTask implements 
Callable<TemplateApiResult> {
+        private final long srcTemplateId;
+        private final DataStore destStore;
+        private final String logid;
 
-        public CopyTemplateTask(TemplateInfo sourceTmpl, DataStore destStore) {
-            this.sourceTmpl = sourceTmpl;
+        CopyTemplateFromSecondaryStorageTask(long srcTemplateId, DataStore 
destStore) {
+            this.srcTemplateId = srcTemplateId;
             this.destStore = destStore;
             this.logid = ThreadContext.get(LOGCONTEXTID);
         }
@@ -639,17 +653,16 @@ public class StorageOrchestrator extends ManagerBase 
implements StorageOrchestra
         public TemplateApiResult call() {
             ThreadContext.put(LOGCONTEXTID, logid);
             TemplateApiResult result;
-            AsyncCallFuture<TemplateApiResult> future = 
templateService.copyTemplateToImageStore(sourceTmpl, destStore);
+            long destZoneId = destStore.getScope().getScopeId();
+            TemplateInfo sourceTmpl = 
templateDataFactory.getTemplate(srcTemplateId, DataStoreRole.Image);
             try {
-                result = future.get();
-            } catch (ExecutionException | InterruptedException e) {
-                logger.warn("Exception while copying template [{}] from image 
store [{}] to image store [{}]: {}",
-                        sourceTmpl.getUniqueName(), 
sourceTmpl.getDataStore().getName(), destStore.getName(), e.toString());
+                
templateService.handleTemplateCopyFromSecondaryStores(srcTemplateId, destStore);
                 result = new TemplateApiResult(sourceTmpl);
-                result.setResult(e.getMessage());
+            } finally {
+                tryCleaningUpExecutor(destZoneId);
+                ThreadContext.clearAll();
             }
-            tryCleaningUpExecutor(destStore.getScope().getScopeId());
-            ThreadContext.clearAll();
+
             return result;
         }
     }
diff --git 
a/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java
 
b/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java
index bee62955051..5fc9bbac352 100644
--- 
a/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java
+++ 
b/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java
@@ -31,6 +31,8 @@ import java.util.concurrent.ExecutionException;
 
 import javax.inject.Inject;
 
+import com.cloud.exception.StorageUnavailableException;
+import org.apache.cloudstack.context.CallContext;
 import 
org.apache.cloudstack.engine.orchestration.service.StorageOrchestrationService;
 import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult;
 import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult;
@@ -67,9 +69,11 @@ import 
org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO;
 import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity;
 import org.apache.cloudstack.storage.image.store.TemplateObject;
 import org.apache.cloudstack.storage.to.TemplateObjectTO;
+import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.ThreadContext;
 import org.springframework.stereotype.Component;
 
 import com.cloud.agent.api.Answer;
@@ -567,10 +571,7 @@ public class TemplateServiceImpl implements 
TemplateService {
                             }
 
                             if 
(availHypers.contains(tmplt.getHypervisorType())) {
-                                boolean copied = 
isCopyFromOtherStoragesEnabled(zoneId) && tryCopyingTemplateToImageStore(tmplt, 
store);
-                                if (!copied) {
-                                    tryDownloadingTemplateToImageStore(tmplt, 
store);
-                                }
+                                
storageOrchestrator.orchestrateTemplateCopyFromSecondaryStores(tmplt.getId(), 
store);
                             } else {
                                 logger.info("Skip downloading template {} 
since current data center does not have hypervisor {}", tmplt, 
tmplt.getHypervisorType());
                             }
@@ -617,6 +618,16 @@ public class TemplateServiceImpl implements 
TemplateService {
 
     }
 
+    @Override
+    public void handleTemplateCopyFromSecondaryStores(long templateId, 
DataStore destStore) {
+        VMTemplateVO template = _templateDao.findById(templateId);
+        long zoneId = destStore.getScope().getScopeId();
+        boolean copied = 
imageStoreDetailsUtil.isCopyTemplatesFromOtherStoragesEnabled(destStore.getId(),
 zoneId) && tryCopyingTemplateToImageStore(template, destStore);
+        if (!copied) {
+            tryDownloadingTemplateToImageStore(template, destStore);
+        }
+    }
+
     protected void tryDownloadingTemplateToImageStore(VMTemplateVO tmplt, 
DataStore destStore) {
         if (tmplt.getUrl() == null) {
             logger.info("Not downloading template [{}] to image store [{}], as 
it has no URL.", tmplt.getUniqueName(),
@@ -634,28 +645,134 @@ public class TemplateServiceImpl implements 
TemplateService {
     }
 
     protected boolean tryCopyingTemplateToImageStore(VMTemplateVO tmplt, 
DataStore destStore) {
-        Long zoneId = destStore.getScope().getScopeId();
-        List<DataStore> storesInZone = 
_storeMgr.getImageStoresByZoneIds(zoneId);
-        for (DataStore sourceStore : storesInZone) {
-            Map<String, TemplateProp> existingTemplatesInSourceStore = 
listTemplate(sourceStore);
-            if (existingTemplatesInSourceStore == null || 
!existingTemplatesInSourceStore.containsKey(tmplt.getUniqueName())) {
-                logger.debug("Template [{}] does not exist on image store 
[{}]; searching on another one.",
-                        tmplt.getUniqueName(), sourceStore.getName());
+        if (searchAndCopyWithinZone(tmplt, destStore)) {
+            return true;
+        }
+
+        Long destZoneId = destStore.getScope().getScopeId();
+        logger.debug("Template [{}] not found in any image store of zone [{}]. 
Checking other zones.",
+                tmplt.getUniqueName(), destZoneId);
+
+        return searchAndCopyAcrossZones(tmplt, destStore, destZoneId);
+    }
+
+    private boolean searchAndCopyAcrossZones(VMTemplateVO tmplt, DataStore 
destStore, Long destZoneId) {
+        List<Long> allZoneIds = _dcDao.listAllIds();
+        for (Long otherZoneId : allZoneIds) {
+            if (otherZoneId.equals(destZoneId)) {
                 continue;
             }
-            TemplateObject sourceTmpl = (TemplateObject) 
_templateFactory.getTemplate(tmplt.getId(), sourceStore);
-            if (sourceTmpl.getInstallPath() == null) {
-                logger.warn("Can not copy template [{}] from image store [{}], 
as it returned a null install path.", tmplt.getUniqueName(),
-                        sourceStore.getName());
+
+            List<DataStore> storesInOtherZone = 
_storeMgr.getImageStoresByZoneIds(otherZoneId);
+            logger.debug("Checking zone [{}] for template [{}]...", 
otherZoneId, tmplt.getUniqueName());
+
+            if (CollectionUtils.isEmpty(storesInOtherZone)) {
+                logger.debug("Zone [{}] has no image stores. Skipping.", 
otherZoneId);
                 continue;
             }
-            
storageOrchestrator.orchestrateTemplateCopyToImageStore(sourceTmpl, destStore);
-            return true;
+
+            TemplateObject sourceTmpl = findUsableTemplate(tmplt, 
storesInOtherZone);
+            if (sourceTmpl == null) {
+                logger.debug("Template [{}] not found with a valid install 
path in any image store of zone [{}].",
+                        tmplt.getUniqueName(), otherZoneId);
+                continue;
+            }
+
+            logger.info("Template [{}] found in zone [{}]. Initiating 
cross-zone copy to zone [{}].",
+                    tmplt.getUniqueName(), otherZoneId, destZoneId);
+
+            return copyTemplateAcrossZones(destStore, sourceTmpl);
         }
-        logger.debug("Can't copy template [{}] from another image store.", 
tmplt.getUniqueName());
+
+        logger.debug("Template [{}] was not found in any zone. Cannot perform 
zone-to-zone copy.", tmplt.getUniqueName());
         return false;
     }
 
+    protected TemplateObject findUsableTemplate(VMTemplateVO tmplt, 
List<DataStore> imageStores) {
+        for (DataStore store : imageStores) {
+
+            Map<String, TemplateProp> templates = listTemplate(store);
+            if (templates == null || 
!templates.containsKey(tmplt.getUniqueName())) {
+                continue;
+            }
+
+            TemplateObject tmpl = (TemplateObject) 
_templateFactory.getTemplate(tmplt.getId(), store);
+            if (tmpl.getInstallPath() == null) {
+                logger.debug("Template [{}] found in image store [{}] but 
install path is null. Skipping.",
+                        tmplt.getUniqueName(), store.getName());
+                continue;
+            }
+            return tmpl;
+        }
+        return null;
+    }
+
+    private boolean searchAndCopyWithinZone(VMTemplateVO tmplt, DataStore 
destStore) {
+        Long destZoneId = destStore.getScope().getScopeId();
+        List<DataStore> storesInSameZone = 
_storeMgr.getImageStoresByZoneIds(destZoneId);
+
+        TemplateObject sourceTmpl = findUsableTemplate(tmplt, 
storesInSameZone);
+        if (sourceTmpl == null) {
+            return false;
+        }
+
+        TemplateApiResult result;
+        AsyncCallFuture<TemplateApiResult> future = 
copyTemplateToImageStore(sourceTmpl, destStore);
+        try {
+            result = future.get();
+        } catch (ExecutionException | InterruptedException e) {
+            logger.warn("Exception while copying template [{}] from image 
store [{}] to image store [{}]: {}",
+                    sourceTmpl.getUniqueName(), 
sourceTmpl.getDataStore().getName(), destStore.getName(), e.toString());
+            result = new TemplateApiResult(sourceTmpl);
+            result.setResult(e.getMessage());
+        }
+        return result.isSuccess();
+    }
+
+    private boolean copyTemplateAcrossZones(DataStore destStore, 
TemplateObject sourceTmpl) {
+        Long dstZoneId = destStore.getScope().getScopeId();
+        DataCenterVO dstZone = _dcDao.findById(dstZoneId);
+
+        if (dstZone == null) {
+            logger.warn("Destination zone [{}] not found for template [{}].", 
dstZoneId, sourceTmpl.getUniqueName());
+            return false;
+        }
+
+        TemplateApiResult result;
+        try {
+            VMTemplateVO template = _templateDao.findById(sourceTmpl.getId());
+            try {
+                DataStore sourceStore = sourceTmpl.getDataStore();
+                long userId = CallContext.current().getCallingUserId();
+                boolean success = _tmpltMgr.copy(userId, template, 
sourceStore, dstZone);
+
+                result = new TemplateApiResult(sourceTmpl);
+                if (!success) {
+                    result.setResult("Cross-zone template copy failed");
+                }
+            } catch (StorageUnavailableException | ResourceAllocationException 
e) {
+                logger.error("Exception while copying template [{}] from zone 
[{}] to zone [{}]",
+                        template,
+                        sourceTmpl.getDataStore().getScope().getScopeId(),
+                        dstZone.getId(),
+                        e);
+                result = new TemplateApiResult(sourceTmpl);
+                result.setResult(e.getMessage());
+            } finally {
+                ThreadContext.clearAll();
+            }
+        } catch (Exception e) {
+            logger.error("Failed to copy template [{}] from zone [{}] to zone 
[{}].",
+                    sourceTmpl.getUniqueName(),
+                    sourceTmpl.getDataStore().getScope().getScopeId(),
+                    dstZoneId,
+                    e);
+            return false;
+        }
+
+        return result.isSuccess();
+    }
+
     @Override
     public AsyncCallFuture<TemplateApiResult> 
copyTemplateToImageStore(DataObject source, DataStore destStore) {
         TemplateObject sourceTmpl = (TemplateObject) source;
@@ -699,10 +816,6 @@ public class TemplateServiceImpl implements 
TemplateService {
         return null;
     }
 
-    protected boolean isCopyFromOtherStoragesEnabled(Long zoneId) {
-        return 
StorageManager.COPY_PUBLIC_TEMPLATES_FROM_OTHER_STORAGES.valueIn(zoneId);
-    }
-
     protected void publishTemplateCreation(TemplateInfo tmplt) {
         VMTemplateVO tmpltVo = _templateDao.findById(tmplt.getId());
 
diff --git 
a/engine/storage/image/src/test/java/org/apache/cloudstack/storage/image/TemplateServiceImplTest.java
 
b/engine/storage/image/src/test/java/org/apache/cloudstack/storage/image/TemplateServiceImplTest.java
index cb7994915b3..e9eac045869 100644
--- 
a/engine/storage/image/src/test/java/org/apache/cloudstack/storage/image/TemplateServiceImplTest.java
+++ 
b/engine/storage/image/src/test/java/org/apache/cloudstack/storage/image/TemplateServiceImplTest.java
@@ -18,13 +18,20 @@
  */
 package org.apache.cloudstack.storage.image;
 
+import com.cloud.dc.DataCenterVO;
+import com.cloud.dc.dao.DataCenterDao;
+import com.cloud.exception.ResourceAllocationException;
+import com.cloud.exception.StorageUnavailableException;
+import com.cloud.storage.dao.VMTemplateDao;
 import com.cloud.storage.template.TemplateProp;
 import com.cloud.template.TemplateManager;
+import com.cloud.user.Account;
+import com.cloud.user.User;
+import org.apache.cloudstack.context.CallContext;
 import 
org.apache.cloudstack.engine.orchestration.service.StorageOrchestrationService;
 import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
 import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
 import org.apache.cloudstack.engine.subsystem.api.storage.Scope;
-import org.apache.cloudstack.framework.async.AsyncCallFuture;
 import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao;
 import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO;
 import org.apache.cloudstack.storage.image.store.TemplateObject;
@@ -46,6 +53,8 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import static org.mockito.Mockito.mock;
+
 @RunWith(MockitoJUnitRunner.class)
 public class TemplateServiceImplTest {
 
@@ -89,6 +98,12 @@ public class TemplateServiceImplTest {
     @Mock
     TemplateManager templateManagerMock;
 
+    @Mock
+    VMTemplateDao templateDao;
+
+    @Mock
+    DataCenterDao _dcDao;
+
     Map<String, TemplateProp> templatesInSourceStore = new HashMap<>();
 
     @Before
@@ -101,7 +116,6 @@ public class TemplateServiceImplTest {
         Mockito.doReturn(List.of(sourceStoreMock, 
destStoreMock)).when(dataStoreManagerMock).getImageStoresByZoneIds(zoneId);
         
Mockito.doReturn(templatesInSourceStore).when(templateService).listTemplate(sourceStoreMock);
         
Mockito.doReturn(null).when(templateService).listTemplate(destStoreMock);
-        
Mockito.doReturn("install-path").when(templateInfoMock).getInstallPath();
         
Mockito.doReturn(templateInfoMock).when(templateDataFactoryMock).getTemplate(2L,
 sourceStoreMock);
         Mockito.doReturn(3L).when(dataStoreMock).getId();
         Mockito.doReturn(zoneScopeMock).when(dataStoreMock).getScope();
@@ -166,7 +180,7 @@ public class TemplateServiceImplTest {
         boolean result = 
templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);
 
         Assert.assertFalse(result);
-        Mockito.verify(storageOrchestrator, 
Mockito.never()).orchestrateTemplateCopyToImageStore(Mockito.any(), 
Mockito.any());
+        Mockito.verify(storageOrchestrator, 
Mockito.never()).orchestrateTemplateCopyFromSecondaryStores(Mockito.anyLong(), 
Mockito.any());
     }
 
     @Test
@@ -174,20 +188,161 @@ public class TemplateServiceImplTest {
         templatesInSourceStore.put(tmpltMock.getUniqueName(), tmpltPropMock);
         Mockito.doReturn(null).when(templateInfoMock).getInstallPath();
 
+        Scope scopeMock = Mockito.mock(Scope.class);
+        Mockito.doReturn(scopeMock).when(destStoreMock).getScope();
+        Mockito.doReturn(1L).when(scopeMock).getScopeId();
+        Mockito.doReturn(List.of(1L)).when(_dcDao).listAllIds();
+
         boolean result = 
templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);
 
         Assert.assertFalse(result);
-        Mockito.verify(storageOrchestrator, 
Mockito.never()).orchestrateTemplateCopyToImageStore(Mockito.any(), 
Mockito.any());
+        Mockito.verify(storageOrchestrator, 
Mockito.never()).orchestrateTemplateCopyFromSecondaryStores(Mockito.anyLong(), 
Mockito.any());
     }
 
     @Test
-    public void 
tryCopyingTemplateToImageStoreTestReturnsTrueWhenTemplateExistsInAnotherStorageAndTaskWasScheduled()
 {
-        templatesInSourceStore.put(tmpltMock.getUniqueName(), tmpltPropMock);
-        Mockito.doReturn(new 
AsyncCallFuture<>()).when(storageOrchestrator).orchestrateTemplateCopyToImageStore(Mockito.any(),
 Mockito.any());
+    public void 
tryCopyingTemplateToImageStoreTestReturnsTrueWhenTemplateExistsInAnotherZone() 
throws StorageUnavailableException, ResourceAllocationException {
+        Scope scopeMock = Mockito.mock(Scope.class);
+        Mockito.doReturn(scopeMock).when(destStoreMock).getScope();
+        Mockito.doReturn(1L).when(scopeMock).getScopeId();
+        Mockito.doReturn(100L).when(tmpltMock).getId();
+        Mockito.doReturn("unique-name").when(tmpltMock).getUniqueName();
+        
Mockito.doReturn(List.of(sourceStoreMock)).when(dataStoreManagerMock).getImageStoresByZoneIds(1L);
+        
Mockito.doReturn(null).when(templateService).listTemplate(sourceStoreMock);
+        Mockito.doReturn(List.of(1L, 2L)).when(_dcDao).listAllIds();
+
+        DataStore otherZoneStoreMock = Mockito.mock(DataStore.class);
+        
Mockito.doReturn(List.of(otherZoneStoreMock)).when(dataStoreManagerMock).getImageStoresByZoneIds(2L);
+
+        Map<String, TemplateProp> templatesInOtherZone = new HashMap<>();
+        templatesInOtherZone.put("unique-name", tmpltPropMock);
+        
Mockito.doReturn(templatesInOtherZone).when(templateService).listTemplate(otherZoneStoreMock);
+
+        TemplateObject sourceTmplMock = Mockito.mock(TemplateObject.class);
+        
Mockito.doReturn(sourceTmplMock).when(templateDataFactoryMock).getTemplate(100L,
 otherZoneStoreMock);
+        
Mockito.doReturn("/mnt/secondary/template.qcow2").when(sourceTmplMock).getInstallPath();
+
+        DataCenterVO dstZoneMock = Mockito.mock(DataCenterVO.class);
+        Mockito.doReturn(dstZoneMock).when(_dcDao).findById(1L);
+        
Mockito.doReturn(true).when(templateManagerMock).copy(Mockito.anyLong(), 
Mockito.any(), Mockito.any(), Mockito.any());
 
         boolean result = 
templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);
 
         Assert.assertTrue(result);
-        
Mockito.verify(storageOrchestrator).orchestrateTemplateCopyToImageStore(Mockito.any(),
 Mockito.any());
+    }
+
+    @Test
+    public void 
tryCopyingTemplateToImageStoreTestReturnsFalseWhenDestinationZoneIsMissing() {
+        Scope scopeMock = Mockito.mock(Scope.class);
+        Mockito.doReturn(scopeMock).when(destStoreMock).getScope();
+        Mockito.doReturn(1L).when(scopeMock).getScopeId();
+        Mockito.doReturn(100L).when(tmpltMock).getId();
+        Mockito.doReturn("unique-name").when(tmpltMock).getUniqueName();
+        Mockito.doReturn(List.of(1L, 2L)).when(_dcDao).listAllIds();
+        
Mockito.doReturn(List.of()).when(dataStoreManagerMock).getImageStoresByZoneIds(1L);
+
+        DataStore otherZoneStoreMock = Mockito.mock(DataStore.class);
+        
Mockito.doReturn(List.of(otherZoneStoreMock)).when(dataStoreManagerMock).getImageStoresByZoneIds(2L);
+
+        Map<String, TemplateProp> templates = new HashMap<>();
+        templates.put("unique-name", tmpltPropMock);
+        
Mockito.doReturn(templates).when(templateService).listTemplate(otherZoneStoreMock);
+
+        TemplateObject sourceTmplMock = Mockito.mock(TemplateObject.class);
+        
Mockito.doReturn(sourceTmplMock).when(templateDataFactoryMock).getTemplate(100L,
 otherZoneStoreMock);
+        
Mockito.doReturn("/mnt/secondary/template.qcow2").when(sourceTmplMock).getInstallPath();
+        Mockito.doReturn(null).when(_dcDao).findById(1L);
+
+        boolean result = 
templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);
+
+        Assert.assertFalse(result);
+    }
+
+    @Test
+    public void 
tryCopyingTemplateToImageStoreTestReturnsTrueWhenCrossZoneCopyTaskIsScheduled() 
throws StorageUnavailableException, ResourceAllocationException {
+        Scope scopeMock = Mockito.mock(Scope.class);
+        Mockito.doReturn(scopeMock).when(destStoreMock).getScope();
+        Mockito.doReturn(1L).when(scopeMock).getScopeId();
+        Mockito.doReturn(100L).when(tmpltMock).getId();
+        Mockito.doReturn("unique-name").when(tmpltMock).getUniqueName();
+        Mockito.doReturn(List.of(1L, 2L)).when(_dcDao).listAllIds();
+        
Mockito.doReturn(List.of()).when(dataStoreManagerMock).getImageStoresByZoneIds(1L);
+
+        DataStore otherZoneStoreMock = Mockito.mock(DataStore.class);
+        
Mockito.doReturn(List.of(otherZoneStoreMock)).when(dataStoreManagerMock).getImageStoresByZoneIds(2L);
+
+        Map<String, TemplateProp> templates = new HashMap<>();
+        templates.put("unique-name", tmpltPropMock);
+        
Mockito.doReturn(templates).when(templateService).listTemplate(otherZoneStoreMock);
+
+        TemplateObject sourceTmplMock = Mockito.mock(TemplateObject.class);
+        
Mockito.doReturn(sourceTmplMock).when(templateDataFactoryMock).getTemplate(100L,
 otherZoneStoreMock);
+        
Mockito.doReturn("/mnt/secondary/template.qcow2").when(sourceTmplMock).getInstallPath();
+        Mockito.doReturn(100L).when(sourceTmplMock).getId();
+
+        DataStore sourceStoreMock = Mockito.mock(DataStore.class);
+        Scope sourceScopeMock = Mockito.mock(Scope.class);
+        Mockito.doReturn(sourceStoreMock).when(sourceTmplMock).getDataStore();
+
+        DataCenterVO dstZoneMock = Mockito.mock(DataCenterVO.class);
+        Mockito.doReturn(dstZoneMock).when(_dcDao).findById(1L);
+        VMTemplateVO templateVoMock = Mockito.mock(VMTemplateVO.class);
+        Mockito.doReturn(templateVoMock).when(templateDao).findById(100L);
+
+        
Mockito.doReturn(true).when(templateManagerMock).copy(Mockito.anyLong(), 
Mockito.any(), Mockito.any(), Mockito.any());
+
+        Account account = mock(Account.class);
+        User user = mock(User.class);
+        CallContext callContext = mock(CallContext.class);
+
+        boolean result = 
templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);
+
+        Assert.assertTrue(result);
+    }
+
+    @Test
+    public void 
tryCopyingTemplateToImageStoreTestReturnsFalseWhenTemplateNotFoundInAnyZone() {
+        Scope scopeMock = Mockito.mock(Scope.class);
+        Mockito.doReturn(scopeMock).when(destStoreMock).getScope();
+        Mockito.doReturn(1L).when(scopeMock).getScopeId();
+        Mockito.doReturn(List.of(1L, 2L)).when(_dcDao).listAllIds();
+        
Mockito.doReturn(List.of(sourceStoreMock)).when(dataStoreManagerMock).getImageStoresByZoneIds(Mockito.anyLong());
+        
Mockito.doReturn(null).when(templateService).listTemplate(Mockito.any());
+
+        boolean result = 
templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);
+
+        Assert.assertFalse(result);
+    }
+
+    @Test
+    public void testFindUsableTemplateReturnsTemplateWithNonNullInstallPath() {
+        VMTemplateVO template = Mockito.mock(VMTemplateVO.class);
+        Mockito.when(template.getId()).thenReturn(10L);
+        Mockito.when(template.getUniqueName()).thenReturn("test-template");
+
+        DataStore storeWithNullPath = Mockito.mock(DataStore.class);
+        Mockito.when(storeWithNullPath.getName()).thenReturn("store-null");
+
+        DataStore storeWithValidPath = Mockito.mock(DataStore.class);
+        TemplateObject tmplWithNullPath = Mockito.mock(TemplateObject.class);
+        Mockito.when(tmplWithNullPath.getInstallPath()).thenReturn(null);
+
+        TemplateObject tmplWithValidPath = Mockito.mock(TemplateObject.class);
+        
Mockito.when(tmplWithValidPath.getInstallPath()).thenReturn("/mnt/secondary/template.qcow2");
+
+        
Mockito.doReturn(tmplWithNullPath).when(templateDataFactoryMock).getTemplate(10L,
 storeWithNullPath);
+        
Mockito.doReturn(tmplWithValidPath).when(templateDataFactoryMock).getTemplate(10L,
 storeWithValidPath);
+
+        Map<String, TemplateProp> templates = new HashMap<>();
+        templates.put("test-template", Mockito.mock(TemplateProp.class));
+
+        
Mockito.doReturn(templates).when(templateService).listTemplate(storeWithNullPath);
+        
Mockito.doReturn(templates).when(templateService).listTemplate(storeWithValidPath);
+
+        List<DataStore> imageStores = List.of(storeWithNullPath, 
storeWithValidPath);
+
+        TemplateObject result = templateService.findUsableTemplate(template, 
imageStores);
+
+        Assert.assertNotNull(result);
+        Assert.assertEquals(tmplWithValidPath, result);
     }
 }
diff --git a/server/src/main/java/com/cloud/storage/ImageStoreDetailsUtil.java 
b/server/src/main/java/com/cloud/storage/ImageStoreDetailsUtil.java
index baf5ef8902d..9f5aa660f4f 100755
--- a/server/src/main/java/com/cloud/storage/ImageStoreDetailsUtil.java
+++ b/server/src/main/java/com/cloud/storage/ImageStoreDetailsUtil.java
@@ -78,4 +78,15 @@ public class ImageStoreDetailsUtil {
         return getGlobalDefaultNfsVersion();
     }
 
+    public boolean isCopyTemplatesFromOtherStoragesEnabled(Long storeId, Long 
zoneId) {
+        final Map<String, String> storeDetails = 
imageStoreDetailsDao.getDetails(storeId);
+        final String keyWithoutDots = 
StorageManager.COPY_TEMPLATES_FROM_OTHER_SECONDARY_STORAGES.key()
+                .replace(".", "");
+
+        if (storeDetails != null && storeDetails.containsKey(keyWithoutDots)) {
+            return Boolean.parseBoolean(storeDetails.get(keyWithoutDots));
+        }
+
+        return 
StorageManager.COPY_TEMPLATES_FROM_OTHER_SECONDARY_STORAGES.valueIn(zoneId);
+    }
 }
diff --git a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java 
b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java
index 13b7fbb00c2..d1dca0fa901 100644
--- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java
+++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java
@@ -4206,7 +4206,7 @@ public class StorageManagerImpl extends ManagerBase 
implements StorageManager, C
                 DataStoreDownloadFollowRedirects,
                 AllowVolumeReSizeBeyondAllocation,
                 StoragePoolHostConnectWorkers,
-                COPY_PUBLIC_TEMPLATES_FROM_OTHER_STORAGES
+                COPY_TEMPLATES_FROM_OTHER_SECONDARY_STORAGES
         };
     }
 
diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java 
b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
index 5773410c35a..78265021c0a 100755
--- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
+++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
@@ -842,6 +842,9 @@ public class TemplateManagerImpl extends ManagerBase 
implements TemplateManager,
         // Copy will just find one eligible image store for the destination 
zone
         // and copy template there, not propagate to all image stores
         // for that zone
+
+        boolean copied = false;
+
         for (DataStore dstSecStore : dstSecStores) {
             TemplateDataStoreVO dstTmpltStore = 
_tmplStoreDao.findByStoreTemplate(dstSecStore.getId(), tmpltId);
             if (dstTmpltStore != null && dstTmpltStore.getDownloadState() == 
Status.DOWNLOADED) {
@@ -856,9 +859,12 @@ public class TemplateManagerImpl extends ManagerBase 
implements TemplateManager,
                 TemplateApiResult result = future.get();
                 if (result.isFailed()) {
                     logger.debug("Copy Template failed for image store {}: 
{}", dstSecStore, result.getResult());
+                    _tmplStoreDao.removeByTemplateStore(tmpltId, 
dstSecStore.getId());
                     continue; // try next image store
                 }
 
+                copied = true;
+
                 _tmpltDao.addTemplateToZone(template, dstZoneId);
 
                 if (account.getId() != Account.ACCOUNT_ID_SYSTEM) {
@@ -886,12 +892,14 @@ public class TemplateManagerImpl extends ManagerBase 
implements TemplateManager,
                         }
                     }
                 }
+
+                return true;
+
             } catch (Exception ex) {
-                logger.debug("Failed to copy Template to image store:{} ,will 
try next one", dstSecStore);
+                logger.debug("Failed to copy Template to image store:{} ,will 
try next one", dstSecStore, ex);
             }
         }
-        return true;
-
+        return copied;
     }
 
     @Override
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index b2465fa325f..99873820d53 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -591,6 +591,8 @@
 "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.counter": "Counter",
@@ -3019,7 +3021,7 @@
 "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.",
+"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 in [...]
 "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.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.",
diff --git a/ui/src/views/infra/AddSecondaryStorage.vue 
b/ui/src/views/infra/AddSecondaryStorage.vue
index 746af5b959d..db4893115a6 100644
--- a/ui/src/views/infra/AddSecondaryStorage.vue
+++ b/ui/src/views/infra/AddSecondaryStorage.vue
@@ -48,6 +48,7 @@
           <a-form-item name="zone" ref="zone" :label="$t('label.zone')">
             <a-select
               v-model:value="form.zone"
+              @change="onZoneChange"
               showSearch
               optionFilterProp="label"
               :filterOption="(input, option) => {
@@ -105,6 +106,7 @@
           <a-form-item name="zone" ref="zone" :label="$t('label.zone')">
             <a-select
               v-model:value="form.zone"
+              @change="onZoneChange"
               showSearch
               optionFilterProp="label"
               :filterOption="(input, option) => {
@@ -159,6 +161,17 @@
             <a-input v-model:value="form.secondaryStorageNFSPath"/>
           </a-form-item>
         </div>
+        <div v-if="showCopyTemplatesToggle">
+          <a-form-item
+            name="copyTemplatesFromOtherSecondaryStorages"
+            ref="copyTemplatesFromOtherSecondaryStorages"
+            :label="$t('label.copy.templates.from.other.secondary.storages')">
+            <a-switch
+              v-model:checked="form.copyTemplatesFromOtherSecondaryStorages"
+              @change="onCopyTemplatesToggleChanged"
+            />
+          </a-form-item>
+        </div>
         <div :span="24" class="action-button">
           <a-button @click="closeModal">{{ $t('label.cancel') }}</a-button>
           <a-button type="primary" ref="submit" @click="handleSubmit">{{ 
$t('label.ok') }}</a-button>
@@ -191,7 +204,9 @@ export default {
       providers: ['NFS', 'SMB/CIFS', 'S3', 'Swift'],
       zones: [],
       loading: false,
-      secondaryStorageNFSStaging: false
+      secondaryStorageNFSStaging: false,
+      showCopyTemplatesToggle: false,
+      copyTemplatesTouched: false
     }
   },
   created () {
@@ -203,7 +218,8 @@ export default {
       this.formRef = ref()
       this.form = reactive({
         provider: 'NFS',
-        secondaryStorageHttps: true
+        secondaryStorageHttps: true,
+        copyTemplatesFromOtherSecondaryStorages: true
       })
       this.rules = reactive({
         zone: [{ required: true, message: this.$t('label.required') }],
@@ -225,20 +241,56 @@ export default {
     },
     fetchData () {
       this.listZones()
+      this.checkOtherSecondaryStorages()
     },
     closeModal () {
       this.$emit('close-action')
     },
+    fetchCopyTemplatesConfig () {
+      if (!this.form.zone) {
+        return
+      }
+
+      api('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 => {
-        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 => {
+        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) !== '/') {
@@ -362,6 +414,22 @@ export default {
           nfsParams.url = nfsUrl
         }
 
+        if (
+          this.showCopyTemplatesToggle &&
+          this.copyTemplatesTouched
+        ) {
+          const copyTemplatesKey = 'copytemplatesfromothersecondarystorages'
+
+          const detailIdx = Object.keys(data)
+            .filter(k => k.startsWith('details['))
+            .map(k => parseInt(k.match(/details\[(\d+)\]/)[1]))
+            .reduce((a, b) => Math.max(a, b), -1) + 1
+
+          data[`details[${detailIdx}].key`] = copyTemplatesKey
+          data[`details[${detailIdx}].value`] =
+            values.copyTemplatesFromOtherSecondaryStorages.toString()
+        }
+
         this.loading = true
 
         try {
diff --git a/ui/src/views/infra/zone/ZoneWizardAddResources.vue 
b/ui/src/views/infra/zone/ZoneWizardAddResources.vue
index 4bd602f0aca..298cc7fec9d 100644
--- a/ui/src/views/infra/zone/ZoneWizardAddResources.vue
+++ b/ui/src/views/infra/zone/ZoneWizardAddResources.vue
@@ -840,6 +840,13 @@ export default {
           display: {
             secondaryStorageProvider: ['Swift']
           }
+        },
+        {
+          title: 'label.copy.templates.from.other.secondary.storages.add.zone',
+          key: 'copyTemplatesFromOtherSecondaryStorages',
+          required: false,
+          switch: true,
+          checked: this.copytemplate
         }
       ]
     }
@@ -860,7 +867,8 @@ export default {
       }],
       storageProviders: [],
       currentStep: null,
-      options: ['primaryStorageScope', 'primaryStorageProtocol', 'provider', 
'primaryStorageProvider']
+      options: ['primaryStorageScope', 'primaryStorageProtocol', 'provider', 
'primaryStorageProvider'],
+      copytemplate: true
     }
   },
   created () {
@@ -885,6 +893,7 @@ export default {
           primaryStorageScope: null
         })
       }
+      
this.applyCopyTemplatesOptionFromGlobalSettingDuringSecondaryStorageAddition()
     }
   },
   watch: {
@@ -1108,6 +1117,20 @@ export default {
         this.storageProviders = storageProviders
       })
     },
+    applyCopyTemplatesOptionFromGlobalSettingDuringSecondaryStorageAddition () 
{
+      api('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 => {
diff --git a/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue 
b/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue
index a787ad839cd..fbf5e6f5c20 100644
--- a/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue
+++ b/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue
@@ -1580,6 +1580,11 @@ export default {
         params.provider = this.prefillContent.secondaryStorageProvider
         params.zoneid = this.stepData.zoneReturned.id
         params.url = url
+        if (this.prefillContent.copyTemplatesFromOtherSecondaryStorages !== 
undefined) {
+          params['details[0].key'] = 'copytemplatesfromothersecondarystorages'
+          params['details[0].value'] =
+            this.prefillContent.copyTemplatesFromOtherSecondaryStorages
+        }
       } else if (this.prefillContent.secondaryStorageProvider === 'SMB') {
         const nfsServer = this.prefillContent.secondaryStorageServer
         const path = this.prefillContent.secondaryStoragePath

Reply via email to