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

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


The following commit(s) were added to refs/heads/main by this push:
     new 5ed4894e97b storage: Add config keys for controlling public/private 
template secondary storage replica counts (#12877)
5ed4894e97b is described below

commit 5ed4894e97b03b8f49cc10e2094bde980f7adfa0
Author: Daman Arora <[email protected]>
AuthorDate: Mon Jun 29 02:14:27 2026 -0400

    storage: Add config keys for controlling public/private template secondary 
storage replica counts (#12877)
    
    Adds two new operator-level configuration keys to control the number of 
secondary storage copies made for public and private templates, decoupling 
replica count from template visibility.
    
    - secstorage.public.template.copy.max (default: 0 = all stores, preserving 
existing behavior)
    - secstorage.private.template.copy.max (default: 1, preserving existing 
behavior)
---
 .../subsystem/api/storage/TemplateService.java     |   6 +
 .../java/com/cloud/template/TemplateManager.java   |  20 ++
 .../storage/image/TemplateServiceImpl.java         | 217 ++++++++++++++++++++-
 .../storage/image/TemplateServiceImplTest.java     |  29 ++-
 .../cloud/storage/ImageStoreUploadMonitorImpl.java |   6 +
 .../cloud/template/HypervisorTemplateAdapter.java  |  19 +-
 .../com/cloud/template/TemplateAdapterBase.java    |  45 ++---
 .../com/cloud/template/TemplateManagerImpl.java    |  37 +++-
 .../template/HypervisorTemplateAdapterTest.java    |  80 ++++----
 9 files changed, 378 insertions(+), 81 deletions(-)

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 269eb4f1c21..2f8d57171bc 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
@@ -67,6 +67,12 @@ public interface TemplateService {
 
     void handleTemplateSync(DataStore store);
 
+    void enforceSecStorageCopyLimit(long templateId, long zoneId);
+
+    boolean canCopyTemplateToImageStore(long templateId, long zoneId);
+
+    void replicateTemplateUpToCap(long templateId, long zoneId);
+
     void downloadBootstrapSysTemplate(DataStore store);
 
     void addSystemVMTemplatesToSecondary(DataStore store);
diff --git 
a/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java 
b/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java
index 24d7bf621f6..8c11fe6c93a 100644
--- 
a/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java
+++ 
b/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java
@@ -45,6 +45,8 @@ import com.cloud.vm.VirtualMachineProfile;
 public interface TemplateManager {
     static final String AllowPublicUserTemplatesCK = 
"allow.public.user.templates";
     static final String TemplatePreloaderPoolSizeCK = 
"template.preloader.pool.size";
+    static final String PublicTemplateSecStorageCopyCK = 
"secstorage.public.template.copy.max";
+    static final String PrivateTemplateSecStorageCopyCK = 
"secstorage.private.template.copy.max";
 
     static final ConfigKey<Boolean> AllowPublicUserTemplates = new 
ConfigKey<Boolean>("Advanced", Boolean.class, AllowPublicUserTemplatesCK, 
"true",
         "If false, users will not be able to create public Templates.", true, 
ConfigKey.Scope.Account);
@@ -64,6 +66,18 @@ public interface TemplateManager {
             true,
             ConfigKey.Scope.Global);
 
+    ConfigKey<Integer> PublicTemplateSecStorageCopy = new 
ConfigKey<Integer>("Advanced", Integer.class,
+            PublicTemplateSecStorageCopyCK, "0",
+            "Maximum number of secondary storage pools to which a public 
template is copied. " +
+            "0 means copy to all secondary storage pools (default behavior).",
+            true, ConfigKey.Scope.Zone);
+
+    ConfigKey<Integer> PrivateTemplateSecStorageCopy = new 
ConfigKey<Integer>("Advanced", Integer.class,
+            PrivateTemplateSecStorageCopyCK, "1",
+            "Maximum number of secondary storage pools to which a private 
template is copied. " +
+            "Default is 1 to preserve existing behavior.",
+            true, ConfigKey.Scope.Zone);
+
     ConfigKey<Integer> VmIsoMaxCount = new ConfigKey<Integer>("Advanced",
             Integer.class,
             "vm.iso.max.count", "1",
@@ -153,6 +167,12 @@ public interface TemplateManager {
 
     List<DataStore> getImageStoreByTemplate(long templateId, Long zoneId);
 
+    /**
+     * Max number of secondary storage copies for the template in this zone; 
{@code 0} means no limit.
+     * SYSTEM/ROUTING/BUILTIN templates are always exempt (returns {@code 0}).
+     */
+    int getSecStorageCopyLimit(VMTemplateVO template, long zoneId);
+
     TemplateInfo prepareIso(long isoId, long dcId, Long hostId, Long poolId);
 
 
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 e29e89cf431..6e32df5d5e3 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
@@ -295,6 +295,171 @@ public class TemplateServiceImpl implements 
TemplateService {
         }
     }
 
+    private int countActiveSecStorageCopies(long templateId, long zoneId) {
+        List<DataStore> stores = _storeMgr.getImageStoresByScope(new 
ZoneScope(zoneId));
+        if (stores == null || stores.isEmpty()) {
+            return 0;
+        }
+        int count = 0;
+        for (DataStore ds : stores) {
+            List<TemplateDataStoreVO> rows = 
_vmTemplateStoreDao.listByTemplateStore(templateId, ds.getId());
+            if (rows == null) {
+                continue;
+            }
+            for (TemplateDataStoreVO row : rows) {
+                State st = row.getState();
+                Status ds_state = row.getDownloadState();
+                if (st != State.Failed && st != State.Destroyed
+                        && ds_state != Status.ABANDONED && ds_state != 
Status.DOWNLOAD_ERROR) {
+                    count++;
+                    break;
+                }
+            }
+        }
+        return count;
+    }
+
+    /**
+     * Central gate for the secondary storage copy limit 
(secstorage.public/private.template.copy.max).
+     * Every template-landing path (periodic sync, cross-zone copy, register, 
upload) should consult this
+     * single method before placing another copy of a template on a secondary 
store in a zone, so the limit
+     * is enforced consistently instead of being re-implemented per call site.
+     *
+     * SYSTEM/ROUTING/BUILTIN templates and a limit of 0 mean "unlimited" 
(return true). The per-template,
+     * per-zone {@link GlobalLock} serializes concurrent placement decisions 
so racing SSVM syncs / copies
+     * cannot collectively exceed the limit.
+     */
+    @Override
+    public boolean canCopyTemplateToImageStore(long templateId, long zoneId) {
+        VMTemplateVO template = _templateDao.findById(templateId);
+        if (template == null) {
+            return false;
+        }
+        int copyLimit = _tmpltMgr.getSecStorageCopyLimit(template, zoneId);
+        if (copyLimit <= 0) {
+            logger.debug("Template [{}] has no secondary storage copy limit in 
zone [{}] (limit={}); copy allowed.",
+                    template.getUniqueName(), zoneId, copyLimit);
+            return true;
+        }
+        int count = countActiveSecStorageCopies(templateId, zoneId);
+        logger.debug("Template [{}] secstorage copy check in zone [{}]: 
count={}, limit={}",
+                template.getUniqueName(), zoneId, count, copyLimit);
+        return count < copyLimit;
+    }
+
+    private boolean hasReachedSecStorageCopyLimit(VMTemplateVO template, long 
zoneId) {
+        return !canCopyTemplateToImageStore(template.getId(), zoneId);
+    }
+
+    @Override
+    public void replicateTemplateUpToCap(long templateId, long zoneId) {
+        VMTemplateVO template = _templateDao.findById(templateId);
+        if (template == null) {
+            return;
+        }
+        int copyLimit = _tmpltMgr.getSecStorageCopyLimit(template, zoneId);
+        if (copyLimit <= 0) {
+            return;
+        }
+        int needed = copyLimit - countActiveSecStorageCopies(templateId, 
zoneId);
+        if (needed <= 0) {
+            return;
+        }
+        List<DataStore> stores = _storeMgr.getImageStoresByScope(new 
ZoneScope(zoneId));
+        if (stores == null || stores.isEmpty()) {
+            return;
+        }
+        int kicked = 0;
+        for (DataStore store : stores) {
+            if (kicked >= needed) {
+                break;
+            }
+            if (hasActiveTemplateCopyOnStore(templateId, store.getId())) {
+                continue;
+            }
+            try {
+                
storageOrchestrator.orchestrateTemplateCopyFromSecondaryStores(templateId, 
store);
+                kicked++;
+            } catch (Exception e) {
+                logger.warn("Failed to proactively replicate template [{}] to 
image store [{}] in zone [{}]: {}",
+                        template.getUniqueName(), store.getName(), zoneId, 
e.getMessage());
+            }
+        }
+    }
+
+    private boolean hasActiveTemplateCopyOnStore(long templateId, long 
storeId) {
+        List<TemplateDataStoreVO> rows = 
_vmTemplateStoreDao.listByTemplateStore(templateId, storeId);
+        if (rows == null) {
+            return false;
+        }
+        for (TemplateDataStoreVO row : rows) {
+            State st = row.getState();
+            Status ds = row.getDownloadState();
+            if (st != State.Failed && st != State.Destroyed
+                    && ds != Status.ABANDONED && ds != Status.DOWNLOAD_ERROR) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void enforceSecStorageCopyLimit(long templateId, long zoneId) {
+        VMTemplateVO template = _templateDao.findById(templateId);
+        if (template == null) {
+            return;
+        }
+        int copyLimit = _tmpltMgr.getSecStorageCopyLimit(template, zoneId);
+        if (copyLimit <= 0) {
+            return;
+        }
+        if (_tmpltMgr.verifyHeuristicRulesForZone(template, zoneId) != null) {
+            return;
+        }
+        GlobalLock lock = GlobalLock.getInternLock("template.copy.limit." + 
templateId + "." + zoneId);
+        try {
+            if (!lock.lock(30)) {
+                logger.warn("Could not acquire lock to enforce secondary 
storage copy limit for template [{}] in zone [{}].",
+                        template.getUniqueName(), zoneId);
+                return;
+            }
+            List<DataStore> stores = _storeMgr.getImageStoresByScope(new 
ZoneScope(zoneId));
+            if (stores == null) {
+                return;
+            }
+            List<TemplateDataStoreVO> removable = new ArrayList<>();
+            for (DataStore ds : stores) {
+                TemplateDataStoreVO ref = 
_vmTemplateStoreDao.findByStoreTemplate(ds.getId(), templateId);
+                if (ref != null
+                        && ref.getState() == State.Ready
+                        && ref.getDownloadState() == Status.DOWNLOADED
+                        && (ref.getRefCnt() == null || ref.getRefCnt() == 0)) {
+                    removable.add(ref);
+                }
+            }
+            int excess = removable.size() - copyLimit;
+            if (excess <= 0) {
+                return;
+            }
+            logger.info("Template [{}] has [{}] removable secondary storage 
copies in zone [{}], limit is [{}]; removing [{}] excess copies.",
+                    template.getUniqueName(), removable.size(), zoneId, 
copyLimit, excess);
+            for (int i = 0; i < excess; i++) {
+                DataStore ds = 
_storeMgr.getDataStore(removable.get(i).getDataStoreId(), DataStoreRole.Image);
+                try {
+                    
deleteTemplateAsync(_templateFactory.getTemplate(templateId, ds));
+                    logger.info("Removed excess copy of template [{}] from 
image store [{}] to honor the secondary storage copy limit.",
+                            template.getUniqueName(), ds.getName());
+                } catch (Exception e) {
+                    logger.warn("Failed to remove excess copy of template [{}] 
from image store [{}]: {}",
+                            template.getUniqueName(), ds, e.getMessage());
+                }
+            }
+        } finally {
+            lock.unlock();
+            lock.releaseRef();
+        }
+    }
+
     protected boolean shouldDownloadTemplateToStore(VMTemplateVO template, 
DataStore store) {
         Long zoneId = store.getScope().getScopeId();
         DataStore directedStore = 
_tmpltMgr.verifyHeuristicRulesForZone(template, zoneId);
@@ -304,6 +469,12 @@ public class TemplateServiceImpl implements 
TemplateService {
             return false;
         }
 
+        if (zoneId != null && hasReachedSecStorageCopyLimit(template, zoneId)) 
{
+            logger.info("Skipping sync of template [{}] to image store [{}]: 
zone [{}] has reached the configured copy limit.",
+                    template.getUniqueName(), store.getName(), zoneId);
+            return false;
+        }
+
         if (template.isPublicTemplate()) {
             logger.debug("Download of template [{}] to image store [{}] cannot 
be skipped, as it is public.", template.getUniqueName(),
                     store.getName());
@@ -328,8 +499,9 @@ public class TemplateServiceImpl implements TemplateService 
{
             return true;
         }
 
-        logger.info("Skipping download of template [{}] to image store [{}].", 
template.getUniqueName(), store.getName());
-        return false;
+        logger.debug("Copying template [{}] to image store [{}] to reach the 
configured secondary storage copy limit in zone [{}].",
+                template.getUniqueName(), store.getName(), zoneId);
+        return true;
     }
 
     @Override
@@ -531,10 +703,13 @@ public class TemplateServiceImpl implements 
TemplateService {
                                         && tmpltStore.getState() == State.Ready
                                         && tmpltStore.getInstallPath() == 
null) {
                                     logger.info("Keep fake entry in template 
store table for migration of previous NFS to object store");
-                                } else {
+                                } else if (tmpltStore.getDownloadState() == 
VMTemplateStorageResourceAssoc.Status.DOWNLOADED
+                                        || tmpltStore.getState() == 
State.Ready) {
                                     logger.info("Removing leftover template {} 
entry from template store table", tmplt);
-                                    // remove those leftover entries
                                     
_vmTemplateStoreDao.remove(tmpltStore.getId());
+                                } else {
+                                    logger.debug("Template {} entry on store 
{} is in pre-download state ({}/{}); not treating as leftover.",
+                                            tmplt, store, 
tmpltStore.getState(), tmpltStore.getDownloadState());
                                 }
                             }
                         }
@@ -556,7 +731,7 @@ public class TemplateServiceImpl implements TemplateService 
{
                         availHypers.add(HypervisorType.None); // bug 9809: 
resume ISO
                         // download.
                         for (VMTemplateVO tmplt : toBeDownloaded) {
-                            // if this is private template, skip sync to a new 
image store
+                            // skip stores excluded by heuristic rules or 
already at the configured copy limit
                             if (!shouldDownloadTemplateToStore(tmplt, store)) {
                                 continue;
                             }
@@ -580,6 +755,12 @@ public class TemplateServiceImpl implements 
TemplateService {
                         }
                     }
 
+                    if (zoneId != null) {
+                        for (VMTemplateVO tmplt : allTemplates) {
+                            enforceSecStorageCopyLimit(tmplt.getId(), zoneId);
+                        }
+                    }
+
                     for (String uniqueName : templateInfos.keySet()) {
                         TemplateProp tInfo = templateInfos.get(uniqueName);
                         if (_tmpltMgr.templateIsDeleteable(tInfo.getId())) {
@@ -965,6 +1146,15 @@ public class TemplateServiceImpl implements 
TemplateService {
             return null;
         }
 
+        try {
+            DataStore destStore = template.getDataStore();
+            if (destStore != null && destStore.getScope() != null && 
destStore.getScope().getScopeId() != null) {
+                enforceSecStorageCopyLimit(template.getId(), 
destStore.getScope().getScopeId());
+            }
+        } catch (Exception e) {
+            logger.warn("Failed to enforce secstorage copy limit after 
template [{}] became Ready: {}", template.getUuid(), e.getMessage());
+        }
+
         if (parentCallback != null) {
             parentCallback.complete(result);
         }
@@ -1406,6 +1596,14 @@ public class TemplateServiceImpl implements 
TemplateService {
                 destTemplate.processEvent(Event.OperationFailed);
             } else {
                 destTemplate.processEvent(Event.OperationSucceeded, 
result.getAnswer());
+                try {
+                    DataStore destStore = destTemplate.getDataStore();
+                    if (destStore != null && destStore.getScope() != null && 
destStore.getScope().getScopeId() != null) {
+                        enforceSecStorageCopyLimit(destTemplate.getId(), 
destStore.getScope().getScopeId());
+                    }
+                } catch (Exception e) {
+                    logger.warn("Failed to enforce secstorage copy limit after 
copy of template [{}] became Ready: {}", destTemplate.getUuid(), 
e.getMessage());
+                }
             }
             future.complete(res);
         } catch (Exception e) {
@@ -1431,6 +1629,15 @@ public class TemplateServiceImpl implements 
TemplateService {
                 destTemplate.processEvent(Event.OperationFailed);
             } else {
                 destTemplate.processEvent(Event.OperationSucceeded, 
result.getAnswer());
+                try {
+                    DataStore destStore = destTemplate.getDataStore();
+                    if (destStore != null && destStore.getScope() != null && 
destStore.getScope().getScopeId() != null) {
+                        replicateTemplateUpToCap(destTemplate.getId(), 
destStore.getScope().getScopeId());
+                    }
+                } catch (Exception e) {
+                    logger.warn("Failed to schedule additional copies for 
cross-zone copied template [{}]: {}",
+                            destTemplate.getUuid(), e.getMessage());
+                }
             }
             future.complete(res);
         } catch (Exception e) {
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 e9eac045869..315fb697894 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
@@ -119,6 +119,7 @@ public class TemplateServiceImplTest {
         
Mockito.doReturn(templateInfoMock).when(templateDataFactoryMock).getTemplate(2L,
 sourceStoreMock);
         Mockito.doReturn(3L).when(dataStoreMock).getId();
         Mockito.doReturn(zoneScopeMock).when(dataStoreMock).getScope();
+        Mockito.lenient().doReturn(tmpltMock).when(templateDao).findById(2L);
     }
 
     @Test
@@ -153,11 +154,37 @@ public class TemplateServiceImplTest {
     }
 
     @Test
-    public void 
shouldDownloadTemplateToStoreTestSkipsPrivateExistingTemplate() {
+    public void 
shouldDownloadTemplateToStoreTestReplicatesPrivateTemplateUnderCopyLimit() {
+        DataStore storeWithCopy = Mockito.mock(DataStore.class);
+        Mockito.doReturn(10L).when(storeWithCopy).getId();
+        Mockito.when(templateManagerMock.getSecStorageCopyLimit(tmpltMock, 
zoneScopeMock.getScopeId())).thenReturn(2);
+        
Mockito.doReturn(List.of(storeWithCopy)).when(dataStoreManagerMock).getImageStoresByScope(Mockito.any());
+        
Mockito.doReturn(List.of(Mockito.mock(TemplateDataStoreVO.class))).when(templateDataStoreDao).listByTemplateStore(2L,
 10L);
         
Mockito.when(templateDataStoreDao.findByTemplateZone(tmpltMock.getId(), 
zoneScopeMock.getScopeId(), 
DataStoreRole.Image)).thenReturn(Mockito.mock(TemplateDataStoreVO.class));
+        
Assert.assertTrue(templateService.shouldDownloadTemplateToStore(tmpltMock, 
dataStoreMock));
+    }
+
+    @Test
+    public void 
shouldDownloadTemplateToStoreTestSkipsPrivateTemplateAtCopyLimit() {
+        DataStore storeWithCopy = Mockito.mock(DataStore.class);
+        Mockito.doReturn(10L).when(storeWithCopy).getId();
+        Mockito.when(templateManagerMock.getSecStorageCopyLimit(tmpltMock, 
zoneScopeMock.getScopeId())).thenReturn(1);
+        
Mockito.doReturn(List.of(storeWithCopy)).when(dataStoreManagerMock).getImageStoresByScope(Mockito.any());
+        
Mockito.doReturn(List.of(Mockito.mock(TemplateDataStoreVO.class))).when(templateDataStoreDao).listByTemplateStore(2L,
 10L);
         
Assert.assertFalse(templateService.shouldDownloadTemplateToStore(tmpltMock, 
dataStoreMock));
     }
 
+    @Test
+    public void canCopyTemplateToImageStoreTestUnlimitedWhenLimitIsZero() {
+        Mockito.when(templateManagerMock.getSecStorageCopyLimit(tmpltMock, 
1L)).thenReturn(0);
+        Assert.assertTrue(templateService.canCopyTemplateToImageStore(2L, 1L));
+    }
+
+    // The under-limit / at-limit behavior of canCopyTemplateToImageStore is 
exercised through
+    // shouldDownloadTemplateToStore above (Replicates*UnderCopyLimit / 
Skips*AtCopyLimit), which run it via
+    // the real call path. Calling the GlobalLock-wrapped method directly on 
the Mockito spy is not reliable
+    // in the unit-test JVM, so it is not duplicated here.
+
     @Test
     public void 
tryDownloadingTemplateToImageStoreTestDownloadsTemplateWhenUrlIsNotNull() {
         Mockito.doReturn("url").when(tmpltMock).getUrl();
diff --git 
a/server/src/main/java/com/cloud/storage/ImageStoreUploadMonitorImpl.java 
b/server/src/main/java/com/cloud/storage/ImageStoreUploadMonitorImpl.java
index c670b631645..64e066f0c91 100755
--- a/server/src/main/java/com/cloud/storage/ImageStoreUploadMonitorImpl.java
+++ b/server/src/main/java/com/cloud/storage/ImageStoreUploadMonitorImpl.java
@@ -574,6 +574,12 @@ public class ImageStoreUploadMonitorImpl extends 
ManagerBase implements ImageSto
                             if (logger.isDebugEnabled()) {
                                 logger.debug("Template {} uploaded 
successfully", tmpTemplate);
                             }
+                            try {
+                                
templateService.replicateTemplateUpToCap(tmpTemplate.getId(), 
vo.getDataCenterId());
+                            } catch (Exception e) {
+                                logger.warn("Failed to schedule additional 
copies for uploaded template [{}] in zone [{}]: {}",
+                                        tmpTemplate.getUuid(), 
vo.getDataCenterId(), e.getMessage());
+                            }
                             break;
                         case IN_PROGRESS:
                             if 
(!checkAndUpdateTemplateResourceLimit(tmpTemplate, tmpTemplateDataStore, 
answer)) {
diff --git 
a/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java 
b/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java
index fdde8f47a67..9417d63c594 100644
--- a/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java
+++ b/server/src/main/java/com/cloud/template/HypervisorTemplateAdapter.java
@@ -19,10 +19,10 @@ package com.cloud.template;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
-import java.util.HashSet;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.Set;
+import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import java.util.stream.Collectors;
 
@@ -264,9 +264,10 @@ public class HypervisorTemplateAdapter extends 
TemplateAdapterBase {
 
             if (imageStore == null) {
                 List<DataStore> imageStores = 
getImageStoresThrowsExceptionIfNotFound(zoneId, profile);
-                standardImageStoreAllocation(imageStores, template);
+                standardImageStoreAllocation(imageStores, template, zoneId);
             } else {
-                validateSecondaryStorageAndCreateTemplate(List.of(imageStore), 
template, null);
+                int copyLimit = getSecStorageCopyLimit(template, zoneId);
+                validateSecondaryStorageAndCreateTemplate(List.of(imageStore), 
template, new HashMap<>(), copyLimit);
             }
         }
     }
@@ -279,17 +280,17 @@ public class HypervisorTemplateAdapter extends 
TemplateAdapterBase {
         return imageStores;
     }
 
-    protected void standardImageStoreAllocation(List<DataStore> imageStores, 
VMTemplateVO template) {
-        Set<Long> zoneSet = new HashSet<Long>();
+    protected void standardImageStoreAllocation(List<DataStore> imageStores, 
VMTemplateVO template, long zoneId) {
+        int copyLimit = getSecStorageCopyLimit(template, zoneId);
         Collections.shuffle(imageStores);
-        validateSecondaryStorageAndCreateTemplate(imageStores, template, 
zoneSet);
+        validateSecondaryStorageAndCreateTemplate(imageStores, template, new 
HashMap<>(), copyLimit);
     }
 
-    protected void validateSecondaryStorageAndCreateTemplate(List<DataStore> 
imageStores, VMTemplateVO template, Set<Long> zoneSet) {
+    protected void validateSecondaryStorageAndCreateTemplate(List<DataStore> 
imageStores, VMTemplateVO template, Map<Long, Integer> zoneCopyCount, int 
copyLimit) {
         for (DataStore imageStore : imageStores) {
             Long zoneId = imageStore.getScope().getScopeId();
 
-            if (!isZoneAndImageStoreAvailable(imageStore, zoneId, zoneSet, 
isPrivateTemplate(template))) {
+            if (!isZoneAndImageStoreAvailable(imageStore, zoneId, 
zoneCopyCount, copyLimit)) {
                 continue;
             }
 
diff --git a/server/src/main/java/com/cloud/template/TemplateAdapterBase.java 
b/server/src/main/java/com/cloud/template/TemplateAdapterBase.java
index 8f508135605..1aa3bedbdf0 100644
--- a/server/src/main/java/com/cloud/template/TemplateAdapterBase.java
+++ b/server/src/main/java/com/cloud/template/TemplateAdapterBase.java
@@ -20,11 +20,9 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 
 import javax.inject.Inject;
 
@@ -169,7 +167,11 @@ public abstract class TemplateAdapterBase extends 
AdapterBase implements Templat
         return heuristicRuleHelper.getImageStoreIfThereIsHeuristicRule(zoneId, 
heuristicType, template);
     }
 
-    protected boolean isZoneAndImageStoreAvailable(DataStore imageStore, Long 
zoneId, Set<Long> zoneSet, boolean isTemplatePrivate) {
+    protected int getSecStorageCopyLimit(VMTemplateVO template, long zoneId) {
+        return templateMgr.getSecStorageCopyLimit(template, zoneId);
+    }
+
+    protected boolean isZoneAndImageStoreAvailable(DataStore imageStore, Long 
zoneId, Map<Long, Integer> zoneCopyCount, int copyLimit) {
         if (zoneId == null) {
             logger.warn(String.format("Zone ID is null, cannot allocate 
ISO/template in image store [%s].", imageStore));
             return false;
@@ -191,33 +193,30 @@ public abstract class TemplateAdapterBase extends 
AdapterBase implements Templat
             return false;
         }
 
-        if (zoneSet == null) {
-            logger.info(String.format("Zone set is null; therefore, the 
ISO/template should be allocated in every secondary storage of zone [%s].", 
zone));
-            return true;
-        }
-
-        if (isTemplatePrivate && zoneSet.contains(zoneId)) {
-            logger.info(String.format("The template is private and it is 
already allocated in a secondary storage in zone [%s]; therefore, image store 
[%s] will be skipped.",
-                    zone, imageStore));
+        int currentCount = zoneCopyCount.getOrDefault(zoneId, 0);
+        if (copyLimit > 0 && currentCount >= copyLimit) {
+            logger.info("Copy limit of {} reached for zone [{}]; skipping 
image store [{}].", copyLimit, zone, imageStore);
             return false;
         }
 
-        logger.info(String.format("Private template will be allocated in image 
store [%s] in zone [%s].", imageStore, zone));
-        zoneSet.add(zoneId);
+        zoneCopyCount.put(zoneId, currentCount + 1);
         return true;
     }
 
     /**
-     * If the template/ISO is marked as private, then it is allocated to a 
random secondary storage; otherwise, allocates to every storage pool in every 
zone given by the
-     * {@link TemplateProfile#getZoneIdList()}.
+     * Allocates the template/ISO to a single image store - the one the file 
will be uploaded to. The upload can only
+     * target one secondary store, so additional copies (up to the configured 
secstorage.public/private.template.copy.max)
+     * are propagated later by template sync instead of being pre-allocated 
here as empty placeholder entries that never
+     * receive the data.
      */
     protected void postUploadAllocation(List<DataStore> imageStores, 
VMTemplateVO template, List<TemplateOrVolumePostUploadCommand> payloads) {
-        Set<Long> zoneSet = new HashSet<>();
+        Map<Long, Integer> zoneCopyCount = new HashMap<>();
         Collections.shuffle(imageStores);
         for (DataStore imageStore : imageStores) {
             Long zoneId_is = imageStore.getScope().getScopeId();
+            int copyLimit = zoneId_is == null ? 0 : 
getSecStorageCopyLimit(template, zoneId_is);
 
-            if (!isZoneAndImageStoreAvailable(imageStore, zoneId_is, zoneSet, 
isPrivateTemplate(template))) {
+            if (!isZoneAndImageStoreAvailable(imageStore, zoneId_is, 
zoneCopyCount, copyLimit)) {
                 continue;
             }
 
@@ -251,15 +250,11 @@ public abstract class TemplateAdapterBase extends 
AdapterBase implements Templat
             payload.setRequiresHvm(template.requiresHvm());
             payload.setDescription(template.getDisplayText());
             payloads.add(payload);
-        }
-    }
 
-    protected boolean isPrivateTemplate(VMTemplateVO template){
-        // if public OR featured OR system template
-        if (template.isPublicTemplate() || template.isFeatured() || 
template.getTemplateType() == TemplateType.SYSTEM) {
-            return false;
-        } else {
-            return true;
+            // The file can only be uploaded to a single secondary store. 
Allocate just this one; additional copies
+            // up to the configured secondary storage copy limit are 
propagated afterwards by template sync, so we do
+            // not create empty placeholder template_store_ref rows on the 
other stores.
+            break;
         }
     }
 
diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java 
b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
index 6cac485c4e1..d9049af679b 100755
--- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
+++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java
@@ -943,6 +943,12 @@ public class TemplateManagerImpl extends ManagerBase 
implements TemplateManager,
                 _tmplStoreDao.removeByTemplateStore(tmpltId, 
dstSecStore.getId());
             }
 
+            if (!_tmpltSvr.canCopyTemplateToImageStore(tmpltId, dstZoneId)) {
+                logger.info("Not copying template {} to image store {}: zone 
{} has reached the configured secondary storage copy limit.",
+                        template, dstSecStore, dstZone);
+                continue;
+            }
+
             AsyncCallFuture<TemplateApiResult> future = 
_tmpltSvr.copyTemplate(srcTemplate, dstSecStore);
             try {
                 TemplateApiResult result = future.get();
@@ -1914,6 +1920,13 @@ public class TemplateManagerImpl extends ManagerBase 
implements TemplateManager,
             _launchPermissionDao.removeAllPermissions(id);
             _messageBus.publish(_name, 
TemplateManager.MESSAGE_RESET_TEMPLATE_PERMISSION_EVENT, PublishScope.LOCAL, 
template.getId());
         }
+
+        if (isPublic != null || isFeatured != null || 
"reset".equalsIgnoreCase(operation)) {
+            for (VMTemplateZoneVO templateZone : 
_tmpltZoneDao.listByTemplateId(template.getId())) {
+                _tmpltSvr.enforceSecStorageCopyLimit(template.getId(), 
templateZone.getZoneId());
+                _tmpltSvr.replicateTemplateUpToCap(template.getId(), 
templateZone.getZoneId());
+            }
+        }
         return true;
     }
 
@@ -1931,10 +1944,10 @@ public class TemplateManagerImpl extends ManagerBase 
implements TemplateManager,
         Account caller = CallContext.current().getCallingAccount();
         boolean kvmSnapshotOnlyInPrimaryStorage = false;
         SnapshotInfo snapInfo = null;
+        long zoneId = 0;
 
         try {
             TemplateInfo tmplInfo = _tmplFactory.getTemplate(templateId, 
DataStoreRole.Image);
-            long zoneId = 0;
             if (snapshotId != null) {
                 snapshot = _snapshotDao.findById(snapshotId);
                 if (command.getZoneId() == null) {
@@ -2074,6 +2087,12 @@ public class TemplateManagerImpl extends ManagerBase 
implements TemplateManager,
         }
 
         if (privateTemplate != null) {
+            try {
+                _tmpltSvr.replicateTemplateUpToCap(privateTemplate.getId(), 
zoneId);
+            } catch (Exception e) {
+                logger.warn("Failed to schedule additional copies for template 
[{}] in zone [{}]: {}",
+                        privateTemplate.getUniqueName(), zoneId, 
e.getMessage());
+            }
             return privateTemplate;
         } else {
             throw new CloudRuntimeException("Failed to create a Template");
@@ -2397,6 +2416,20 @@ public class TemplateManagerImpl extends ManagerBase 
implements TemplateManager,
         return stores;
     }
 
+    @Override
+    public int getSecStorageCopyLimit(VMTemplateVO template, long zoneId) {
+        if (template == null) {
+            return 0;
+        }
+        TemplateType type = template.getTemplateType();
+        if (type == TemplateType.SYSTEM || type == TemplateType.ROUTING || 
type == TemplateType.BUILTIN) {
+            return 0;
+        }
+        return template.isPublicTemplate()
+                ? PublicTemplateSecStorageCopy.valueIn(zoneId)
+                : PrivateTemplateSecStorageCopy.valueIn(zoneId);
+    }
+
     @Override
     @ActionEvent(eventType = EventTypes.EVENT_ISO_UPDATE, eventDescription = 
"Updating ISO", async = false)
     public VMTemplateVO updateTemplate(UpdateIsoCmd cmd) {
@@ -2718,6 +2751,8 @@ public class TemplateManagerImpl extends ManagerBase 
implements TemplateManager,
                 TemplatePreloaderPoolSize,
                 ValidateUrlIsResolvableBeforeRegisteringTemplate,
                 TemplateDeleteFromPrimaryStorage,
+                PublicTemplateSecStorageCopy,
+                PrivateTemplateSecStorageCopy,
                 VmIsoMaxCount};
     }
 
diff --git 
a/server/src/test/java/com/cloud/template/HypervisorTemplateAdapterTest.java 
b/server/src/test/java/com/cloud/template/HypervisorTemplateAdapterTest.java
index e2a97be469f..4cd48e686b0 100644
--- a/server/src/test/java/com/cloud/template/HypervisorTemplateAdapterTest.java
+++ b/server/src/test/java/com/cloud/template/HypervisorTemplateAdapterTest.java
@@ -32,10 +32,8 @@ import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.concurrent.ExecutionException;
 
 import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
@@ -339,7 +337,7 @@ public class HypervisorTemplateAdapterTest {
         Mockito.when(templateProfileMock.getZoneIdList()).thenReturn(zoneIds);
         
Mockito.doReturn(null).when(_adapter).getImageStoresThrowsExceptionIfNotFound(Mockito.any(Long.class),
 Mockito.any(TemplateProfile.class));
         
Mockito.doReturn(null).when(_templateMgr).verifyHeuristicRulesForZone(Mockito.any(VMTemplateVO.class),
 Mockito.anyLong());
-        
Mockito.doNothing().when(_adapter).standardImageStoreAllocation(Mockito.isNull(),
 Mockito.any(VMTemplateVO.class));
+        
Mockito.doNothing().when(_adapter).standardImageStoreAllocation(Mockito.isNull(),
 Mockito.any(VMTemplateVO.class), Mockito.anyLong());
 
         _adapter.createTemplateWithinZones(templateProfileMock, 
vmTemplateVOMock);
 
@@ -355,11 +353,11 @@ public class HypervisorTemplateAdapterTest {
         Mockito.when(templateProfileMock.getZoneIdList()).thenReturn(zoneIds);
         
Mockito.doReturn(null).when(_adapter).getImageStoresThrowsExceptionIfNotFound(Mockito.any(Long.class),
 Mockito.any(TemplateProfile.class));
         
Mockito.doReturn(null).when(_templateMgr).verifyHeuristicRulesForZone(Mockito.any(VMTemplateVO.class),
 Mockito.anyLong());
-        
Mockito.doNothing().when(_adapter).standardImageStoreAllocation(Mockito.isNull(),
 Mockito.any(VMTemplateVO.class));
+        
Mockito.doNothing().when(_adapter).standardImageStoreAllocation(Mockito.isNull(),
 Mockito.any(VMTemplateVO.class), Mockito.anyLong());
 
         _adapter.createTemplateWithinZones(templateProfileMock, 
vmTemplateVOMock);
 
-        Mockito.verify(_adapter, 
Mockito.times(1)).standardImageStoreAllocation(Mockito.isNull(), 
Mockito.any(VMTemplateVO.class));
+        Mockito.verify(_adapter, 
Mockito.times(1)).standardImageStoreAllocation(Mockito.isNull(), 
Mockito.any(VMTemplateVO.class), Mockito.anyLong());
     }
 
     @Test
@@ -371,11 +369,11 @@ public class HypervisorTemplateAdapterTest {
 
         Mockito.when(templateProfileMock.getZoneIdList()).thenReturn(zoneIds);
         
Mockito.doReturn(dataStoreMock).when(_templateMgr).verifyHeuristicRulesForZone(Mockito.any(VMTemplateVO.class),
 Mockito.anyLong());
-        
Mockito.doNothing().when(_adapter).validateSecondaryStorageAndCreateTemplate(Mockito.any(List.class),
 Mockito.any(VMTemplateVO.class), Mockito.isNull());
+        
Mockito.doNothing().when(_adapter).validateSecondaryStorageAndCreateTemplate(Mockito.any(List.class),
 Mockito.any(VMTemplateVO.class), Mockito.any(Map.class), Mockito.anyInt());
 
         _adapter.createTemplateWithinZones(templateProfileMock, 
vmTemplateVOMock);
 
-        Mockito.verify(_adapter, 
Mockito.times(1)).validateSecondaryStorageAndCreateTemplate(Mockito.any(List.class),
 Mockito.any(VMTemplateVO.class), Mockito.isNull());
+        Mockito.verify(_adapter, 
Mockito.times(1)).validateSecondaryStorageAndCreateTemplate(Mockito.any(List.class),
 Mockito.any(VMTemplateVO.class), Mockito.any(Map.class), Mockito.anyInt());
     }
 
     @Test(expected = CloudRuntimeException.class)
@@ -411,11 +409,8 @@ public class HypervisorTemplateAdapterTest {
     @Test
     public void 
isZoneAndImageStoreAvailableTestZoneIdIsNullShouldReturnFalse() {
         DataStore dataStoreMock = Mockito.mock(DataStore.class);
-        Long zoneId = null;
-        Set<Long> zoneSet = null;
-        boolean isTemplatePrivate = false;
 
-        boolean result = _adapter.isZoneAndImageStoreAvailable(dataStoreMock, 
zoneId, zoneSet, isTemplatePrivate);
+        boolean result = _adapter.isZoneAndImageStoreAvailable(dataStoreMock, 
null, new HashMap<>(), 0);
 
         Mockito.verify(loggerMock, Mockito.times(1)).warn(String.format("Zone 
ID is null, cannot allocate ISO/template in image store [%s].", dataStoreMock));
         Assert.assertFalse(result);
@@ -425,13 +420,10 @@ public class HypervisorTemplateAdapterTest {
     public void isZoneAndImageStoreAvailableTestZoneIsNullShouldReturnFalse() {
         DataStore dataStoreMock = Mockito.mock(DataStore.class);
         Long zoneId = 1L;
-        Set<Long> zoneSet = null;
-        boolean isTemplatePrivate = false;
-        DataCenterVO dataCenterVOMock = null;
 
-        
Mockito.when(_dcDao.findById(Mockito.anyLong())).thenReturn(dataCenterVOMock);
+        Mockito.when(_dcDao.findById(Mockito.anyLong())).thenReturn(null);
 
-        boolean result = _adapter.isZoneAndImageStoreAvailable(dataStoreMock, 
zoneId, zoneSet, isTemplatePrivate);
+        boolean result = _adapter.isZoneAndImageStoreAvailable(dataStoreMock, 
zoneId, new HashMap<>(), 0);
 
         Mockito.verify(loggerMock, Mockito.times(1)).warn("Unable to find zone 
by id [{}], so skip downloading template to its image store [{}].",
                 zoneId, dataStoreMock);
@@ -442,14 +434,12 @@ public class HypervisorTemplateAdapterTest {
     public void 
isZoneAndImageStoreAvailableTestZoneIsDisabledShouldReturnFalse() {
         DataStore dataStoreMock = Mockito.mock(DataStore.class);
         Long zoneId = 1L;
-        Set<Long> zoneSet = null;
-        boolean isTemplatePrivate = false;
         DataCenterVO dataCenterVOMock = Mockito.mock(DataCenterVO.class);
 
         
Mockito.when(_dcDao.findById(Mockito.anyLong())).thenReturn(dataCenterVOMock);
         
Mockito.when(dataCenterVOMock.getAllocationState()).thenReturn(Grouping.AllocationState.Disabled);
 
-        boolean result = _adapter.isZoneAndImageStoreAvailable(dataStoreMock, 
zoneId, zoneSet, isTemplatePrivate);
+        boolean result = _adapter.isZoneAndImageStoreAvailable(dataStoreMock, 
zoneId, new HashMap<>(), 0);
 
         Mockito.verify(loggerMock, Mockito.times(1)).info("Zone [{}] is 
disabled. Skip downloading template to its image store [{}].", 
dataCenterVOMock, dataStoreMock);
         Assert.assertFalse(result);
@@ -459,15 +449,13 @@ public class HypervisorTemplateAdapterTest {
     public void 
isZoneAndImageStoreAvailableTestImageStoreDoesNotHaveEnoughCapacityShouldReturnFalse()
 {
         DataStore dataStoreMock = Mockito.mock(DataStore.class);
         Long zoneId = 1L;
-        Set<Long> zoneSet = null;
-        boolean isTemplatePrivate = false;
         DataCenterVO dataCenterVOMock = Mockito.mock(DataCenterVO.class);
 
         
Mockito.when(_dcDao.findById(Mockito.anyLong())).thenReturn(dataCenterVOMock);
         
Mockito.when(dataCenterVOMock.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);
         
Mockito.when(statsCollectorMock.imageStoreHasEnoughCapacity(any(DataStore.class))).thenReturn(false);
 
-        boolean result = _adapter.isZoneAndImageStoreAvailable(dataStoreMock, 
zoneId, zoneSet, isTemplatePrivate);
+        boolean result = _adapter.isZoneAndImageStoreAvailable(dataStoreMock, 
zoneId, new HashMap<>(), 0);
 
         Mockito.verify(loggerMock, times(1)).info("Image store doesn't have 
enough capacity. Skip downloading template to this image store [{}].",
                 dataStoreMock);
@@ -475,60 +463,72 @@ public class HypervisorTemplateAdapterTest {
     }
 
     @Test
-    public void 
isZoneAndImageStoreAvailableTestImageStoreHasEnoughCapacityAndZoneSetIsNullShouldReturnTrue()
 {
+    public void 
isZoneAndImageStoreAvailableTestReplicaLimitZeroShouldCopyToAllStores() {
         DataStore dataStoreMock = Mockito.mock(DataStore.class);
         Long zoneId = 1L;
-        Set<Long> zoneSet = null;
-        boolean isTemplatePrivate = false;
         DataCenterVO dataCenterVOMock = Mockito.mock(DataCenterVO.class);
+        Map<Long, Integer> zoneCopyCount = new HashMap<>();
+        zoneCopyCount.put(zoneId, 999);
 
         
Mockito.when(_dcDao.findById(Mockito.anyLong())).thenReturn(dataCenterVOMock);
         
Mockito.when(dataCenterVOMock.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);
         
Mockito.when(statsCollectorMock.imageStoreHasEnoughCapacity(any(DataStore.class))).thenReturn(true);
 
-        boolean result = _adapter.isZoneAndImageStoreAvailable(dataStoreMock, 
zoneId, zoneSet, isTemplatePrivate);
+        boolean result = _adapter.isZoneAndImageStoreAvailable(dataStoreMock, 
zoneId, zoneCopyCount, 0);
 
-        Mockito.verify(loggerMock, times(1)).info(String.format("Zone set is 
null; therefore, the ISO/template should be allocated in every secondary 
storage " +
-                "of zone [%s].", dataCenterVOMock));
         Assert.assertTrue(result);
+        Assert.assertEquals(1000, (int) zoneCopyCount.get(zoneId));
     }
 
     @Test
-    public void 
isZoneAndImageStoreAvailableTestTemplateIsPrivateAndItIsAlreadyAllocatedToTheSameZoneShouldReturnFalse()
 {
+    public void 
isZoneAndImageStoreAvailableTestReplicaLimitReachedShouldReturnFalse() {
         DataStore dataStoreMock = Mockito.mock(DataStore.class);
         Long zoneId = 1L;
-        Set<Long> zoneSet = Set.of(1L);
-        boolean isTemplatePrivate = true;
         DataCenterVO dataCenterVOMock = Mockito.mock(DataCenterVO.class);
+        Map<Long, Integer> zoneCopyCount = new HashMap<>();
+        zoneCopyCount.put(zoneId, 1);
 
         
Mockito.when(_dcDao.findById(Mockito.anyLong())).thenReturn(dataCenterVOMock);
         
Mockito.when(dataCenterVOMock.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);
         
Mockito.when(statsCollectorMock.imageStoreHasEnoughCapacity(any(DataStore.class))).thenReturn(true);
 
-        boolean result = _adapter.isZoneAndImageStoreAvailable(dataStoreMock, 
zoneId, zoneSet, isTemplatePrivate);
+        boolean result = _adapter.isZoneAndImageStoreAvailable(dataStoreMock, 
zoneId, zoneCopyCount, 1);
 
-        Mockito.verify(loggerMock, times(1)).info(String.format("The template 
is private and it is already allocated in a secondary storage in zone [%s]; " +
-                "therefore, image store [%s] will be skipped.", 
dataCenterVOMock, dataStoreMock));
+        Mockito.verify(loggerMock, times(1)).info("Copy limit of {} reached 
for zone [{}]; skipping image store [{}].", 1, dataCenterVOMock, dataStoreMock);
         Assert.assertFalse(result);
     }
 
     @Test
-    public void 
isZoneAndImageStoreAvailableTestTemplateIsPrivateAndItIsNotAlreadyAllocatedToTheSameZoneShouldReturnTrue()
 {
+    public void 
isZoneAndImageStoreAvailableTestReplicaLimitNotYetReachedShouldReturnTrueAndIncrementCount()
 {
         DataStore dataStoreMock = Mockito.mock(DataStore.class);
         Long zoneId = 1L;
-        Set<Long> zoneSet = new HashSet<>();
-        boolean isTemplatePrivate = true;
         DataCenterVO dataCenterVOMock = Mockito.mock(DataCenterVO.class);
+        Map<Long, Integer> zoneCopyCount = new HashMap<>();
 
         
Mockito.when(_dcDao.findById(Mockito.anyLong())).thenReturn(dataCenterVOMock);
         
Mockito.when(dataCenterVOMock.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);
         
Mockito.when(statsCollectorMock.imageStoreHasEnoughCapacity(any(DataStore.class))).thenReturn(true);
 
-        boolean result = _adapter.isZoneAndImageStoreAvailable(dataStoreMock, 
zoneId, zoneSet, isTemplatePrivate);
+        boolean result = _adapter.isZoneAndImageStoreAvailable(dataStoreMock, 
zoneId, zoneCopyCount, 2);
 
-        Mockito.verify(loggerMock, times(1)).info(String.format("Private 
template will be allocated in image store [%s] in zone [%s].",
-                dataStoreMock, dataCenterVOMock));
         Assert.assertTrue(result);
+        Assert.assertEquals(1, (int) zoneCopyCount.get(zoneId));
+    }
+
+    @Test
+    public void 
isZoneAndImageStoreAvailableTestReplicaLimitOfTwoShouldCopyToExactlyTwoStores() 
{
+        Long zoneId = 1L;
+        DataCenterVO dataCenterVOMock = Mockito.mock(DataCenterVO.class);
+        Map<Long, Integer> zoneCopyCount = new HashMap<>();
+
+        
Mockito.when(_dcDao.findById(Mockito.anyLong())).thenReturn(dataCenterVOMock);
+        
Mockito.when(dataCenterVOMock.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);
+        
Mockito.when(statsCollectorMock.imageStoreHasEnoughCapacity(any(DataStore.class))).thenReturn(true);
+
+        
Assert.assertTrue(_adapter.isZoneAndImageStoreAvailable(Mockito.mock(DataStore.class),
 zoneId, zoneCopyCount, 2));
+        
Assert.assertTrue(_adapter.isZoneAndImageStoreAvailable(Mockito.mock(DataStore.class),
 zoneId, zoneCopyCount, 2));
+        
Assert.assertFalse(_adapter.isZoneAndImageStoreAvailable(Mockito.mock(DataStore.class),
 zoneId, zoneCopyCount, 2));
+        Assert.assertEquals(2, (int) zoneCopyCount.get(zoneId));
     }
 
     @Test

Reply via email to