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..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 @@ -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; @@ -127,8 +131,9 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE dm.put(ApiConstants.S3_USE_TCP_KEEPALIVE, getUseTCPKeepAlive().toString()); } + String url = "s3://" + getEndPoint().replaceFirst("^https?://", "") + "/" + getBucketName(); try{ - ImageStore result = _storageService.discoverImageStore(null, null, "S3", null, dm); + ImageStore result = _storageService.discoverImageStore(null, url, "S3", zoneId, dm); ImageStoreResponse storeResponse; if (result != null) { storeResponse = _responseGenerator.createImageStoreResponse(result); 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/core/src/main/java/com/cloud/storage/template/S3TemplateDownloader.java b/core/src/main/java/com/cloud/storage/template/S3TemplateDownloader.java index 70df906d1ce3..9dd73c2a6562 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,13 @@ 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(); 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..9458aefdc943 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()) { + updateSystemVmTemplateUrlsForNonNfsStores(); + return; + } for (Long zoneId : zoneIds) { String filePath = null; try { @@ -984,6 +988,21 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { } } + public void updateSystemVmTemplateUrlsForNonNfsStores() { + for (Pair hypervisorArch : + clusterDao.listDistinctHypervisorsArchAcrossClusters(null)) { + 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); 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 } } 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..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 @@ -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,17 +84,13 @@ 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); - if (useHttps != null && Boolean.parseBoolean(useHttps)) { - protocol = "https"; - } - imageStoreParameters.put("protocol", protocol); + imageStoreParameters.put("protocol", "s3"); 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/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/server/src/main/java/com/cloud/storage/StorageManagerImpl.java b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java index d23856552ee3..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); @@ -3585,6 +3575,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 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("/"); 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; 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') } 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' 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));