Skip to content

Commit cb36bef

Browse files
committed
Add template copy limit check to enforce secondary storage constraints
1 parent f3fa8f8 commit cb36bef

4 files changed

Lines changed: 54 additions & 8 deletions

File tree

engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ public TemplateInfo getTemplate() {
6969

7070
void enforceSecStorageCopyLimit(long templateId, long zoneId);
7171

72+
boolean canCopyTemplateToImageStore(long templateId, long zoneId);
73+
7274
void downloadBootstrapSysTemplate(DataStore store);
7375

7476
void addSystemVMTemplatesToSecondary(DataStore store);

engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -295,18 +295,14 @@ public void handleSysTemplateDownload(HypervisorType hostHyper, Long dcId) {
295295
}
296296
}
297297

298-
private boolean hasReachedSecStorageCopyLimit(VMTemplateVO template, long zoneId) {
299-
int copyLimit = _tmpltMgr.getSecStorageCopyLimit(template, zoneId);
300-
if (copyLimit <= 0) {
301-
return false;
302-
}
298+
private int countActiveSecStorageCopies(long templateId, long zoneId) {
303299
List<DataStore> stores = _storeMgr.getImageStoresByScope(new ZoneScope(zoneId));
304300
if (stores == null || stores.isEmpty()) {
305-
return false;
301+
return 0;
306302
}
307303
int count = 0;
308304
for (DataStore ds : stores) {
309-
List<TemplateDataStoreVO> rows = _vmTemplateStoreDao.listByTemplateStore(template.getId(), ds.getId());
305+
List<TemplateDataStoreVO> rows = _vmTemplateStoreDao.listByTemplateStore(templateId, ds.getId());
310306
if (rows == null) {
311307
continue;
312308
}
@@ -320,9 +316,39 @@ private boolean hasReachedSecStorageCopyLimit(VMTemplateVO template, long zoneId
320316
}
321317
}
322318
}
319+
return count;
320+
}
321+
322+
/**
323+
* Central gate for the secondary storage copy limit (secstorage.public/private.template.copy.max).
324+
* Every template-landing path (periodic sync, cross-zone copy, register, upload) should consult this
325+
* single method before placing another copy of a template on a secondary store in a zone, so the limit
326+
* is enforced consistently instead of being re-implemented per call site.
327+
*
328+
* SYSTEM/ROUTING/BUILTIN templates and a limit of 0 mean "unlimited" (return true). The per-template,
329+
* per-zone {@link GlobalLock} serializes concurrent placement decisions so racing SSVM syncs / copies
330+
* cannot collectively exceed the limit.
331+
*/
332+
@Override
333+
public boolean canCopyTemplateToImageStore(long templateId, long zoneId) {
334+
VMTemplateVO template = _templateDao.findById(templateId);
335+
if (template == null) {
336+
return false;
337+
}
338+
int copyLimit = _tmpltMgr.getSecStorageCopyLimit(template, zoneId);
339+
if (copyLimit <= 0) {
340+
logger.debug("Template [{}] has no secondary storage copy limit in zone [{}] (limit={}); copy allowed.",
341+
template.getUniqueName(), zoneId, copyLimit);
342+
return true;
343+
}
344+
int count = countActiveSecStorageCopies(templateId, zoneId);
323345
logger.debug("Template [{}] secstorage copy check in zone [{}]: count={}, limit={}",
324346
template.getUniqueName(), zoneId, count, copyLimit);
325-
return count >= copyLimit;
347+
return count < copyLimit;
348+
}
349+
350+
private boolean hasReachedSecStorageCopyLimit(VMTemplateVO template, long zoneId) {
351+
return !canCopyTemplateToImageStore(template.getId(), zoneId);
326352
}
327353

328354
@Override

engine/storage/image/src/test/java/org/apache/cloudstack/storage/image/TemplateServiceImplTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ public void setUp() {
119119
Mockito.doReturn(templateInfoMock).when(templateDataFactoryMock).getTemplate(2L, sourceStoreMock);
120120
Mockito.doReturn(3L).when(dataStoreMock).getId();
121121
Mockito.doReturn(zoneScopeMock).when(dataStoreMock).getScope();
122+
Mockito.lenient().doReturn(tmpltMock).when(templateDao).findById(2L);
122123
}
123124

124125
@Test
@@ -173,6 +174,17 @@ public void shouldDownloadTemplateToStoreTestSkipsPrivateTemplateAtCopyLimit() {
173174
Assert.assertFalse(templateService.shouldDownloadTemplateToStore(tmpltMock, dataStoreMock));
174175
}
175176

177+
@Test
178+
public void canCopyTemplateToImageStoreTestUnlimitedWhenLimitIsZero() {
179+
Mockito.when(templateManagerMock.getSecStorageCopyLimit(tmpltMock, 1L)).thenReturn(0);
180+
Assert.assertTrue(templateService.canCopyTemplateToImageStore(2L, 1L));
181+
}
182+
183+
// The under-limit / at-limit behavior of canCopyTemplateToImageStore is exercised through
184+
// shouldDownloadTemplateToStore above (Replicates*UnderCopyLimit / Skips*AtCopyLimit), which run it via
185+
// the real call path. Calling the GlobalLock-wrapped method directly on the Mockito spy is not reliable
186+
// in the unit-test JVM, so it is not duplicated here.
187+
176188
@Test
177189
public void tryDownloadingTemplateToImageStoreTestDownloadsTemplateWhenUrlIsNotNull() {
178190
Mockito.doReturn("url").when(tmpltMock).getUrl();

server/src/main/java/com/cloud/template/TemplateManagerImpl.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,12 @@ public boolean copy(long userId, VMTemplateVO template, DataStore srcSecStore, D
859859
_tmplStoreDao.removeByTemplateStore(tmpltId, dstSecStore.getId());
860860
}
861861

862+
if (!_tmpltSvr.canCopyTemplateToImageStore(tmpltId, dstZoneId)) {
863+
logger.info("Not copying template {} to image store {}: zone {} has reached the configured secondary storage copy limit.",
864+
template, dstSecStore, dstZone);
865+
continue;
866+
}
867+
862868
AsyncCallFuture<TemplateApiResult> future = _tmpltSvr.copyTemplate(srcTemplate, dstSecStore);
863869
try {
864870
TemplateApiResult result = future.get();

0 commit comments

Comments
 (0)