From 387f958df1381f99e79ddddd65c26f3069d406ec Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Wed, 14 Jan 2026 14:12:00 -0500 Subject: [PATCH 1/4] Allow endpoint selection logic for system templates in region and zone scopes --- .../cloudstack/storage/endpoint/DefaultEndPointSelector.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java index 7e9f65f43b34..1d3b781afb12 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java @@ -400,7 +400,9 @@ public EndPoint select(DataObject object) { } if (object instanceof TemplateInfo) { TemplateInfo tmplInfo = (TemplateInfo)object; - if (store.getScope().getScopeType() == ScopeType.ZONE && store.getScope().getScopeId() == null && tmplInfo.getTemplateType() == TemplateType.SYSTEM) { + if (tmplInfo.getTemplateType() == TemplateType.SYSTEM && + (store.getScope().getScopeType() == ScopeType.REGION || + (store.getScope().getScopeType() == ScopeType.ZONE && store.getScope().getScopeId() == null))) { return LocalHostEndpoint.getEndpoint(); // for bootstrap system vm template downloading to region image store } } From dd39393400e6b58968ab5a0fe74618df608f01ca Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Wed, 14 Jan 2026 14:12:31 -0500 Subject: [PATCH 2/4] Skip null or missing URLs for S3 --- .../secondarystorage/SecondaryStorageManagerImpl.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java index 5698632249d3..66aab2a39c8f 100644 --- a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java +++ b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java @@ -1247,6 +1247,10 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl protected void addSecondaryStorageServerAddressToBuffer(StringBuilder buffer, List dataStores, String vmName) { List addresses = new ArrayList<>(); for (DataStore dataStore: dataStores) { + // S3 and other object stores may not have a URL, so better to skip them + if (dataStore == null || dataStore.getTO() == null || dataStore.getTO().getUrl() == null) { + continue; + } String url = dataStore.getTO().getUrl(); String[] urlArray = url.split("/"); From d6bf807d6e97825360b33dac572a6c739b8352fe Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Wed, 14 Jan 2026 14:12:47 -0500 Subject: [PATCH 3/4] Enable path-style access for S3-compatible storage --- utils/src/main/java/com/cloud/utils/storage/S3/S3Utils.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/utils/src/main/java/com/cloud/utils/storage/S3/S3Utils.java b/utils/src/main/java/com/cloud/utils/storage/S3/S3Utils.java index 6d85d2d1dadb..c04e14a9a0ba 100644 --- a/utils/src/main/java/com/cloud/utils/storage/S3/S3Utils.java +++ b/utils/src/main/java/com/cloud/utils/storage/S3/S3Utils.java @@ -114,6 +114,8 @@ public static TransferManager getTransferManager(final ClientOptions clientOptio LOGGER.debug(format("Setting the end point for S3 client with access key %1$s to %2$s.", clientOptions.getAccessKey(), clientOptions.getEndPoint())); client.setEndpoint(clientOptions.getEndPoint()); + // Enable path-style access for S3-compatible storage + client.setS3ClientOptions(com.amazonaws.services.s3.S3ClientOptions.builder().setPathStyleAccess(true).build()); } TRANSFERMANAGER_ACCESSKEY_MAP.put(clientOptions.getAccessKey(), new TransferManager(client)); From 9a2ba84d427618dcf8e7c1072ce9da25f263ec30 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Wed, 14 Jan 2026 14:13:01 -0500 Subject: [PATCH 4/4] Add test for handling null entries in secondary storage --- .../SecondaryStorageManagerImplTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/services/secondary-storage/controller/src/test/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImplTest.java b/services/secondary-storage/controller/src/test/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImplTest.java index 83596b64ec0f..d5719aee398b 100644 --- a/services/secondary-storage/controller/src/test/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImplTest.java +++ b/services/secondary-storage/controller/src/test/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImplTest.java @@ -117,6 +117,45 @@ public void testAddSecondaryStorageServerAddressToBufferInvalidAddress() { runAddSecondaryStorageServerAddressToBufferTest(addresses, StringUtils.join(List.of(randomIp1, randomIp2), ",")); } + @Test + public void testAddSecondaryStorageServerAddressToBufferWithNullEntries() { + String randomIp1 = InetAddresses.fromInteger(secureRandom.nextInt()).getHostAddress(); + String randomIp2 = InetAddresses.fromInteger(secureRandom.nextInt()).getHostAddress(); + + List dataStores = new ArrayList<>(); + + DataStore validStore1 = Mockito.mock(DataStore.class); + DataStoreTO validStoreTO1 = Mockito.mock(DataStoreTO.class); + when(validStoreTO1.getUrl()).thenReturn(String.format("http://%s", randomIp1)); + when(validStore1.getTO()).thenReturn(validStoreTO1); + dataStores.add(validStore1); + + dataStores.add(null); + + DataStore nullToStore = Mockito.mock(DataStore.class); + when(nullToStore.getTO()).thenReturn(null); + dataStores.add(nullToStore); + + DataStore nullUrlStore = Mockito.mock(DataStore.class); + DataStoreTO nullUrlStoreTO = Mockito.mock(DataStoreTO.class); + when(nullUrlStoreTO.getUrl()).thenReturn(null); + when(nullUrlStore.getTO()).thenReturn(nullUrlStoreTO); + dataStores.add(nullUrlStore); + + DataStore validStore2 = Mockito.mock(DataStore.class); + DataStoreTO validStoreTO2 = Mockito.mock(DataStoreTO.class); + when(validStoreTO2.getUrl()).thenReturn(String.format("http://%s", randomIp2)); + when(validStore2.getTO()).thenReturn(validStoreTO2); + dataStores.add(validStore2); + + StringBuilder builder = new StringBuilder(); + secondaryStorageManager.addSecondaryStorageServerAddressToBuffer(builder, dataStores, "VM"); + String result = builder.toString(); + result = result.contains("=") ? result.split("=")[1] : null; + + assertEquals(StringUtils.join(List.of(randomIp1, randomIp2), ","), result); + } + @Test public void testCreateSecondaryStorageVm_New() { long dataCenterId = 1L;