Skip to content

Conversation

@harikrishna-patnala
Copy link
Contributor

@harikrishna-patnala harikrishna-patnala commented Dec 18, 2025

Description

When a new Secondary Storage is added, the Management Server attempts to make existing templates available on it. Templates are first copied from other Secondary Storages within the same zone (see the related PR #10363 for same zone improvements).

This PR extends the behavior to also allow templates to be copied from Secondary Storage servers in other zones. If the copying of template fails or not possible then management server falls back to the old behavior of downloading the templates from their original source URLs.

I've added a doc PR for this apache/cloudstack-documentation#611

Types of changes

  • Breaking change (fix or feature that would cause existing functionality to change)
  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Enhancement (improves an existing feature and functionality)
  • Cleanup (Code refactoring and cleanup, that may add test cases)
  • Build/CI
  • Test (unit or integration test code)

Feature/Enhancement Scale or Bug Severity

Feature/Enhancement Scale

  • Major
  • Minor

Bug Severity

  • BLOCKER
  • Critical
  • Major
  • Minor
  • Trivial

Screenshots (if appropriate):

How Has This Been Tested?

How did you try to break this feature and the system with this change?

@codecov
Copy link

codecov bot commented Dec 18, 2025

Codecov Report

❌ Patch coverage is 92.06349% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 16.24%. Comparing base (e8200a0) to head (1c448d3).

Files with missing lines Patch % Lines
.../cloudstack/storage/image/TemplateServiceImpl.java 91.93% 3 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##               4.20   #12296   +/-   ##
=========================================
  Coverage     16.23%   16.24%           
- Complexity    13371    13385   +14     
=========================================
  Files          5657     5657           
  Lines        498860   498913   +53     
  Branches      60543    60550    +7     
=========================================
+ Hits          81003    81052   +49     
- Misses       408824   408825    +1     
- Partials       9033     9036    +3     
Flag Coverage Δ
uitests 4.00% <ø> (ø)
unittests 17.10% <92.06%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@harikrishna-patnala
Copy link
Contributor Author

@blueorangutan package

@blueorangutan
Copy link

@harikrishna-patnala a [SL] Jenkins job has been kicked to build packages. It will be bundled with KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress.

@blueorangutan
Copy link

Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 16099

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR enhances the template copying functionality when adding a new Secondary Storage server by allowing templates to be copied from Secondary Storages in other zones, in addition to the existing same-zone copying. If copying fails or is not possible, the system falls back to downloading templates from their original source URLs.

Key changes:

  • Renamed configuration key from COPY_PUBLIC_TEMPLATES_FROM_OTHER_STORAGES to COPY_TEMPLATES_FROM_OTHER_SECONDARY_STORAGES with updated description
  • Refactored template copying logic into separate methods for same-zone and cross-zone operations
  • Added comprehensive test coverage for the new cross-zone copying functionality

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
engine/components-api/src/main/java/com/cloud/storage/StorageManager.java Renamed and updated the configuration key to reflect the expanded functionality that now includes cross-zone template copying
server/src/main/java/com/cloud/storage/StorageManagerImpl.java Updated reference to the renamed configuration key
engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java Refactored tryCopyingTemplateToImageStore into separate methods for same-zone and cross-zone copy operations; added new helper methods searchAndCopyAcrossZones, searchAndCopyWithinZone, findTemplateInStores, and copyTemplateAcrossZones
engine/storage/image/src/test/java/org/apache/cloudstack/storage/image/TemplateServiceImplTest.java Added test cases for cross-zone template copying scenarios including successful copy, missing destination zone, copy exception handling, and template not found cases

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +203 to +226
@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);
}
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.
Comment on lines +249 to +275
@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);
}
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.
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.
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.
Long userId = CallContext.current().getCallingUserId();
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.
Comment on lines +688 to +696
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;
}
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.
}

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.
Copy link
Contributor

@shwstppr shwstppr left a comment

Choose a reason for hiding this comment

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

Will be a good addition. One comment around config change
Only wish there could be tighter control on the behaviour while adding the secondary storage, like passing a flag at the time of adding. But it can be covered in documentation that the operator must toggle the behaviour before adding the secondary storage.


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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants