-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Allow copy of templates from secondary storages of other zone when adding a new secondary storage #12296
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 4.20
Are you sure you want to change the base?
Allow copy of templates from secondary storages of other zone when adding a new secondary storage #12296
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||
|
|
@@ -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", | ||||||
|
||||||
| 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
AI
Dec 19, 2025
There was a problem hiding this comment.
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
AI
Dec 19, 2025
There was a problem hiding this comment.
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.
| logger.warn("Destination zone [{}] not found for template [{}]", | |
| logger.warn("Destination zone [{}] not found for template [{}].", |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
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'.
| Long userId = CallContext.current().getCallingUserId(); | |
| long userId = CallContext.current().getCallingUserId(); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
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.
| logger.error("Failed to copy template [{}] from zone [{}] to zone [{}]", | |
| logger.error("Failed to copy template [{}] from zone [{}] to zone [{}].", |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -82,6 +87,12 @@ public class TemplateServiceImplTest { | |
| @Mock | ||
| StorageOrchestrationService storageOrchestrator; | ||
|
|
||
| @Mock | ||
| DataCenterDao _dcDao; | ||
|
|
||
| @Mock | ||
| TemplateManager _tmpltMgr; | ||
|
|
||
| Map<String, TemplateProp> templatesInSourceStore = new HashMap<>(); | ||
|
|
||
| @Before | ||
|
|
@@ -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); | ||
|
|
@@ -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
|
||
|
|
||
| @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
|
||
|
|
||
| @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); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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