From 387f958df1381f99e79ddddd65c26f3069d406ec Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Wed, 14 Jan 2026 14:12:00 -0500 Subject: [PATCH 01/14] 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 02/14] 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 03/14] 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 04/14] 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; From 4a6482800da5d4ab1a10ca92c5afcbe832f15d66 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Tue, 3 Feb 2026 11:19:55 -0500 Subject: [PATCH 05/14] capture S3 execptions to show actual error messages --- .../cloud/storage/template/S3TemplateDownloader.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/cloud/storage/template/S3TemplateDownloader.java b/core/src/main/java/com/cloud/storage/template/S3TemplateDownloader.java index 70df906d1ce3..ccb5b80f99c4 100644 --- a/core/src/main/java/com/cloud/storage/template/S3TemplateDownloader.java +++ b/core/src/main/java/com/cloud/storage/template/S3TemplateDownloader.java @@ -216,8 +216,15 @@ public void progressChanged(ProgressEvent progressEvent) { // Wait for the upload to complete. upload.waitForCompletion(); } catch (InterruptedException e) { - // Interruption while waiting for the upload to complete. - logger.warn("Interruption occurred while waiting for upload of " + downloadUrl + " to complete"); + errorString = "Interruption occurred while waiting for upload of " + downloadUrl + " to complete"; + logger.warn(errorString); + + status = Status.UNRECOVERABLE_ERROR; + } catch (Exception e) { + errorString = "S3 upload failed for " + downloadUrl + ": " + e.getMessage(); + logger.warn(errorString, e); + + status = Status.UNRECOVERABLE_ERROR; } downloadTime = new Date().getTime() - start.getTime(); From 26dc485e13c61b03d4c1757c0cf7ab988352a1a6 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Tue, 3 Feb 2026 11:59:05 -0500 Subject: [PATCH 06/14] removed trailing whitespace --- .../java/com/cloud/storage/template/S3TemplateDownloader.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/main/java/com/cloud/storage/template/S3TemplateDownloader.java b/core/src/main/java/com/cloud/storage/template/S3TemplateDownloader.java index ccb5b80f99c4..9dd73c2a6562 100644 --- a/core/src/main/java/com/cloud/storage/template/S3TemplateDownloader.java +++ b/core/src/main/java/com/cloud/storage/template/S3TemplateDownloader.java @@ -218,12 +218,10 @@ public void progressChanged(ProgressEvent progressEvent) { } catch (InterruptedException e) { errorString = "Interruption occurred while waiting for upload of " + downloadUrl + " to complete"; logger.warn(errorString); - status = Status.UNRECOVERABLE_ERROR; } catch (Exception e) { errorString = "S3 upload failed for " + downloadUrl + ": " + e.getMessage(); logger.warn(errorString, e); - status = Status.UNRECOVERABLE_ERROR; } From 1a5625a79967c584d3e6f08815b21ece0af412f6 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Tue, 3 Feb 2026 15:10:47 -0500 Subject: [PATCH 07/14] display S3 endpoint and bucket in secondary storage details --- .../api/response/ImageStoreResponse.java | 24 +++++++++++++++++++ .../api/query/dao/ImageStoreJoinDaoImpl.java | 14 +++++++++++ .../config/section/infra/secondaryStorages.js | 2 +- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ImageStoreResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ImageStoreResponse.java index 79f7eb295ea2..a617906eee1c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ImageStoreResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ImageStoreResponse.java @@ -71,6 +71,14 @@ public class ImageStoreResponse extends BaseResponseWithAnnotations { @Param(description = "The host's currently used disk size") private Long diskSizeUsed; + @SerializedName(ApiConstants.S3_END_POINT) + @Param(description = "The S3 endpoint URL") + private String s3Endpoint; + + @SerializedName(ApiConstants.S3_BUCKET_NAME) + @Param(description = "The S3 bucket name") + private String s3BucketName; + public ImageStoreResponse() { } @@ -156,4 +164,20 @@ public void setDiskSizeTotal(Long diskSizeTotal) { public void setDiskSizeUsed(Long diskSizeUsed) { this.diskSizeUsed = diskSizeUsed; } + + public String getS3Endpoint() { + return s3Endpoint; + } + + public void setS3Endpoint(String s3Endpoint) { + this.s3Endpoint = s3Endpoint; + } + + public String getS3BucketName() { + return s3BucketName; + } + + public void setS3BucketName(String s3BucketName) { + this.s3BucketName = s3BucketName; + } } diff --git a/server/src/main/java/com/cloud/api/query/dao/ImageStoreJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/ImageStoreJoinDaoImpl.java index 9a0c271fdb48..4af590ab6265 100644 --- a/server/src/main/java/com/cloud/api/query/dao/ImageStoreJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/ImageStoreJoinDaoImpl.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import javax.inject.Inject; @@ -26,7 +27,10 @@ import com.cloud.user.AccountManager; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider; +import org.apache.cloudstack.storage.datastore.db.ImageStoreDetailsDao; import org.springframework.stereotype.Component; import org.apache.cloudstack.api.response.ImageStoreResponse; @@ -48,6 +52,8 @@ public class ImageStoreJoinDaoImpl extends GenericDaoBase dsSearch; @@ -92,6 +98,14 @@ public ImageStoreResponse newImageStoreResponse(ImageStoreJoinVO ids) { osResponse.setHasAnnotation(annotationDao.hasAnnotations(ids.getUuid(), AnnotationService.EntityType.SECONDARY_STORAGE.name(), accountManager.isRootAdmin(CallContext.current().getCallingAccount().getId()))); + if (DataStoreProvider.S3_IMAGE.equalsIgnoreCase(ids.getProviderName())) { + Map s3Details = imageStoreDetailsDao.getDetails(ids.getId()); + if (s3Details != null) { + osResponse.setS3Endpoint(s3Details.get(ApiConstants.S3_END_POINT)); + osResponse.setS3BucketName(s3Details.get(ApiConstants.S3_BUCKET_NAME)); + } + } + osResponse.setObjectName("imagestore"); return osResponse; } diff --git a/ui/src/config/section/infra/secondaryStorages.js b/ui/src/config/section/infra/secondaryStorages.js index 3fc64c5c9575..cedc893878a9 100644 --- a/ui/src/config/section/infra/secondaryStorages.js +++ b/ui/src/config/section/infra/secondaryStorages.js @@ -36,7 +36,7 @@ export default { return fields }, details: () => { - var fields = ['name', 'id', 'url', 'protocol', 'provider', 'scope', 'zonename'] + var fields = ['name', 'id', 'url', 'protocol', 'provider', 'scope', 'zonename', 'endpoint', 'bucket'] if (store.getters.apis.listImageStores.params.filter(x => x.name === 'readonly').length > 0) { fields.push('readonly') } From 28a65643b807e3a03519836a842da4b552793ac3 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Thu, 5 Feb 2026 13:17:40 -0500 Subject: [PATCH 08/14] Fix SystemVM template URL update for S3 --- .../upgrade/SystemVmTemplateRegistration.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java b/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java index a6aecf477f78..4a0a759cc89a 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java @@ -959,6 +959,10 @@ public void registerTemplates(List> @Override public void doInTransactionWithoutResult(final TransactionStatus status) { List zoneIds = getEligibleZoneIds(); + if (zoneIds.isEmpty()) { + updateTemplateUrlsForNonNfsStores(hypervisorsArchInUse); + return; + } for (Long zoneId : zoneIds) { String filePath = null; try { @@ -984,6 +988,20 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { } } + private void updateTemplateUrlsForNonNfsStores(List> hypervisorsInUse) { + for (Pair hypervisorArch : hypervisorsInUse) { + MetadataTemplateDetails templateDetails = getMetadataTemplateDetails(hypervisorArch.first(), hypervisorArch.second()); + if (templateDetails == null) { + continue; + } + VMTemplateVO templateVO = vmTemplateDao.findLatestTemplateByTypeAndHypervisorAndArch( + templateDetails.getHypervisorType(), templateDetails.getArch(), Storage.TemplateType.SYSTEM); + if (templateVO != null) { + updateTemplateUrlChecksumAndGuestOsId(templateVO, templateDetails); + } + } + } + private void updateRegisteredTemplateDetails(Long templateId, MetadataTemplateDetails templateDetails) { VMTemplateVO templateVO = vmTemplateDao.findById(templateId); templateVO.setTemplateType(Storage.TemplateType.SYSTEM); From 71013b6a94a3f7ec306348878d52692dcbc8e4eb Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Fri, 6 Feb 2026 13:14:55 -0500 Subject: [PATCH 09/14] fix SystemVM template URL update for S3 secondary storage --- .../com/cloud/upgrade/SystemVmTemplateRegistration.java | 7 ++++--- .../main/java/com/cloud/storage/StorageManagerImpl.java | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java b/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java index 4a0a759cc89a..9458aefdc943 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java @@ -960,7 +960,7 @@ public void registerTemplates(List> public void doInTransactionWithoutResult(final TransactionStatus status) { List zoneIds = getEligibleZoneIds(); if (zoneIds.isEmpty()) { - updateTemplateUrlsForNonNfsStores(hypervisorsArchInUse); + updateSystemVmTemplateUrlsForNonNfsStores(); return; } for (Long zoneId : zoneIds) { @@ -988,8 +988,9 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { } } - private void updateTemplateUrlsForNonNfsStores(List> hypervisorsInUse) { - for (Pair hypervisorArch : hypervisorsInUse) { + public void updateSystemVmTemplateUrlsForNonNfsStores() { + for (Pair hypervisorArch : + clusterDao.listDistinctHypervisorsArchAcrossClusters(null)) { MetadataTemplateDetails templateDetails = getMetadataTemplateDetails(hypervisorArch.first(), hypervisorArch.second()); if (templateDetails == null) { continue; diff --git a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java index d23856552ee3..a632391f89e6 100644 --- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java @@ -3585,6 +3585,7 @@ public ImageStore discoverImageStore(String name, String url, String providerNam if (((ImageStoreProvider)storeProvider).needDownloadSysTemplate()) { // trigger system vm template download + new SystemVmTemplateRegistration().updateSystemVmTemplateUrlsForNonNfsStores(); _imageSrv.downloadBootstrapSysTemplate(store); } else { // populate template_store_ref table From 6becb331996971599388181cc6bba87d48a21ca8 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Tue, 17 Feb 2026 06:45:05 -0500 Subject: [PATCH 10/14] Add zoneId parameter to S3 image store commands --- .../api/command/admin/storage/AddImageStoreS3CMD.java | 6 +++++- .../datastore/lifecycle/S3ImageStoreLifeCycleImpl.java | 4 +++- .../datastore/provider/S3ImageStoreProviderImpl.java | 4 +--- .../java/com/cloud/storage/StorageManagerImpl.java | 10 ---------- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java index 2fe3c7cd106a..c37ac17376cf 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java @@ -46,6 +46,7 @@ import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.ImageStoreResponse; +import org.apache.cloudstack.api.response.ZoneResponse; import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.DiscoveryException; @@ -61,6 +62,9 @@ public final class AddImageStoreS3CMD extends BaseCmd implements ClientOptions { private static final String s_name = "addImageStoreS3Response"; + @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class, description = "The Zone ID for the S3 image store") + private Long zoneId; + @Parameter(name = S3_ACCESS_KEY, type = STRING, required = true, description = "S3 access key") private String accessKey; @@ -128,7 +132,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE } try{ - ImageStore result = _storageService.discoverImageStore(null, null, "S3", null, dm); + ImageStore result = _storageService.discoverImageStore(null, null, "S3", zoneId, dm); ImageStoreResponse storeResponse; if (result != null) { storeResponse = _responseGenerator.createImageStoreResponse(result); diff --git a/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/S3ImageStoreLifeCycleImpl.java b/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/S3ImageStoreLifeCycleImpl.java index 5e5069af3fc2..0ef3df67bf25 100644 --- a/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/S3ImageStoreLifeCycleImpl.java +++ b/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/S3ImageStoreLifeCycleImpl.java @@ -72,6 +72,7 @@ public S3ImageStoreLifeCycleImpl() { @Override public DataStore initialize(Map dsInfos) { + Long dcId = (Long)dsInfos.get("zoneId"); String url = (String)dsInfos.get("url"); String name = (String)dsInfos.get("name"); String providerName = (String)dsInfos.get("providerName"); @@ -83,6 +84,7 @@ public DataStore initialize(Map dsInfos) { Map imageStoreParameters = new HashMap(); imageStoreParameters.put("name", name); + imageStoreParameters.put("zoneId", dcId); imageStoreParameters.put("url", url); String protocol = "http"; String useHttps = details.get(ApiConstants.S3_HTTPS_FLAG); @@ -93,7 +95,7 @@ public DataStore initialize(Map dsInfos) { if (scope != null) { imageStoreParameters.put("scope", scope); } else { - imageStoreParameters.put("scope", ScopeType.REGION); + imageStoreParameters.put("scope", ScopeType.ZONE); } imageStoreParameters.put("providerName", providerName); imageStoreParameters.put("role", role); diff --git a/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/provider/S3ImageStoreProviderImpl.java b/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/provider/S3ImageStoreProviderImpl.java index 18947949831b..000ccb7bf284 100644 --- a/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/provider/S3ImageStoreProviderImpl.java +++ b/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/provider/S3ImageStoreProviderImpl.java @@ -90,9 +90,7 @@ public Set getTypes() { @Override public boolean isScopeSupported(ScopeType scope) { - if (scope == ScopeType.REGION) - return true; - return false; + return scope == ScopeType.ZONE || scope == ScopeType.REGION; } @Override diff --git a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java index a632391f89e6..91c2d24b4d7e 100644 --- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java @@ -3537,16 +3537,6 @@ public ImageStore discoverImageStore(String name, String url, String providerNam throw new InvalidParameterValueException("Image store provider " + providerName + " does not support scope " + scopeType); } - // check if we have already image stores from other different providers, - // we currently are not supporting image stores from different - // providers co-existing - List imageStores = _imageStoreDao.listImageStores(); - for (ImageStoreVO store : imageStores) { - if (!store.getProviderName().equalsIgnoreCase(providerName)) { - throw new InvalidParameterValueException("You can only add new image stores from the same provider " + store.getProviderName() + " already added"); - } - } - if (zoneId != null) { // Check if the zone exists in the system DataCenterVO zone = _dcDao.findById(zoneId); From f47711402c664c0940d8411b646a8627ab33c1d8 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Tue, 17 Feb 2026 06:49:11 -0500 Subject: [PATCH 11/14] Refactor S3 provider handling and add zone ID to parameters in zone launch --- ui/src/views/infra/AddSecondaryStorage.vue | 2 +- .../infra/zone/ZoneWizardAddResources.vue | 19 ++++++------------- .../views/infra/zone/ZoneWizardLaunchZone.vue | 1 + 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/ui/src/views/infra/AddSecondaryStorage.vue b/ui/src/views/infra/AddSecondaryStorage.vue index 746af5b959d0..53a720489cd2 100644 --- a/ui/src/views/infra/AddSecondaryStorage.vue +++ b/ui/src/views/infra/AddSecondaryStorage.vue @@ -347,7 +347,7 @@ export default { data.url = url } data.provider = provider - if (values.zone && !['Swift', 'S3'].includes(provider)) { + if (values.zone && !['Swift'].includes(provider)) { data.zoneid = values.zone } diff --git a/ui/src/views/infra/zone/ZoneWizardAddResources.vue b/ui/src/views/infra/zone/ZoneWizardAddResources.vue index 4bd602f0acaa..c2e635945134 100644 --- a/ui/src/views/infra/zone/ZoneWizardAddResources.vue +++ b/ui/src/views/infra/zone/ZoneWizardAddResources.vue @@ -1094,19 +1094,12 @@ export default { }) }, fetchProvider () { - const storageProviders = [] - api('listImageStores', { provider: 'S3' }).then(json => { - const s3stores = json.listimagestoresresponse.imagestore - if (s3stores != null && s3stores.length > 0) { - storageProviders.push({ id: 'S3', description: 'S3' }) - } else { - storageProviders.push({ id: 'NFS', description: 'NFS' }) - storageProviders.push({ id: 'SMB', description: 'SMB/CIFS' }) - storageProviders.push({ id: 'S3', description: 'S3' }) - storageProviders.push({ id: 'Swift', description: 'Swift' }) - } - this.storageProviders = storageProviders - }) + this.storageProviders = [ + { id: 'NFS', description: 'NFS' }, + { id: 'SMB', description: 'SMB/CIFS' }, + { id: 'S3', description: 'S3' }, + { id: 'Swift', description: 'Swift' } + ] }, fetchPrimaryStorageProvider () { this.primaryStorageProviders = [] diff --git a/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue b/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue index a787ad839cdb..fa8dd2afff22 100644 --- a/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue +++ b/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue @@ -1596,6 +1596,7 @@ export default { params['details[2].value'] = this.prefillContent.secondaryStorageSMBDomain } else if (this.prefillContent.secondaryStorageProvider === 'S3') { params.provider = this.prefillContent.secondaryStorageProvider + params.zoneid = this.stepData.zoneReturned.id params['details[0].key'] = 'accesskey' params['details[0].value'] = this.prefillContent.secondaryStorageAccessKey params['details[1].key'] = 'secretkey' From e971019caf0aa11cd0b86f4a0c39a17a217aa1d9 Mon Sep 17 00:00:00 2001 From: Damans227 <61474540+Damans227@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:40:16 -0500 Subject: [PATCH 12/14] Update S3 image store discovery to include endpoint and bucket name in URL --- .../api/command/admin/storage/AddImageStoreS3CMD.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java index c37ac17376cf..d9e9f4e383c6 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java @@ -131,8 +131,9 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE dm.put(ApiConstants.S3_USE_TCP_KEEPALIVE, getUseTCPKeepAlive().toString()); } + String url = getEndPoint() + "/" + getBucketName(); try{ - ImageStore result = _storageService.discoverImageStore(null, null, "S3", zoneId, dm); + ImageStore result = _storageService.discoverImageStore(null, url, "S3", zoneId, dm); ImageStoreResponse storeResponse; if (result != null) { storeResponse = _responseGenerator.createImageStoreResponse(result); From f2dfd10178abdfd341b145cb6d38c0fdb53368e6 Mon Sep 17 00:00:00 2001 From: Damans227 <61474540+Damans227@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:52:34 -0500 Subject: [PATCH 13/14] Update S3 image store URL format to use 's3://' scheme --- .../api/command/admin/storage/AddImageStoreS3CMD.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java index d9e9f4e383c6..c322e3ae44b9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java @@ -131,7 +131,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE dm.put(ApiConstants.S3_USE_TCP_KEEPALIVE, getUseTCPKeepAlive().toString()); } - String url = getEndPoint() + "/" + getBucketName(); + String url = "s3://" + getEndPoint().replaceFirst("^https?://", "") + "/" + getBucketName(); try{ ImageStore result = _storageService.discoverImageStore(null, url, "S3", zoneId, dm); ImageStoreResponse storeResponse; From 2f629470aeec9a85744887c70d495d04fb33cf02 Mon Sep 17 00:00:00 2001 From: Damans227 <61474540+Damans227@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:52:41 -0500 Subject: [PATCH 14/14] Update S3 image store protocol to use 's3' instead of dynamic HTTP/HTTPS --- .../datastore/lifecycle/S3ImageStoreLifeCycleImpl.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/S3ImageStoreLifeCycleImpl.java b/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/S3ImageStoreLifeCycleImpl.java index 0ef3df67bf25..41cb04baa574 100644 --- a/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/S3ImageStoreLifeCycleImpl.java +++ b/plugins/storage/image/s3/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/S3ImageStoreLifeCycleImpl.java @@ -86,12 +86,7 @@ public DataStore initialize(Map dsInfos) { imageStoreParameters.put("name", name); imageStoreParameters.put("zoneId", dcId); imageStoreParameters.put("url", url); - String protocol = "http"; - String useHttps = details.get(ApiConstants.S3_HTTPS_FLAG); - if (useHttps != null && Boolean.parseBoolean(useHttps)) { - protocol = "https"; - } - imageStoreParameters.put("protocol", protocol); + imageStoreParameters.put("protocol", "s3"); if (scope != null) { imageStoreParameters.put("scope", scope); } else {