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)
This commit is contained in:
Daman Arora 2026-06-29 02:14:27 -04:00 committed by GitHub
parent 06aebb63ee
commit 5ed4894e97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 378 additions and 81 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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) {

View File

@ -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();

View File

@ -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)) {

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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};
}

View File

@ -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