Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,9 @@ public interface StorageManager extends StorageService {
"storage.pool.host.connect.workers", "1",
"Number of worker threads to be used to connect hosts to a primary storage", true);

ConfigKey<Boolean> COPY_PUBLIC_TEMPLATES_FROM_OTHER_STORAGES = new ConfigKey<>(Boolean.class, "copy.public.templates.from.other.storages",
"Storage", "true", "Allow SSVMs to try copying public templates from one secondary storage to another instead of downloading them from the source.",
ConfigKey<Boolean> COPY_TEMPLATES_FROM_OTHER_SECONDARY_STORAGES = new ConfigKey<>(Boolean.class, "copy.templates.from.other.secondary.storages",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will not replace the existing config but will add a new one. We may need a change in the upgrade path

"Storage", "true", "Allow templates to be copied from existing Secondary Storage servers (within the same zone or across zones) " +
"when adding a new Secondary Storage, instead of downloading them from the source URL.",
true, ConfigKey.Scope.Zone, null);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

import javax.inject.Inject;

import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.orchestration.service.StorageOrchestrationService;
import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult;
import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult;
Expand Down Expand Up @@ -615,28 +616,110 @@ protected void tryDownloadingTemplateToImageStore(VMTemplateVO tmplt, DataStore
}

protected boolean tryCopyingTemplateToImageStore(VMTemplateVO tmplt, DataStore destStore) {
Long zoneId = destStore.getScope().getScopeId();
List<DataStore> storesInZone = _storeMgr.getImageStoresByZoneIds(zoneId);
for (DataStore sourceStore : storesInZone) {
Long destZoneId = destStore.getScope().getScopeId();

List<DataStore> storesInSameZone = _storeMgr.getImageStoresByZoneIds(destZoneId);
if (searchAndCopyWithinZone(tmplt, destStore, storesInSameZone)) {
return true;
}

logger.debug("Template [{}] not found in any image store of zone [{}]. Checking other zones",
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log message should end with a period for consistency with other logging statements in this class.

Suggested change
logger.debug("Template [{}] not found in any image store of zone [{}]. Checking other zones",
logger.debug("Template [{}] not found in any image store of zone [{}]. Checking other zones.",

Copilot uses AI. Check for mistakes.
tmplt.getUniqueName(), destZoneId);

return searchAndCopyAcrossZones(tmplt, destStore, destZoneId);
}

private boolean searchAndCopyAcrossZones(VMTemplateVO tmplt, DataStore destStore, Long destZoneId) {
List<Long> allZoneIds = _dcDao.listAllIds();
for (Long otherZoneId : allZoneIds) {
if (otherZoneId.equals(destZoneId)) {
continue;
}

List<DataStore> storesInOtherZone = _storeMgr.getImageStoresByZoneIds(otherZoneId);
logger.debug("Checking zone [{}] for template [{}]...", otherZoneId, tmplt.getUniqueName());

if (storesInOtherZone == null || storesInOtherZone.isEmpty()) {
logger.debug("Zone [{}] has no image stores. Skipping.", otherZoneId);
continue;
}

DataStore sourceStore = findTemplateInStores(tmplt, storesInOtherZone);
if (sourceStore == null) {
logger.debug("Template [{}] not found in any image store of zone [{}].",
tmplt.getUniqueName(), otherZoneId);
continue;
}

logger.info("Template [{}] found in zone [{}]. Initiating cross-zone copy to zone [{}].",
tmplt.getUniqueName(), otherZoneId, destZoneId);

return copyTemplateAcrossZones(sourceStore, destStore, tmplt);
}

logger.debug("Template [{}] was not found in any zone. Cannot perform zone-to-zone copy.",
tmplt.getUniqueName());
return false;
}

private boolean searchAndCopyWithinZone(VMTemplateVO tmplt, DataStore destStore, List<DataStore> stores) {
for (DataStore sourceStore : stores) {
Map<String, TemplateProp> existingTemplatesInSourceStore = listTemplate(sourceStore);
if (existingTemplatesInSourceStore == null || !existingTemplatesInSourceStore.containsKey(tmplt.getUniqueName())) {
logger.debug("Template [{}] does not exist on image store [{}]; searching on another one.",
if (existingTemplatesInSourceStore == null ||
!existingTemplatesInSourceStore.containsKey(tmplt.getUniqueName())) {
logger.debug("Template [{}] does not exist on image store [{}]; searching another.",
tmplt.getUniqueName(), sourceStore.getName());
continue;
}

TemplateObject sourceTmpl = (TemplateObject) _templateFactory.getTemplate(tmplt.getId(), sourceStore);
if (sourceTmpl.getInstallPath() == null) {
logger.warn("Can not copy template [{}] from image store [{}], as it returned a null install path.", tmplt.getUniqueName(),
sourceStore.getName());
logger.warn("Cannot copy template [{}] from image store [{}]; install path is null.",
tmplt.getUniqueName(), sourceStore.getName());
continue;
}

storageOrchestrator.orchestrateTemplateCopyToImageStore(sourceTmpl, destStore);
return true;
}
logger.debug("Can't copy template [{}] from another image store.", tmplt.getUniqueName());
return false;
}

private DataStore findTemplateInStores(VMTemplateVO tmplt, List<DataStore> stores) {
for (DataStore store : stores) {
Map<String, TemplateProp> templates = listTemplate(store);
if (templates != null && templates.containsKey(tmplt.getUniqueName())) {
return store;
}
}
return null;
}
Comment on lines +688 to +696
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The findTemplateInStores method doesn't validate the template's install path before returning the store. In searchAndCopyWithinZone (lines 676-679), there's a check to ensure the install path is not null before attempting the copy. Consider adding a similar validation in findTemplateInStores or in copyTemplateAcrossZones to avoid attempting cross-zone copies with templates that have null install paths. This would provide consistency with the same-zone copy logic and potentially avoid failed copy attempts.

Copilot uses AI. Check for mistakes.

private boolean copyTemplateAcrossZones(DataStore sourceStore,
DataStore destStore,
VMTemplateVO tmplt) {
Long dstZoneId = destStore.getScope().getScopeId();
DataCenterVO dstZone = _dcDao.findById(dstZoneId);

if (dstZone == null) {
logger.warn("Destination zone [{}] not found for template [{}]",
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log message should end with a period for consistency with other logging statements in this class.

Suggested change
logger.warn("Destination zone [{}] not found for template [{}]",
logger.warn("Destination zone [{}] not found for template [{}].",

Copilot uses AI. Check for mistakes.
dstZoneId, tmplt.getUniqueName());
return false;
}

try {
Long userId = CallContext.current().getCallingUserId();
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable 'userId' is only assigned values of primitive type and is never 'null', but it is declared with the boxed type 'Long'.

Suggested change
Long userId = CallContext.current().getCallingUserId();
long userId = CallContext.current().getCallingUserId();

Copilot uses AI. Check for mistakes.
return _tmpltMgr.copy(userId, tmplt, sourceStore, dstZone);
} catch (Exception e) {
logger.error("Failed to copy template [{}] from zone [{}] to zone [{}]",
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log message should end with a period for consistency with other logging statements in this class.

Suggested change
logger.error("Failed to copy template [{}] from zone [{}] to zone [{}]",
logger.error("Failed to copy template [{}] from zone [{}] to zone [{}].",

Copilot uses AI. Check for mistakes.
tmplt.getUniqueName(),
sourceStore.getScope().getScopeId(),
dstZoneId,
e);
return false;
}
}

@Override
public AsyncCallFuture<TemplateApiResult> copyTemplateToImageStore(DataObject source, DataStore destStore) {
TemplateObject sourceTmpl = (TemplateObject) source;
Expand Down Expand Up @@ -681,7 +764,7 @@ protected Void copyTemplateToImageStoreCallback(AsyncCallbackDispatcher<Template
}

protected boolean isCopyFromOtherStoragesEnabled(Long zoneId) {
return StorageManager.COPY_PUBLIC_TEMPLATES_FROM_OTHER_STORAGES.valueIn(zoneId);
return StorageManager.COPY_TEMPLATES_FROM_OTHER_SECONDARY_STORAGES.valueIn(zoneId);
}

protected void publishTemplateCreation(TemplateInfo tmplt) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
*/
package org.apache.cloudstack.storage.image;

import com.cloud.dc.DataCenterVO;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.StorageUnavailableException;
import com.cloud.storage.template.TemplateProp;
import com.cloud.template.TemplateManager;
import org.apache.cloudstack.engine.orchestration.service.StorageOrchestrationService;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
Expand Down Expand Up @@ -82,6 +87,12 @@ public class TemplateServiceImplTest {
@Mock
StorageOrchestrationService storageOrchestrator;

@Mock
DataCenterDao _dcDao;

@Mock
TemplateManager _tmpltMgr;

Map<String, TemplateProp> templatesInSourceStore = new HashMap<>();

@Before
Expand Down Expand Up @@ -167,6 +178,11 @@ public void tryCopyingTemplateToImageStoreTestReturnsFalseWhenInstallPathIsNull(
templatesInSourceStore.put(tmpltMock.getUniqueName(), tmpltPropMock);
Mockito.doReturn(null).when(templateInfoMock).getInstallPath();

Scope scopeMock = Mockito.mock(Scope.class);
Mockito.doReturn(scopeMock).when(destStoreMock).getScope();
Mockito.doReturn(1L).when(scopeMock).getScopeId();
Mockito.doReturn(List.of(1L)).when(_dcDao).listAllIds();

boolean result = templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);

Assert.assertFalse(result);
Expand All @@ -183,4 +199,92 @@ public void tryCopyingTemplateToImageStoreTestReturnsTrueWhenTemplateExistsInAno
Assert.assertTrue(result);
Mockito.verify(storageOrchestrator).orchestrateTemplateCopyToImageStore(Mockito.any(), Mockito.any());
}

@Test
public void tryCopyingTemplateToImageStoreTestReturnsTrueWhenTemplateExistsInAnotherZone() throws StorageUnavailableException, ResourceAllocationException {
Scope scopeMock = Mockito.mock(Scope.class);
Mockito.doReturn(scopeMock).when(destStoreMock).getScope();
Mockito.doReturn(1L).when(scopeMock).getScopeId();
Mockito.doReturn(List.of(sourceStoreMock)).when(dataStoreManagerMock).getImageStoresByZoneIds(1L);
Mockito.doReturn(null).when(templateService).listTemplate(sourceStoreMock);
Mockito.doReturn(List.of(1L, 2L)).when(_dcDao).listAllIds();

DataStore otherZoneStoreMock = Mockito.mock(DataStore.class);
Mockito.doReturn(List.of(otherZoneStoreMock)).when(dataStoreManagerMock).getImageStoresByZoneIds(2L);

Map<String, TemplateProp> templatesInOtherZone = new HashMap<>();
templatesInOtherZone.put(tmpltMock.getUniqueName(), tmpltPropMock);
Mockito.doReturn(templatesInOtherZone).when(templateService).listTemplate(otherZoneStoreMock);

DataCenterVO dstZoneMock = Mockito.mock(DataCenterVO.class);
Mockito.doReturn(dstZoneMock).when(_dcDao).findById(1L);
Mockito.doReturn(true).when(_tmpltMgr).copy(Mockito.anyLong(), Mockito.eq(tmpltMock), Mockito.eq(otherZoneStoreMock), Mockito.eq(dstZoneMock));

boolean result = templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);

Assert.assertTrue(result);
}
Comment on lines +203 to +226
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test needs to mock CallContext to avoid a NullPointerException. The implementation code calls CallContext.current().getCallingUserId() in the copyTemplateAcrossZones method (line 711 in TemplateServiceImpl.java), but the test doesn't set up the CallContext. You should add mocking for CallContext.current() to return a mock CallContext that provides a valid user ID when getCallingUserId() is called.

Copilot uses AI. Check for mistakes.

@Test
public void tryCopyingTemplateToImageStoreTestReturnsFalseWhenDestinationZoneIsMissing() {
Scope scopeMock = Mockito.mock(Scope.class);
Mockito.doReturn(scopeMock).when(destStoreMock).getScope();
Mockito.doReturn(1L).when(scopeMock).getScopeId();
Mockito.doReturn(List.of(1L, 2L)).when(_dcDao).listAllIds();
Mockito.doReturn(List.of()).when(dataStoreManagerMock).getImageStoresByZoneIds(1L);

DataStore otherZoneStoreMock = Mockito.mock(DataStore.class);
Mockito.doReturn(List.of(otherZoneStoreMock)).when(dataStoreManagerMock).getImageStoresByZoneIds(2L);

Map<String, TemplateProp> templates = new HashMap<>();
templates.put(tmpltMock.getUniqueName(), tmpltPropMock);
Mockito.doReturn(templates).when(templateService).listTemplate(otherZoneStoreMock);
Mockito.doReturn(null).when(_dcDao).findById(1L);

boolean result = templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);

Assert.assertFalse(result);
}

@Test
public void tryCopyingTemplateToImageStoreTestReturnsFalseWhenCrossZoneCopyThrowsException() throws StorageUnavailableException, ResourceAllocationException {
Scope scopeMock = Mockito.mock(Scope.class);
Mockito.doReturn(scopeMock).when(destStoreMock).getScope();
Mockito.doReturn(1L).when(scopeMock).getScopeId();
Mockito.doReturn(List.of(1L, 2L)).when(_dcDao).listAllIds();
Mockito.doReturn(List.of()).when(dataStoreManagerMock).getImageStoresByZoneIds(1L);

DataStore otherZoneStoreMock = Mockito.mock(DataStore.class);
Mockito.doReturn(List.of(otherZoneStoreMock)).when(dataStoreManagerMock).getImageStoresByZoneIds(2L);

Map<String, TemplateProp> templates = new HashMap<>();
templates.put(tmpltMock.getUniqueName(), tmpltPropMock);
Mockito.doReturn(templates).when(templateService).listTemplate(otherZoneStoreMock);

DataCenterVO dstZoneMock = Mockito.mock(DataCenterVO.class);
Mockito.doReturn(dstZoneMock).when(_dcDao).findById(1L);
Mockito.doThrow(new RuntimeException("copy failed")).when(_tmpltMgr).copy(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any());

Scope sourceScopeMock = Mockito.mock(Scope.class);
Mockito.doReturn(sourceScopeMock).when(otherZoneStoreMock).getScope();
Mockito.doReturn(2L).when(sourceScopeMock).getScopeId();

boolean result = templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);

Assert.assertFalse(result);
}
Comment on lines +249 to +275
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test needs to mock CallContext to avoid a NullPointerException. The implementation code calls CallContext.current().getCallingUserId() in the copyTemplateAcrossZones method (line 711 in TemplateServiceImpl.java), but the test doesn't set up the CallContext. You should add mocking for CallContext.current() to return a mock CallContext that provides a valid user ID when getCallingUserId() is called.

Copilot uses AI. Check for mistakes.

@Test
public void tryCopyingTemplateToImageStoreTestReturnsFalseWhenTemplateNotFoundInAnyZone() {
Scope scopeMock = Mockito.mock(Scope.class);
Mockito.doReturn(scopeMock).when(destStoreMock).getScope();
Mockito.doReturn(1L).when(scopeMock).getScopeId();
Mockito.doReturn(List.of(1L, 2L)).when(_dcDao).listAllIds();
Mockito.doReturn(List.of(sourceStoreMock)).when(dataStoreManagerMock).getImageStoresByZoneIds(Mockito.anyLong());
Mockito.doReturn(null).when(templateService).listTemplate(Mockito.any());

boolean result = templateService.tryCopyingTemplateToImageStore(tmpltMock, destStoreMock);

Assert.assertFalse(result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4195,7 +4195,7 @@ public ConfigKey<?>[] getConfigKeys() {
DataStoreDownloadFollowRedirects,
AllowVolumeReSizeBeyondAllocation,
StoragePoolHostConnectWorkers,
COPY_PUBLIC_TEMPLATES_FROM_OTHER_STORAGES
COPY_TEMPLATES_FROM_OTHER_SECONDARY_STORAGES
};
}

Expand Down
Loading