diff --git a/api/src/main/java/com/cloud/server/ResourceTag.java b/api/src/main/java/com/cloud/server/ResourceTag.java index b3026deceff8..7b49a19d041f 100644 --- a/api/src/main/java/com/cloud/server/ResourceTag.java +++ b/api/src/main/java/com/cloud/server/ResourceTag.java @@ -71,7 +71,7 @@ public enum ResourceObjectType { NetworkOffering(false, true), VpcOffering(true, false), Domain(false, false, true), - ObjectStore(false, false, true); + ObjectStore(false, true, true); ResourceObjectType(boolean resourceTagsSupport, boolean resourceMetadataSupport) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/UpdateObjectStoragePoolCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/UpdateObjectStoragePoolCmd.java index 497179d25ef1..2f0d1cc0b57e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/UpdateObjectStoragePoolCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/UpdateObjectStoragePoolCmd.java @@ -18,6 +18,10 @@ package org.apache.cloudstack.api.command.admin.storage; import org.apache.cloudstack.storage.object.ObjectStore; + +import java.util.HashMap; +import java.util.Map; + import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -41,9 +45,17 @@ public class UpdateObjectStoragePoolCmd extends BaseCmd { @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "the name for the object store") private String name; + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, description = "the object store provider name") + private String providerName; + @Parameter(name = ApiConstants.URL, type = CommandType.STRING, description = "the url for the object store") private String url; + @Parameter(name = ApiConstants.DETAILS, + type = CommandType.MAP, + description = "the details for the object store. Example: details[0].key=accesskey&details[0].value=s389ddssaa&details[1].key=secretkey&details[1].value=8dshfsss") + private Map details; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// @@ -61,6 +73,24 @@ public String getUrl() { return url; } + public String getProviderName() { + return providerName; + } + + public Map getDetails() { + Map detailsMap = null; + if (details != null && !details.isEmpty()) { + detailsMap = new HashMap<>(); + for (Object prop : details.values()) { + HashMap detail = (HashMap) prop; + String key = detail.get(ApiConstants.KEY); + String value = detail.get(ApiConstants.VALUE); + detailsMap.put(key, value); + } + } + return detailsMap; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -69,7 +99,7 @@ public String getUrl() { public void execute() { ObjectStore result = _storageService.updateObjectStore(getId(), this); - ObjectStoreResponse storeResponse = null; + ObjectStoreResponse storeResponse; if (result != null) { storeResponse = _responseGenerator.createObjectStoreResponse(result); storeResponse.setResponseName(getCommandName()); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/storage/UpdateObjectStoragePoolCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/storage/UpdateObjectStoragePoolCmdTest.java index 307d80aa3f83..1eeb5e1e02cf 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/storage/UpdateObjectStoragePoolCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/storage/UpdateObjectStoragePoolCmdTest.java @@ -19,11 +19,13 @@ package org.apache.cloudstack.api.command.admin.storage; import com.cloud.storage.StorageService; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ResponseGenerator; import org.apache.cloudstack.api.response.ObjectStoreResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.storage.object.ObjectStore; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; @@ -32,6 +34,9 @@ import org.mockito.Spy; import org.springframework.test.util.ReflectionTestUtils; +import java.util.HashMap; +import java.util.Map; + import static org.mockito.ArgumentMatchers.any; public class UpdateObjectStoragePoolCmdTest { @@ -83,4 +88,32 @@ public void testUpdateObjectStore() { .updateObjectStore(1L, updateObjectStoragePoolCmd); } + @Test + public void testGetDetailsNone() { + Assert.assertNull(updateObjectStoragePoolCmd.getDetails()); + } + + @Test + public void testGetDetails() { + // test setup + // Build the "details" Map which has the following format: + // {0={value=value0, key=key0}, 1={value=value1, key=key1}, ...} + Map details = new HashMap<>(); + Map map0 = new HashMap<>(); + map0.put(ApiConstants.KEY, "key0"); + map0.put(ApiConstants.VALUE, "value0"); + details.put("0", map0); + Map map1 = new HashMap<>(); + map1.put(ApiConstants.KEY, "key1"); + map1.put(ApiConstants.VALUE, "value1"); + details.put("1", map1); + ReflectionTestUtils.setField(updateObjectStoragePoolCmd, "details", details); + + // Test: getDetails() should return a simple map + Map outDetails = updateObjectStoragePoolCmd.getDetails(); + Assert.assertNotNull(outDetails); + Assert.assertEquals(2, outDetails.size()); + Assert.assertEquals("value0", outDetails.get("key0")); + Assert.assertEquals("value1", outDetails.get("key1")); + } } diff --git a/engine/api/src/main/java/org/apache/cloudstack/storage/object/ObjectStoreEntity.java b/engine/api/src/main/java/org/apache/cloudstack/storage/object/ObjectStoreEntity.java index 7efb72d23b27..0040b1961956 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/storage/object/ObjectStoreEntity.java +++ b/engine/api/src/main/java/org/apache/cloudstack/storage/object/ObjectStoreEntity.java @@ -29,6 +29,8 @@ public interface ObjectStoreEntity extends DataStore, ObjectStore { List listBuckets(); + void verifyServiceConnectivity(); + boolean createUser(long accountId); boolean deleteBucket(BucketTO bucket); diff --git a/engine/storage/object/src/main/java/org/apache/cloudstack/storage/object/store/ObjectStoreImpl.java b/engine/storage/object/src/main/java/org/apache/cloudstack/storage/object/store/ObjectStoreImpl.java index a96d87ada045..ae28756ce43d 100644 --- a/engine/storage/object/src/main/java/org/apache/cloudstack/storage/object/store/ObjectStoreImpl.java +++ b/engine/storage/object/src/main/java/org/apache/cloudstack/storage/object/store/ObjectStoreImpl.java @@ -160,6 +160,11 @@ public List listBuckets() { return driver.listBuckets(objectStoreVO.getId()); } + @Override + public void verifyServiceConnectivity() { + driver.verifyServiceConnectivity(objectStoreVO.getId()); + } + /* Create user if not exists */ diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/object/ObjectStoreDriver.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/object/ObjectStoreDriver.java index 13aaf7c002ef..9b1564e01b68 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/object/ObjectStoreDriver.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/object/ObjectStoreDriver.java @@ -21,6 +21,7 @@ import com.amazonaws.services.s3.model.AccessControlList; import com.amazonaws.services.s3.model.BucketPolicy; import com.cloud.agent.api.to.BucketTO; +import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver; import java.util.List; @@ -31,6 +32,13 @@ public interface ObjectStoreDriver extends DataStoreDriver { List listBuckets(long storeId); + /** + * Verify Service Connectivity by testing the configuration. + * @param storeId identifies which store's configuration to verify. + * @throws CloudRuntimeException if there are any connectivity issues. + */ + void verifyServiceConnectivity(long storeId); + boolean deleteBucket(BucketTO bucket, long storeId); AccessControlList getBucketAcl(BucketTO bucket, long storeId); diff --git a/plugins/storage/object/ceph/src/main/java/org/apache/cloudstack/storage/datastore/driver/CephObjectStoreDriverImpl.java b/plugins/storage/object/ceph/src/main/java/org/apache/cloudstack/storage/datastore/driver/CephObjectStoreDriverImpl.java index 5b5eaa08dc55..9ff45e81bc13 100644 --- a/plugins/storage/object/ceph/src/main/java/org/apache/cloudstack/storage/datastore/driver/CephObjectStoreDriverImpl.java +++ b/plugins/storage/object/ceph/src/main/java/org/apache/cloudstack/storage/datastore/driver/CephObjectStoreDriverImpl.java @@ -138,6 +138,12 @@ public List listBuckets(long storeId) { return bucketsList; } + @Override + public void verifyServiceConnectivity(long storeId) { + // ideally just ping the storage using credentials. listBuckets() for now. + listBuckets(storeId); + } + @Override public boolean deleteBucket(BucketTO bucket, long storeId) { RgwAdmin rgwAdmin = getRgwAdminClient(storeId); diff --git a/plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudianHyperStoreObjectStoreDriverImpl.java b/plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudianHyperStoreObjectStoreDriverImpl.java index a0a717f52e42..2df151654d5f 100644 --- a/plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudianHyperStoreObjectStoreDriverImpl.java +++ b/plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudianHyperStoreObjectStoreDriverImpl.java @@ -123,14 +123,15 @@ protected String getHyperStoreGroupId(Domain domain) { /** * Create the HyperStore user resources matching this account if it doesn't exist. - * + *

* The following resources are created for the account: - * - HyperStore Group to match the CloudStack Domain UUID - * - HyperStore User to match the CloudStack Account UUID - * - HyperStore Root User Credentials to manage Account Buckets etc (kept private to this plugin) - * - HyperStore IAM User with IAM policy granting all S3 actions except create/delete buckets. - * - HyperStore IAM User Credentials (visible to end user as part of Bucket Details) - * + *
    + *
  • HyperStore Group to match the CloudStack Domain UUID
  • + *
  • HyperStore User to match the CloudStack Account UUID
  • + *
  • HyperStore Root User Credentials to manage Account Buckets etc (kept private to this plugin)
  • + *
  • HyperStore IAM User with IAM policy granting all S3 actions except create/delete buckets.
  • + *
  • HyperStore IAM User Credentials (visible to end user as part of Bucket Details)
  • + *
* @param accountId the CloudStack account * @param storeId the object store. * @@ -161,8 +162,8 @@ public boolean createUser(long accountId, long storeId) { logger.error(msg); throw new CloudRuntimeException(msg); } else { - // User exists and is active. We know that the group therefore exists but - // we should ensure that it is active or it will lead to unknown access key errors + // User exists and is active. We know that the group therefore exists, but + // we should ensure that it is active, or it will lead to unknown access key errors // which might confuse the administrator. Checking is clearer. CloudianGroup group = client.listGroup(hsGroupId); if (group != null && ! group.getActive()) { @@ -187,7 +188,7 @@ public boolean createUser(long accountId, long storeId) { /** * Create IAM credentials if required. - * + *

* When the HyperStore user is first created, this method will create an IAM User with an appropriate * permission policy and a set of credentials which will be returned. * After the first run, the IAM resources should already be in place in which case we just ensure @@ -238,7 +239,7 @@ protected AccessKey createIAMCredentials(long storeId, Map detai iamClient.putUserPolicy(new PutUserPolicyRequest(iamUser, CloudianHyperStoreUtil.IAM_USER_POLICY_NAME, CloudianHyperStoreUtil.IAM_USER_POLICY)); if (! createdUser && iamAccessKeyId == null) { - // User already exists but we never saved any access key before. We should try clean up + // User already exists, but we never saved any access key before. We should try clean up logger.debug("Looking for any un-managed IAM credentials for IAM User {}", iamUser); ListAccessKeysResult listRes = iamClient.listAccessKeys(new ListAccessKeysRequest().withUserName(iamUser)); for (AccessKeyMetadata accessKeyMetadata : listRes.getAccessKeyMetadata()) { @@ -379,7 +380,6 @@ private void createHSGroup(CloudianClient client, String hsGroupId, Domain domai } // Group exists and is enabled. Nothing to log. - return; } /** @@ -390,7 +390,7 @@ private void createHSGroup(CloudianClient client, String hsGroupId, Domain domai * @param hsGroupId the group to add him to * @param account the account the user represents * @return user object if successfully created, null otherwise - * @throws ServerAPIException if on other other. + * @throws CloudRuntimeException if any exception occurs */ private CloudianUser createHSUser(CloudianClient client, String hsUserId, String hsGroupId, Account account) { CloudianUser user = new CloudianUser(); @@ -401,7 +401,7 @@ private CloudianUser createHSUser(CloudianClient client, String hsUserId, String user.setFullName(account.getAccountName()); if (! client.addUser(user)) { - // The failure shouldn't be that the user already exists at this point so its something else. + // The failure shouldn't be that the user already exists at this point so it's something else. logger.error("Failed to add user id={} groupId={}", hsUserId, hsGroupId); return null; } else { @@ -426,7 +426,7 @@ public Bucket createBucket(Bucket bucket, boolean objectLock) { long storeId = bucket.getObjectStoreId(); long accountId = bucket.getAccountId(); - // get an s3client using Account Root User Credentials + // get an S3 client using Account Root User Credentials Map storeDetails = _storeDetailsDao.getDetails(storeId); String s3url = storeDetails.get(CloudianHyperStoreUtil.STORE_DETAILS_KEY_S3_URL); Map accountDetails = _accountDetailsDao.findDetails(accountId); @@ -449,14 +449,14 @@ public Bucket createBucket(Bucket bucket, boolean objectLock) { } // Step 2: Any Exception here, we try to delete the bucket. - // If deletion fails, it is not the end of the world as the - // user can try again to create the bucket which if he is - // already the owner, it will succeed. + // If deletion fails, it is not the end of the world. The + // user can try to create the bucket again as it will succeed + // if he is already the owner. try { // Enable a permissive CORS configuration configureBucketCORS(s3client, bucketName); - // Update the Bucket Information (for Bucket details page etc) + // Update the Bucket Information (for Bucket details page etc.) BucketVO bucketVO = _bucketDao.findById(bucket.getId()); bucketVO.setAccessKey(iamAccessKey); bucketVO.setSecretKey(iamSecretKey); @@ -478,15 +478,15 @@ public Bucket createBucket(Bucket bucket, boolean objectLock) { /** * Configure a permissive CrossOrigin setting on the given bucket. - * + *

* Cloudian does not enable CORS by default. The CORS configuration * is required by CloudStack so that the Javascript S3 bucket * browser can function properly. - * + *

* This method does not catch any exceptions which should be caught * by the calling method. * - * @param s3client bucket owner s3client + * @param s3client bucket owner S3 connection * @param bucketName the bucket name. * * @throws AmazonClientException and derivatives @@ -494,7 +494,7 @@ public Bucket createBucket(Bucket bucket, boolean objectLock) { private void configureBucketCORS(AmazonS3 s3client, String bucketName) { logger.debug("Configuring CORS for bucket {}", bucketName); - List corsRules = new ArrayList(); + List corsRules = new ArrayList<>(); CORSRule allowAnyRule = new CORSRule().withId("AllowAny"); allowAnyRule.setAllowedOrigins("*"); allowAnyRule.setAllowedHeaders("*"); @@ -523,7 +523,7 @@ private void configureBucketCORS(AmazonS3 s3client, String bucketName) { @Override public List listBuckets(long storeId) { Map bucketUsage = getAllBucketsUsage(storeId); - List bucketList = new ArrayList(); + List bucketList = new ArrayList<>(); for (String bucketName : bucketUsage.keySet()) { Bucket bucket = new BucketObject(); bucket.setName(bucketName); @@ -532,24 +532,47 @@ public List listBuckets(long storeId) { return bucketList; } + /** + * Verify Service Connectivity by testing the configuration. + * @param storeId identifies which store's configuration to verify. + * @throws CloudRuntimeException if there are any connectivity issues. + */ + @Override + public void verifyServiceConnectivity(long storeId) { + // 1. test Admin Service connectivity. + CloudianClient cloudianClient = getCloudianClientByStoreId(storeId); + String version = cloudianClient.getServerVersion(); + + // 2. test S3 Service connectivity. + Map storeDetails = _storeDetailsDao.getDetails(storeId); + String s3Url = storeDetails.get(CloudianHyperStoreUtil.STORE_DETAILS_KEY_S3_URL); + CloudianHyperStoreUtil.validateS3Url(s3Url); + + // 3. test IAM Service connectivity. + String iamUrl = storeDetails.get(CloudianHyperStoreUtil.STORE_DETAILS_KEY_IAM_URL); + CloudianHyperStoreUtil.validateIAMUrl(iamUrl); + + logger.info("Connectivity confirmed to Cloudian HyperStore {}", version); + } + /** * Delete an empty bucket. * This operation fails if the bucket is not empty. * @param bucket the bucket to delete * @param storeId the store the bucket belongs to. - * @returns true on success or throws an exception. + * @return true on success or throws an exception. * @throws CloudRuntimeException if the bucket deletion fails */ @Override public boolean deleteBucket(BucketTO bucket, long storeId) { AmazonS3 s3client = getS3ClientByBucketAndStore(bucket, storeId); - logger.debug("Deleting bucket {}", bucket.getName()); + logger.debug("Deleting the bucket {}", bucket.getName()); try { s3client.deleteBucket(bucket.getName()); - logger.info("Successfully deleted bucket {}", bucket.getName()); + logger.info("Successfully deleted the bucket {}", bucket.getName()); return true; } catch (AmazonClientException e) { - logger.error("Failed to delete bucket " + bucket.getName(), e); + logger.error("Failed to delete bucket {}", bucket.getName(), e); throw new CloudRuntimeException(e); } } @@ -563,7 +586,7 @@ public AccessControlList getBucketAcl(BucketTO bucket, long storeId) { logger.info("Successfully got the bucket ACL for {}", bucket.getName()); return acl; } catch (AmazonClientException e) { - logger.error("Failed to get the bucket ACL for " + bucket.getName(), e); + logger.error("Failed to get the bucket ACL for {}", bucket.getName(), e); throw new CloudRuntimeException(e); } } @@ -575,9 +598,8 @@ public void setBucketAcl(BucketTO bucket, AccessControlList acl, long storeId) { try { s3client.setBucketAcl(bucket.getName(), acl); logger.info("Successfully set the bucket ACL for {}", bucket.getName()); - return; } catch (AmazonClientException e) { - logger.error("Failed to set the bucket ACL for " + bucket.getName(), e); + logger.error("Failed to set the bucket ACL for {}", bucket.getName(), e); throw new CloudRuntimeException(e); } } @@ -594,30 +616,28 @@ public void setBucketPolicy(BucketTO bucket, String policy, long storeId) { return; } - StringBuilder sb = new StringBuilder(); - sb.append("{\n"); - sb.append(" \"Version\": \"2012-10-17\",\n"); - sb.append(" \"Statement\": [\n"); - sb.append(" {\n"); - sb.append(" \"Sid\": \"PublicReadForObjects\",\n"); - sb.append(" \"Effect\": \"Allow\",\n"); - sb.append(" \"Principal\": \"*\",\n"); - sb.append(" \"Action\": \"s3:GetObject\",\n"); - sb.append(" \"Resource\": \"arn:aws:s3:::%s/*\"\n"); - sb.append(" }\n"); - sb.append(" ]\n"); - sb.append("}\n"); + String policyFormat = "{\n" + + " \"Version\": \"2012-10-17\",\n" + + " \"Statement\": [\n" + + " {\n" + + " \"Sid\": \"PublicReadForObjects\",\n" + + " \"Effect\": \"Allow\",\n" + + " \"Principal\": \"*\",\n" + + " \"Action\": \"s3:GetObject\",\n" + + " \"Resource\": \"arn:aws:s3:::%s/*\"\n" + + " }\n" + + " ]\n" + + "}\n"; - String jsonPolicy = String.format(sb.toString(), bucket.getName()); + String jsonPolicy = String.format(policyFormat, bucket.getName()); AmazonS3 s3client = getS3ClientByBucketAndStore(bucket, storeId); logger.debug("Setting the bucket policy to {} for {}", policy, bucket.getName()); try { s3client.setBucketPolicy(bucket.getName(), jsonPolicy); logger.info("Successfully set the bucket policy to {} for {}", policy, bucket.getName()); - return; } catch (AmazonClientException e) { - logger.error("Failed to set the bucket policy for " + bucket.getName(), e); + logger.error("Failed to set the bucket policy for {}", bucket.getName(), e); throw new CloudRuntimeException(e); } } @@ -631,7 +651,7 @@ public BucketPolicy getBucketPolicy(BucketTO bucket, long storeId) { logger.info("Successfully got the bucket policy for {}", bucket.getName()); return bp; } catch (AmazonClientException e) { - logger.error("Failed to get the bucket policy for " + bucket.getName(), e); + logger.error("Failed to get the bucket policy for {}", bucket.getName(), e); throw new CloudRuntimeException(e); } } @@ -643,9 +663,8 @@ public void deleteBucketPolicy(BucketTO bucket, long storeId) { try { s3client.deleteBucketPolicy(bucket.getName()); logger.info("Successfully deleted bucket policy for {}", bucket.getName()); - return; } catch (AmazonClientException e) { - logger.error("Failed to delete bucket policy for " + bucket.getName(), e); + logger.error("Failed to delete bucket policy for {}", bucket.getName(), e); throw new CloudRuntimeException(e); } } @@ -664,7 +683,7 @@ public boolean setBucketEncryption(BucketTO bucket, long storeId) { ServerSideEncryptionRule sseRule = new ServerSideEncryptionRule(); sseRule.setApplyServerSideEncryptionByDefault(sseByDefault); - List sseRules = new ArrayList(); + List sseRules = new ArrayList<>(); sseRules.add(sseRule); ServerSideEncryptionConfiguration sseConf = new ServerSideEncryptionConfiguration(); @@ -676,7 +695,7 @@ public boolean setBucketEncryption(BucketTO bucket, long storeId) { logger.info("Successfully enabled bucket encryption configuration for {}", bucket.getName()); return true; } catch (AmazonClientException e) { - logger.error("Failed to enable bucket encryption configuration for " + bucket.getName(), e); + logger.error("Failed to enable bucket encryption configuration for {}", bucket.getName(), e); throw new CloudRuntimeException(e); } } @@ -690,7 +709,7 @@ public boolean deleteBucketEncryption(BucketTO bucket, long storeId) { logger.info("Successfully deleted bucket encryption configuration for {}", bucket.getName()); return true; } catch (AmazonClientException e) { - logger.error("Failed to delete bucket encryption configuration for " + bucket.getName(), e); + logger.error("Failed to delete bucket encryption configuration for {}", bucket.getName(), e); throw new CloudRuntimeException(e); } } @@ -706,7 +725,7 @@ public boolean setBucketVersioning(BucketTO bucket, long storeId) { logger.info("Successfully enabled versioning for bucket {}", bucket.getName()); return true; } catch (AmazonClientException e) { - logger.error("Failed to enable versioning for bucket " + bucket.getName(), e); + logger.error("Failed to enable versioning for bucket {}", bucket.getName(), e); throw new CloudRuntimeException(e); } } @@ -722,14 +741,14 @@ public boolean deleteBucketVersioning(BucketTO bucket, long storeId) { logger.info("Successfully suspended versioning for bucket {}", bucket.getName()); return true; } catch (AmazonClientException e) { - logger.error("Failed to suspend versioning for bucket " + bucket.getName(), e); + logger.error("Failed to suspend versioning for bucket {}", bucket.getName(), e); throw new CloudRuntimeException(e); } } /** * Set the bucket quota to a size limit specified in GiB. - * + *

* Cloudian HyperStore does not currently support bucket quota limits. * CloudStack itself requires a quota to be set. HyperStore may add * Bucket Quota support in a future version. Currently, we only support @@ -753,8 +772,8 @@ public void setBucketQuota(BucketTO bucket, long storeId, long size) { /** * Return a map of bucket names managed by this store and their sizes (in bytes). - * - * Note: Bucket Usage Statistics in HyperStore are disabled by default. They + *

+ * Note: Bucket Usage Statistics in HyperStore are disabled by default. They * can be enabled by the HyperStore Administrator by setting of the configuration * 's3.qos.bucketLevel=true'. If this is not enabled, the values returned will * either be 0 or out of date. @@ -763,16 +782,16 @@ public void setBucketQuota(BucketTO bucket, long storeId, long size) { */ @Override public Map getAllBucketsUsage(long storeId) { - Map bucketUsage = new HashMap(); + Map bucketUsage = new HashMap<>(); List bucketList = _bucketDao.listByObjectStoreId(storeId); if (bucketList.isEmpty()) { return bucketUsage; } - // Create an unique list of domains from the bucket list + // Create a unique list of domains from the bucket list // and add all the bucket names to the bucketUsage map with value -1 as a marker // to know which buckets CloudStack cares about. The -1 will be replaced later. - List domainIds = new ArrayList(); + List domainIds = new ArrayList<>(); for (BucketVO bucket : bucketList) { long bucketDomainId = bucket.getDomainId(); if (! domainIds.contains(bucketDomainId)) { @@ -793,8 +812,8 @@ public Map getAllBucketsUsage(long storeId) { // Update the -1 entry to actual byteCount. bucketUsage.replace(cbu.getBucketName(), cbu.getByteCount()); } else { - // Replace with 0 instead of actual value. Race condition can cause this and it - // should be fixed automatically by a repair job. + // Replace with 0 instead of a negative value. A race condition can cause + // on the server. Usage is corrected periodically so it should self-medicate. bucketUsage.replace(cbu.getBucketName(), 0L); logger.info("Ignoring negative bucket usage for \"{}\": {}", cbu.getBucketName(), cbu.getByteCount()); } @@ -803,7 +822,7 @@ public Map getAllBucketsUsage(long storeId) { } // Remove any remaining -1 entries. These would probably be buckets that were - // deleted outside of CloudStack control. A missing entry might be better than + // deleted outside CloudStack control. A missing entry might be better than // returning the bucket name with -1 or 0. bucketUsage.entrySet().removeIf(entry -> entry.getValue() == -1); @@ -832,8 +851,8 @@ protected CloudianClient getCloudianClientByStoreId(long storeId) { * Returns an S3 connection for the store and account identified by the bucket. * NOTE: https connections must use a trusted certificate. * - * @param store the object store of the S3 service to connect to * @param bucket bucket information identifying the account which identifies the credentials to use. + * @param storeId the object store of the S3 service to connect to * @return an S3 connection (never null) * @throws CloudRuntimeException on failure. */ diff --git a/plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudianHyperStoreObjectStoreLifeCycleImpl.java b/plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudianHyperStoreObjectStoreLifeCycleImpl.java index d2fda09c65d1..81abd2e1e7f4 100644 --- a/plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudianHyperStoreObjectStoreLifeCycleImpl.java +++ b/plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudianHyperStoreObjectStoreLifeCycleImpl.java @@ -66,7 +66,7 @@ public DataStore initialize(Map dsInfos) { throw new CloudRuntimeException(msg); } - Map objectStoreParameters = new HashMap(); + Map objectStoreParameters = new HashMap<>(); objectStoreParameters.put(CloudianHyperStoreUtil.STORE_KEY_NAME, name); objectStoreParameters.put(CloudianHyperStoreUtil.STORE_KEY_URL, url); objectStoreParameters.put(CloudianHyperStoreUtil.STORE_KEY_PROVIDER_NAME, providerName); diff --git a/plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/provider/CloudianHyperStoreObjectStoreProviderImpl.java b/plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/provider/CloudianHyperStoreObjectStoreProviderImpl.java index eb924b3b709b..bd28b7da986e 100644 --- a/plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/provider/CloudianHyperStoreObjectStoreProviderImpl.java +++ b/plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/provider/CloudianHyperStoreObjectStoreProviderImpl.java @@ -28,7 +28,6 @@ import org.apache.cloudstack.storage.datastore.lifecycle.CloudianHyperStoreObjectStoreLifeCycleImpl; import org.apache.cloudstack.storage.datastore.util.CloudianHyperStoreUtil; import org.apache.cloudstack.storage.object.ObjectStoreDriver; -import org.apache.cloudstack.storage.object.datastore.ObjectStoreHelper; import org.apache.cloudstack.storage.object.datastore.ObjectStoreProviderManager; import org.apache.cloudstack.storage.object.store.lifecycle.ObjectStoreLifeCycle; import org.springframework.stereotype.Component; @@ -43,10 +42,7 @@ public class CloudianHyperStoreObjectStoreProviderImpl implements ObjectStorePro @Inject ObjectStoreProviderManager storeMgr; - @Inject - ObjectStoreHelper helper; - private final String providerName = CloudianHyperStoreUtil.OBJECT_STORE_PROVIDER_NAME; protected ObjectStoreLifeCycle lifeCycle; protected ObjectStoreDriver driver; @@ -57,7 +53,7 @@ public DataStoreLifeCycle getDataStoreLifeCycle() { @Override public String getName() { - return this.providerName; + return CloudianHyperStoreUtil.OBJECT_STORE_PROVIDER_NAME; } @Override @@ -80,7 +76,7 @@ public HypervisorHostListener getHostListener() { @Override public Set getTypes() { - Set types = new HashSet(); + Set types = new HashSet<>(); types.add(DataStoreProviderType.OBJECT); return types; } diff --git a/plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/util/CloudianHyperStoreUtil.java b/plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/util/CloudianHyperStoreUtil.java index efa10229428c..1d110b7b1322 100644 --- a/plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/util/CloudianHyperStoreUtil.java +++ b/plugins/storage/object/cloudian/src/main/java/org/apache/cloudstack/storage/datastore/util/CloudianHyperStoreUtil.java @@ -90,7 +90,7 @@ public class CloudianHyperStoreUtil { /** * This method is solely for test purposes so that we can mock the timeout. * - * @returns the timeout in seconds + * @return the timeout in seconds */ protected static int getAdminTimeoutSeconds() { return DEFAULT_ADMIN_TIMEOUT_SECONDS; @@ -101,7 +101,7 @@ protected static int getAdminTimeoutSeconds() { * @param url the url of the ADMIN API service * @param user the admin username to connect as * @param pass the matching admin password - * @param validateSSL validate the SSL Certificate (when using https://) + * @param validateSSL validate the SSL Certificate (when using https) * @return a connection object (never null) * @throws CloudRuntimeException if the connection fails for any reason */ @@ -166,7 +166,7 @@ public static AmazonIdentityManagement getIAMClient(String url, String accessKey /** * Test the S3Url to confirm it behaves like an S3 Service. - * + *

* The method uses bad credentials and looks for the particular error from S3 * that says InvalidAccessKeyId was used. The method quietly returns if * we connect and get the expected error back. @@ -176,6 +176,9 @@ public static AmazonIdentityManagement getIAMClient(String url, String accessKey * @throws RuntimeException if there is any issue. */ public static void validateS3Url(String s3Url) { + if (StringUtils.isBlank(s3Url)) { + throw new CloudRuntimeException("The S3 URL must not be blank."); + } try { AmazonS3 s3Client = CloudianHyperStoreUtil.getS3Client(s3Url, "unknown", "unknown"); s3Client.listBuckets(); @@ -189,7 +192,7 @@ public static void validateS3Url(String s3Url) { /** * Test the IAMUrl to confirm it behaves like an IAM Service. - * + *

* The method uses bad credentials and looks for the particular error from IAM * that says InvalidAccessKeyId or InvalidClientTokenId was used. The method quietly * returns if we connect and get the expected error back. @@ -199,6 +202,9 @@ public static void validateS3Url(String s3Url) { * @throws RuntimeException if there is any issue. */ public static void validateIAMUrl(String iamUrl) { + if (StringUtils.isBlank(iamUrl)) { + throw new CloudRuntimeException("The IAM URL must not be blank."); + } try { AmazonIdentityManagement iamClient = CloudianHyperStoreUtil.getIAMClient(iamUrl, "unknown", "unknown"); iamClient.listAccessKeys(); diff --git a/plugins/storage/object/cloudian/src/test/java/org/apache/cloudstack/storage/datastore/driver/CloudianHyperStoreObjectStoreDriverImplTest.java b/plugins/storage/object/cloudian/src/test/java/org/apache/cloudstack/storage/datastore/driver/CloudianHyperStoreObjectStoreDriverImplTest.java index 1cf99ca53465..44b385c55dcc 100644 --- a/plugins/storage/object/cloudian/src/test/java/org/apache/cloudstack/storage/datastore/driver/CloudianHyperStoreObjectStoreDriverImplTest.java +++ b/plugins/storage/object/cloudian/src/test/java/org/apache/cloudstack/storage/datastore/driver/CloudianHyperStoreObjectStoreDriverImplTest.java @@ -159,7 +159,7 @@ public void setUp() { when(objectStoreVO.getUrl()).thenReturn(TEST_ADMIN_URL); // The StoreDetailMap has Endpoint info and Admin Credentials - StoreDetailsMap = new HashMap(); + StoreDetailsMap = new HashMap<>(); StoreDetailsMap.put(CloudianHyperStoreUtil.STORE_DETAILS_KEY_USER_NAME, TEST_ADMIN_USER_NAME); StoreDetailsMap.put(CloudianHyperStoreUtil.STORE_DETAILS_KEY_PASSWORD, TEST_ADMIN_PASSWORD); StoreDetailsMap.put(CloudianHyperStoreUtil.STORE_DETAILS_KEY_VALIDATE_SSL, TEST_ADMIN_VALIDATE_SSL); @@ -168,7 +168,7 @@ public void setUp() { when(objectStoreDetailsDao.getDetails(TEST_STORE_ID)).thenReturn(StoreDetailsMap); // The AccountDetailsMap has credentials for operating on the account. - AccountDetailsMap = new HashMap(); + AccountDetailsMap = new HashMap<>(); AccountDetailsMap.put(CloudianHyperStoreUtil.KEY_ROOT_ACCESS_KEY, TEST_ROOT_AK); AccountDetailsMap.put(CloudianHyperStoreUtil.KEY_ROOT_SECRET_KEY, TEST_ROOT_SK); AccountDetailsMap.put(CloudianHyperStoreUtil.KEY_IAM_ACCESS_KEY, TEST_IAM_AK); @@ -190,7 +190,7 @@ public void testGetStoreTO() { } @Test - public void testCreateBucket() throws Exception { + public void testCreateBucket() { doReturn(s3Client).when(cloudianHyperStoreObjectStoreDriverImpl).getS3Client(anyString(), anyString(), anyString()); when(bucketDao.findById(anyLong())).thenReturn(bucketVo); @@ -213,9 +213,9 @@ public void testCreateBucket() throws Exception { } @Test - public void testCreateHSCredential() throws Exception { + public void testCreateHSCredential() { cloudianClient = mock(CloudianClient.class); - List CredList = new ArrayList(); + List CredList = new ArrayList<>(); CloudianCredential c1 = new CloudianCredential(); c1.setActive(false); c1.setCreateDate(new Date(1L)); // oldest but inactive @@ -241,8 +241,8 @@ public void testCreateHSCredential() throws Exception { } @Test - public void testGetAllBucketsUsageNoBuckets() throws Exception { - when(bucketDao.listByObjectStoreId(TEST_STORE_ID)).thenReturn(new ArrayList()); + public void testGetAllBucketsUsageNoBuckets() { + when(bucketDao.listByObjectStoreId(TEST_STORE_ID)).thenReturn(new ArrayList<>()); Map emptyMap = cloudianHyperStoreObjectStoreDriverImpl.getAllBucketsUsage(TEST_STORE_ID); assertNotNull(emptyMap); assertEquals(0, emptyMap.size()); @@ -255,7 +255,7 @@ public void testGetAllBucketsUsageTwoDomains() { BucketVO b2 = new BucketVO(TEST_ACCOUNT_ID, 1L, TEST_STORE_ID, "b2", null, false, false, false, null); BucketVO b3 = new BucketVO(TEST_ACCOUNT_ID, 2L, TEST_STORE_ID, "b3", null, false, false, false, null); BucketVO b4 = new BucketVO(TEST_ACCOUNT_ID, 2L, TEST_STORE_ID, "b4", null, false, false, false, null); - List BucketList = new ArrayList(); + List BucketList = new ArrayList<>(); BucketList.add(b1); // b1 owned by domain 1, exists BucketList.add(b2); // b2 owned by domain 1, deleted in object store (so no usage info) BucketList.add(b3); // b3 owned by domain 2, exists @@ -287,9 +287,9 @@ public void testGetAllBucketsUsageTwoDomains() { CloudianBucketUsage bu5 = new CloudianBucketUsage(); bu5.setBucketName("b5"); bu5.setByteCount(5L); - List d1bucketList = new ArrayList(); + List d1bucketList = new ArrayList<>(); d1bucketList.add(bu1); - List d2bucketList = new ArrayList(); + List d2bucketList = new ArrayList<>(); d2bucketList.add(bu3); d2bucketList.add(bu4); d2bucketList.add(bu5); @@ -297,9 +297,9 @@ public void testGetAllBucketsUsageTwoDomains() { when(d1U1Usage.getBuckets()).thenReturn(d1bucketList); CloudianUserBucketUsage d2U1Usage = mock(CloudianUserBucketUsage.class); when(d2U1Usage.getBuckets()).thenReturn(d2bucketList); - List d1Usage = new ArrayList(); + List d1Usage = new ArrayList<>(); d1Usage.add(d1U1Usage); - List d2Usage = new ArrayList(); + List d2Usage = new ArrayList<>(); d2Usage.add(d2U1Usage); doReturn(cloudianClient).when(cloudianHyperStoreObjectStoreDriverImpl).getCloudianClientByStoreId(TEST_STORE_ID); @@ -322,10 +322,10 @@ public void testGetAllBucketsUsageTwoDomains() { } @Test - public void testCreateUserNotExists() throws Exception { + public void testCreateUserNotExists() { // ensure no account credentials are returned in the account details for new user. Mockito.reset(accountDetailsDao); - when(accountDetailsDao.findDetails(TEST_ACCOUNT_ID)).thenReturn(new HashMap()); + when(accountDetailsDao.findDetails(TEST_ACCOUNT_ID)).thenReturn(new HashMap<>()); String hsUserId = "user1"; String hsGroupId = "group1"; @@ -337,12 +337,12 @@ public void testCreateUserNotExists() throws Exception { doReturn(cloudianClient).when(cloudianHyperStoreObjectStoreDriverImpl).getCloudianClientByStoreId(TEST_STORE_ID); - // Setup the user and group as not found. + // Set up the user and group as not found. when(cloudianClient.listUser(hsUserId, hsGroupId)).thenReturn(null); when(cloudianClient.listGroup(hsGroupId)).thenReturn(null); when(cloudianClient.addUser(any(CloudianUser.class))).thenReturn(true); - // lets assume no credentials added, so we add new ones. - when(cloudianClient.listCredentials(hsUserId, hsGroupId)).thenReturn(new ArrayList()); + // let's assume no credentials added, so we add new ones. + when(cloudianClient.listCredentials(hsUserId, hsGroupId)).thenReturn(new ArrayList<>()); CloudianCredential credential = new CloudianCredential(); credential.setAccessKey(TEST_ROOT_AK); credential.setSecretKey(TEST_ROOT_SK); @@ -358,11 +358,11 @@ public void testCreateUserNotExists() throws Exception { when(iamClient.createAccessKey(any(CreateAccessKeyRequest.class))).thenReturn(accessKeyResult); // Next Check what will be persisted in DB after everything created. - // Even though its not going to be true for a new user, lets have 1 bucket + // Even though it's not going to be true for a new user, lets have 1 bucket // whose credentials need to be updated. BucketVO bucketToUpdate = mock(BucketVO.class); when(bucketToUpdate.getId()).thenReturn(9L); - List bucketUpdateList = new ArrayList(); + List bucketUpdateList = new ArrayList<>(); bucketUpdateList.add(bucketToUpdate); when(bucketDao.listByObjectStoreIdAndAccountId(TEST_STORE_ID, TEST_ACCOUNT_ID)).thenReturn(bucketUpdateList); @@ -413,7 +413,7 @@ public void testCreateUserExists() { doReturn(cloudianClient).when(cloudianHyperStoreObjectStoreDriverImpl).getCloudianClientByStoreId(TEST_STORE_ID); - // Setup the user/group as existing and active + // Set up the user/group as existing and active CloudianUser user = mock(CloudianUser.class); CloudianGroup group = mock(CloudianGroup.class); when(user.getActive()).thenReturn(true); @@ -421,20 +421,20 @@ public void testCreateUserExists() { when(cloudianClient.listUser(hsUserId, hsGroupId)).thenReturn(user); when(cloudianClient.listGroup(hsGroupId)).thenReturn(group); - // Setup the HS Credential to match known Root credential + // Set up the HS Credential to match known Root credential CloudianCredential credential = new CloudianCredential(); credential.setAccessKey(TEST_ROOT_AK); credential.setSecretKey(TEST_ROOT_SK); credential.setActive(true); credential.setCreateDate(new Date(1L)); - List credentials = new ArrayList(); + List credentials = new ArrayList<>(); credentials.add(credential); when(cloudianClient.listCredentials(hsUserId, hsGroupId)).thenReturn(credentials); // Setup IAM to return 2 credentials, one that matches and one that doesn't doReturn(iamClient).when(cloudianHyperStoreObjectStoreDriverImpl).getIAMClientByStoreId(TEST_STORE_ID, credential); ListAccessKeysResult listAccessKeyResult = mock(ListAccessKeysResult.class); - List listAccessKeyMetadata = new ArrayList(); + List listAccessKeyMetadata = new ArrayList<>(); AccessKeyMetadata accessKeyNoMatch = mock(AccessKeyMetadata.class); when(accessKeyNoMatch.getAccessKeyId()).thenReturn("no_match"); AccessKeyMetadata accessKeyMatch = mock(AccessKeyMetadata.class); @@ -479,7 +479,7 @@ public void testCreateUserDisabledUserExists() { doReturn(cloudianClient).when(cloudianHyperStoreObjectStoreDriverImpl).getCloudianClientByStoreId(TEST_STORE_ID); - // Setup the user to be found but inactive. + // Set up the user to be found but inactive. CloudianUser user = mock(CloudianUser.class); when(user.getActive()).thenReturn(false); when(cloudianClient.listUser(hsUserId, hsGroupId)).thenReturn(user); @@ -501,7 +501,7 @@ public void testCreateUserDisabledGroupExists() { doReturn(cloudianClient).when(cloudianHyperStoreObjectStoreDriverImpl).getCloudianClientByStoreId(TEST_STORE_ID); - // Setup the user to not be found so that we check for a group + // Set up the user to not be found so that we check for a group when(cloudianClient.listUser(hsUserId, hsGroupId)).thenReturn(null); CloudianGroup group = mock(CloudianGroup.class); when(group.getActive()).thenReturn(false); @@ -514,7 +514,7 @@ public void testCreateUserDisabledGroupExists() { @Test public void testListBuckets() { - Map bucketUsageMap = new HashMap(); + Map bucketUsageMap = new HashMap<>(); bucketUsageMap.put("b1", 1L); bucketUsageMap.put("b2", 2L); when(cloudianHyperStoreObjectStoreDriverImpl.getAllBucketsUsage(anyLong())).thenReturn(bucketUsageMap); @@ -525,7 +525,46 @@ public void testListBuckets() { } @Test - public void testDeleteBucket() throws Exception { + public void testVerifyServiceConnectivity() { + when(cloudianHyperStoreObjectStoreDriverImpl.getCloudianClientByStoreId(TEST_STORE_ID)).thenReturn(cloudianClient); + + try (MockedStatic mockStatic = Mockito.mockStatic(CloudianHyperStoreUtil.class)) { + // This allows both validateS3URL and validateIAMUrl to be called. + cloudianHyperStoreObjectStoreDriverImpl.verifyServiceConnectivity(TEST_STORE_ID); + + // getServerVersion() should be called once. + verify(cloudianClient, times(1)).getServerVersion(); + // validate called once on each Url type from the map. + mockStatic.verify(() -> CloudianHyperStoreUtil.validateS3Url(TEST_S3_URL), times(1)); + mockStatic.verify(() -> CloudianHyperStoreUtil.validateIAMUrl(TEST_IAM_URL), times(1)); + } + } + + @Test + public void testVerifyServiceConnectivityMissingS3Url() { + // delete the S3 URL from our map + StoreDetailsMap.remove(CloudianHyperStoreUtil.STORE_DETAILS_KEY_S3_URL); + when(cloudianHyperStoreObjectStoreDriverImpl.getCloudianClientByStoreId(TEST_STORE_ID)).thenReturn(cloudianClient); + + try (MockedStatic mockStatic = Mockito.mockStatic(CloudianHyperStoreUtil.class)) { + // mock so that we can verify what is called, but pass s3 through to real validation. + mockStatic.when(() -> CloudianHyperStoreUtil.validateS3Url(any())).thenCallRealMethod(); + + // Lack of S3 url should mean validate will fail with an exception + CloudRuntimeException thrown = assertThrows(CloudRuntimeException.class, () -> cloudianHyperStoreObjectStoreDriverImpl.verifyServiceConnectivity(TEST_STORE_ID)); + assertNotNull(thrown); + assertTrue(thrown.getMessage().contains("must not be blank")); + + // getServerVersion() should be called once. + verify(cloudianClient, times(1)).getServerVersion(); + // S3 validate will be called once with null, but IAM validate will not. + mockStatic.verify(() -> CloudianHyperStoreUtil.validateS3Url(null), times(1)); + mockStatic.verify(() -> CloudianHyperStoreUtil.validateIAMUrl(TEST_IAM_URL), never()); + } + } + + @Test + public void testDeleteBucket() { doReturn(s3Client).when(cloudianHyperStoreObjectStoreDriverImpl).getS3ClientByBucketAndStore(any(), anyLong()); BucketTO bucket = mock(BucketTO.class); when(bucket.getName()).thenReturn(TEST_BUCKET_NAME); @@ -535,7 +574,7 @@ public void testDeleteBucket() throws Exception { } @Test - public void testSetBucketPolicyPrivate() throws Exception { + public void testSetBucketPolicyPrivate() { doReturn(s3Client).when(cloudianHyperStoreObjectStoreDriverImpl).getS3ClientByBucketAndStore(any(), anyLong()); BucketTO bucket = mock(BucketTO.class); when(bucket.getName()).thenReturn(TEST_BUCKET_NAME); @@ -546,7 +585,7 @@ public void testSetBucketPolicyPrivate() throws Exception { } @Test - public void testSetBucketPolicyPublic() throws Exception { + public void testSetBucketPolicyPublic() { doReturn(s3Client).when(cloudianHyperStoreObjectStoreDriverImpl).getS3ClientByBucketAndStore(any(), anyLong()); BucketTO bucket = mock(BucketTO.class); when(bucket.getName()).thenReturn(TEST_BUCKET_NAME); @@ -587,7 +626,7 @@ public void testSetBucketEncryption() { } @Test - public void testDeleteBucketEncryption() throws Exception { + public void testDeleteBucketEncryption() { doReturn(s3Client).when(cloudianHyperStoreObjectStoreDriverImpl).getS3ClientByBucketAndStore(any(), anyLong()); BucketTO bucket = mock(BucketTO.class); when(bucket.getName()).thenReturn(TEST_BUCKET_NAME); @@ -597,7 +636,7 @@ public void testDeleteBucketEncryption() throws Exception { } @Test - public void testSetBucketVersioning() throws Exception { + public void testSetBucketVersioning() { doReturn(s3Client).when(cloudianHyperStoreObjectStoreDriverImpl).getS3ClientByBucketAndStore(any(), anyLong()); BucketTO bucket = mock(BucketTO.class); when(bucket.getName()).thenReturn(TEST_BUCKET_NAME); @@ -612,7 +651,7 @@ public void testSetBucketVersioning() throws Exception { } @Test - public void testDeleteBucketVersioning() throws Exception { + public void testDeleteBucketVersioning() { doReturn(s3Client).when(cloudianHyperStoreObjectStoreDriverImpl).getS3ClientByBucketAndStore(any(), anyLong()); BucketTO bucket = mock(BucketTO.class); when(bucket.getName()).thenReturn(TEST_BUCKET_NAME); @@ -643,7 +682,7 @@ public void testGetS3ClientByBucketAndStore() { BucketVO b2 = new BucketVO(TEST_ACCOUNT_ID, 1L, TEST_STORE_ID, "b2", null, false, false, false, null); BucketVO b3 = new BucketVO(TEST_ACCOUNT_ID, 2L, TEST_STORE_ID, TEST_BUCKET_NAME, null, false, false, false, null); BucketVO b4 = new BucketVO(TEST_ACCOUNT_ID, 2L, TEST_STORE_ID, "b4", null, false, false, false, null); - List BucketList = new ArrayList(); + List BucketList = new ArrayList<>(); BucketList.add(b1); // b1 owned by domain 1, exists BucketList.add(b2); // b2 owned by domain 1, exists BucketList.add(b3); // b3 owned by domain 2, exists - our TEST BUCKET @@ -672,7 +711,7 @@ public void testGetS3ClientByBucketAndStore() { public void testGetS3ClientByBucketAndStoreNoMatch() { // Prepare Buckets the store knows about. BucketVO b1 = new BucketVO(TEST_ACCOUNT_ID, 1L, TEST_STORE_ID, "b1", null, false, false, false, null); - List BucketList = new ArrayList(); + List BucketList = new ArrayList<>(); BucketList.add(b1); // b1 owned by domain 1, exists when(bucketDao.listByObjectStoreId(TEST_STORE_ID)).thenReturn(BucketList); diff --git a/plugins/storage/object/cloudian/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudianHyperStoreObjectStoreLifeCycleImplTest.java b/plugins/storage/object/cloudian/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudianHyperStoreObjectStoreLifeCycleImplTest.java index fe6dc16f3fc5..20e77ca93ce6 100644 --- a/plugins/storage/object/cloudian/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudianHyperStoreObjectStoreLifeCycleImplTest.java +++ b/plugins/storage/object/cloudian/src/test/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudianHyperStoreObjectStoreLifeCycleImplTest.java @@ -106,13 +106,13 @@ public void setUp() { cloudianHyperStoreObjectStoreLifeCycleImpl.objectStoreHelper = objectStoreHelper; cloudianHyperStoreObjectStoreLifeCycleImpl.objectStoreMgr = objectStoreMgr; - guiDetailMap = new HashMap(); + guiDetailMap = new HashMap<>(); guiDetailMap.put("accesskey", TEST_ADMIN_USERNAME); guiDetailMap.put("secretkey", TEST_ADMIN_PASSWORD); guiDetailMap.put("validateSSL", TEST_VALIDATE_SSL); guiDetailMap.put("s3Url", TEST_S3_URL); guiDetailMap.put("iamUrl", TEST_IAM_URL); - guiDataStoreMap = new HashMap(); + guiDataStoreMap = new HashMap<>(); guiDataStoreMap.put("name", TEST_STORE_NAME); guiDataStoreMap.put("url", TEST_ADMIN_URL); guiDataStoreMap.put("providerName", TEST_PROVIDER_NAME); @@ -130,7 +130,7 @@ public void testInitializeValidation() { mockStatic.when(() -> CloudianHyperStoreUtil.getCloudianClient(anyString(), anyString(), anyString(), anyBoolean())).thenReturn(cloudianClient); mockStatic.when(() -> CloudianHyperStoreUtil.getS3Client(anyString(), anyString(), anyString())).thenReturn(s3Client); mockStatic.when(() -> CloudianHyperStoreUtil.getIAMClient(anyString(), anyString(), anyString())).thenReturn(iamClient); - // Ensure real validation methods are called (as everything was mocked). These ones we need. + // Ensure real validation methods are called (as everything was mocked). These we need. mockStatic.when(() -> CloudianHyperStoreUtil.validateS3Url(anyString())).thenCallRealMethod(); mockStatic.when(() -> CloudianHyperStoreUtil.validateIAMUrl(anyString())).thenCallRealMethod(); diff --git a/plugins/storage/object/cloudian/src/test/java/org/apache/cloudstack/storage/datastore/util/CloudianHyperStoreUtilTest.java b/plugins/storage/object/cloudian/src/test/java/org/apache/cloudstack/storage/datastore/util/CloudianHyperStoreUtilTest.java index c83d5d7fd2f5..9f2312281f46 100644 --- a/plugins/storage/object/cloudian/src/test/java/org/apache/cloudstack/storage/datastore/util/CloudianHyperStoreUtilTest.java +++ b/plugins/storage/object/cloudian/src/test/java/org/apache/cloudstack/storage/datastore/util/CloudianHyperStoreUtilTest.java @@ -112,34 +112,40 @@ public void testGetCloudianClientShortenedTimeout() { mockStatic.when(() -> CloudianHyperStoreUtil.getAdminTimeoutSeconds()).thenReturn(1); mockStatic.when(() -> CloudianHyperStoreUtil.getCloudianClient(anyString(), anyString(), anyString(), anyBoolean())).thenCallRealMethod(); - // Get a connection and try using it but it should timeout + // Get a connection and try using it but it should time out String url = String.format("http://localhost:%d", port); CloudianClient cc = CloudianHyperStoreUtil.getCloudianClient(url, "u", "p", false); long before = System.currentTimeMillis(); - ServerApiException thrown = assertThrows(ServerApiException.class, () -> cc.getServerVersion()); + ServerApiException thrown = assertThrows(ServerApiException.class, cc::getServerVersion); long after = System.currentTimeMillis(); assertNotNull(thrown); assertEquals(ApiErrorCode.RESOURCE_UNAVAILABLE_ERROR, thrown.getErrorCode()); - assertTrue((after - before) >= 1000); // should timeout after 1 second. + assertTrue((after - before) >= 1000); // should time out after 1 second. } } + @Test + public void testValidateS3UrlNull() { + CloudRuntimeException thrown = assertThrows(CloudRuntimeException.class, () -> CloudianHyperStoreUtil.validateS3Url(null)); + assertNotNull(thrown); + assertTrue(thrown.getMessage().contains("must not be blank")); + } + @Test public void testValidateS3UrlGood() { // Mock an AWS S3 invalid access key response. - StringBuilder ERR_XML = new StringBuilder("\n"); - ERR_XML.append("\n"); - ERR_XML.append(" InvalidAccessKeyId\n"); - ERR_XML.append(" The AWS Access Key Id you provided does not exist in our records.\n"); - ERR_XML.append(" unknown\n"); - ERR_XML.append(" 12345=\n"); - ERR_XML.append("\n"); + String ERR_XML = "\n" + "\n" + + " InvalidAccessKeyId\n" + + " The AWS Access Key Id you provided does not exist in our records.\n" + + " unknown\n" + + " 12345=\n" + + "\n"; wireMockRule.stubFor(get(urlEqualTo("/")) .willReturn(aResponse() .withStatus(403) .withHeader("content-type", "application/xml") - .withBody(ERR_XML.toString()))); + .withBody(ERR_XML))); // Test: validates the AmazonS3 client returned by CloudianHyperStoreUtil.getS3Client() // which is called indirectly via the validateS3Url() method can connect to the @@ -161,24 +167,30 @@ public void testValidateS3UrlBadRequest() { assertNotNull(thrown); } + @Test + public void testValidateIAMUrlNull() { + CloudRuntimeException thrown = assertThrows(CloudRuntimeException.class, () -> CloudianHyperStoreUtil.validateIAMUrl(null)); + assertNotNull(thrown); + assertTrue(thrown.getMessage().contains("must not be blank")); + } + @Test public void testValidateIAMUrlGoodInvalidClientTokenId() { // Mock an AWS IAM invalid access key response. - StringBuilder ERR_XML = new StringBuilder(); - ERR_XML.append("\n"); - ERR_XML.append(" \n"); - ERR_XML.append(" Sender\n"); - ERR_XML.append(" InvalidClientTokenId\n"); - ERR_XML.append(" The security token included in the request is invalid.\n"); - ERR_XML.append(" \n"); - ERR_XML.append(" a2c47f7e-0196-4b45-af18-a9e99e4d9ed5\n"); - ERR_XML.append("\n"); + String ERR_XML = "\n" + + " \n" + + " Sender\n" + + " InvalidClientTokenId\n" + + " The security token included in the request is invalid.\n" + + " \n" + + " a2c47f7e-0196-4b45-af18-a9e99e4d9ed5\n" + + "\n"; wireMockRule.stubFor(post(urlEqualTo("/")) .willReturn(aResponse() .withStatus(403) .withHeader("content-type", "text/xml") - .withBody(ERR_XML.toString()))); + .withBody(ERR_XML))); // Test: validates the AmazonIdentityManagement client returned by CloudianHyperStoreUtil.getIAMClient() // which is called indirectly via the validateIAMUrl() method can connect to the @@ -190,20 +202,20 @@ public void testValidateIAMUrlGoodInvalidClientTokenId() { @Test public void testValidateIAMUrlGoodInvalidAccessKeyId() { // Mock HyperStore IAM invalid access key current response. - StringBuilder ERR_XML = new StringBuilder("\n"); - ERR_XML.append("\n"); - ERR_XML.append(" \n"); - ERR_XML.append(" InvalidAccessKeyId\n"); - ERR_XML.append(" The Access Key Id you provided does not exist in our records.\n"); - ERR_XML.append(" \n"); - ERR_XML.append(" a2c47f7e-0196-4b45-af18-a9e99e4d9ed5\n"); - ERR_XML.append("\n"); + String ERR_XML = "\n" + + "\n" + + " \n" + + " InvalidAccessKeyId\n" + + " The Access Key Id you provided does not exist in our records.\n" + + " \n" + + " a2c47f7e-0196-4b45-af18-a9e99e4d9ed5\n" + + "\n"; wireMockRule.stubFor(post(urlEqualTo("/")) .willReturn(aResponse() .withStatus(403) .withHeader("content-type", "application/xml;charset=UTF-8") - .withBody(ERR_XML.toString()))); + .withBody(ERR_XML))); // Test: validates the AmazonIdentityManagement client returned by CloudianHyperStoreUtil.getIAMClient() // which is called indirectly via the validateIAMUrl() method can connect to the diff --git a/plugins/storage/object/minio/src/main/java/org/apache/cloudstack/storage/datastore/driver/MinIOObjectStoreDriverImpl.java b/plugins/storage/object/minio/src/main/java/org/apache/cloudstack/storage/datastore/driver/MinIOObjectStoreDriverImpl.java index 9dc4b30414e6..0a71f79529b1 100644 --- a/plugins/storage/object/minio/src/main/java/org/apache/cloudstack/storage/datastore/driver/MinIOObjectStoreDriverImpl.java +++ b/plugins/storage/object/minio/src/main/java/org/apache/cloudstack/storage/datastore/driver/MinIOObjectStoreDriverImpl.java @@ -180,6 +180,12 @@ public List listBuckets(long storeId) { return bucketsList; } + @Override + public void verifyServiceConnectivity(long storeId) { + // ideally ping the service. For now, listBuckets() will do. + listBuckets(storeId); + } + @Override public boolean deleteBucket(BucketTO bucket, long storeId) { String bucketName = bucket.getName(); diff --git a/plugins/storage/object/simulator/src/main/java/org/apache/cloudstack/storage/datastore/driver/SimulatorObjectStoreDriverImpl.java b/plugins/storage/object/simulator/src/main/java/org/apache/cloudstack/storage/datastore/driver/SimulatorObjectStoreDriverImpl.java index 7b9ac59d5b1e..c9f027025098 100644 --- a/plugins/storage/object/simulator/src/main/java/org/apache/cloudstack/storage/datastore/driver/SimulatorObjectStoreDriverImpl.java +++ b/plugins/storage/object/simulator/src/main/java/org/apache/cloudstack/storage/datastore/driver/SimulatorObjectStoreDriverImpl.java @@ -71,6 +71,10 @@ public List listBuckets(long storeId) { return bucketsList; } + @Override + public void verifyServiceConnectivity(long storeId) { + } + @Override public boolean deleteBucket(BucketTO bucket, long storeId) { return true; diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index d601213329f0..850c09fca569 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -5585,6 +5585,12 @@ public List listResourceDetails(ListResourceDetailsCmd c String value = cmd.getValue(); Long resourceId = null; + // Reject requests for Object Store Details to non-RootAdmins + Account account = CallContext.current().getCallingAccount(); + if (! accountMgr.isRootAdmin(account.getAccountId()) && resourceType == ResourceTag.ResourceObjectType.ObjectStore) { + throw new PermissionDeniedException("The " + account + " is not authorized to list object store details."); + } + //Validation - 1.1 - resourceId and value can't be null. if (resourceIdStr == null && value == null) { throw new InvalidParameterValueException("Insufficient parameters passed for listing by resourceId OR key,value pair. Please check your params and try again."); diff --git a/server/src/main/java/com/cloud/metadata/ResourceMetaDataManagerImpl.java b/server/src/main/java/com/cloud/metadata/ResourceMetaDataManagerImpl.java index fc2453286cbb..e728d0defbec 100644 --- a/server/src/main/java/com/cloud/metadata/ResourceMetaDataManagerImpl.java +++ b/server/src/main/java/com/cloud/metadata/ResourceMetaDataManagerImpl.java @@ -45,6 +45,7 @@ import org.apache.cloudstack.resourcedetail.dao.LBStickinessPolicyDetailsDao; import org.apache.cloudstack.resourcedetail.dao.LBHealthCheckPolicyDetailsDao; import org.apache.cloudstack.resourcedetail.dao.GuestOsDetailsDao; +import org.apache.cloudstack.storage.datastore.db.ObjectStoreDetailsDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.springframework.stereotype.Component; @@ -127,6 +128,8 @@ public class ResourceMetaDataManagerImpl extends ManagerBase implements Resource @Inject NetworkOfferingDetailsDao _networkOfferingDetailsDao; @Inject + ObjectStoreDetailsDao _objectStoreDetailsDao; + @Inject ResourceManagerUtil resourceManagerUtil; private static Map> s_daoMap = new HashMap>(); @@ -162,6 +165,7 @@ public boolean configure(String name, Map params) throws Configu s_daoMap.put(ResourceObjectType.SnapshotPolicy, _snapshotPolicyDetailsDao); s_daoMap.put(ResourceObjectType.GuestOs, _guestOsDetailsDao); s_daoMap.put(ResourceObjectType.NetworkOffering, _networkOfferingDetailsDao); + s_daoMap.put(ResourceObjectType.ObjectStore, _objectStoreDetailsDao); return true; } diff --git a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java index 5de7ade696ac..11d3d4274f9a 100644 --- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java @@ -4717,34 +4717,72 @@ public ObjectStore updateObjectStore(Long id, UpdateObjectStoragePoolCmd cmd) { if (objectStoreVO == null) { throw new IllegalArgumentException("Unable to find object store with ID: " + id); } + if (! StringUtils.equals(objectStoreVO.getProviderName(), cmd.getProviderName())) { + String msg = String.format("Unexpected providerName %s does not match store's providerName %s", cmd.getProviderName(), objectStoreVO.getProviderName()); + logger.error(msg); + throw new IllegalArgumentException(msg); + } + + boolean updated = false; + boolean detailsUpdated = false; + + String newName = cmd.getName(); + String oldName = objectStoreVO.getName(); + if (newName != null && !newName.equals(oldName)) { + logger.debug("name {} => {}", oldName, newName); + objectStoreVO.setName(newName); + updated = true; + } - if(cmd.getUrl() != null ) { - String url = cmd.getUrl(); + String newUrl = cmd.getUrl(); + String oldUrl = objectStoreVO.getUrl(); + if (newUrl != null && !newUrl.equals(oldUrl)) { try { - // Check URL - UriUtils.validateUrl(url); - } catch (final Exception e) { - throw new InvalidParameterValueException(url + " is not a valid URL"); - } - ObjectStoreEntity objectStore = (ObjectStoreEntity)_dataStoreMgr.getDataStore(objectStoreVO.getId(), DataStoreRole.Object); - String oldUrl = objectStoreVO.getUrl(); - objectStoreVO.setUrl(url); + UriUtils.validateUrl(newUrl); + logger.debug("url {} => {}", oldUrl, newUrl); + objectStoreVO.setUrl(newUrl); + updated = true; + } catch (final IllegalArgumentException e) { + throw new InvalidParameterValueException(newUrl + " is not a valid URL"); + } + } + + if (updated) { + // any update to the store, save now _objectStoreDao.update(id, objectStoreVO); - //Update URL and check access - try { - objectStore.listBuckets(); - } catch (Exception e) { - //Revert to old URL on failure + } + + // If any details are given and if there is any difference, then + // we update with all the new details. + Map oldDetails = _objectStoreDetailsDao.getDetails(id); + Map newDetails = cmd.getDetails(); + if (newDetails != null && !oldDetails.equals(newDetails)) { + logger.debug("details {} => {}", oldDetails, newDetails); + _objectStoreDetailsDao.update(id, newDetails); + detailsUpdated = true; + } + + // Any updates have been saved. Try validate the connection. + // We test the connection even if there were no updates as that's also useful. + try { + ObjectStoreEntity objectStore = (ObjectStoreEntity)_dataStoreMgr.getDataStore(id, DataStoreRole.Object); + objectStore.verifyServiceConnectivity(); + } catch (Exception e) { + String msg = String.format("Unable to access Object Storage with URL: %s", cmd.getUrl()); + logger.warn(msg, e); + + // revert any updates made. + if (updated) { + objectStoreVO.setName(oldName); objectStoreVO.setUrl(oldUrl); _objectStoreDao.update(id, objectStoreVO); - throw new IllegalArgumentException("Unable to access Object Storage with URL: " + cmd.getUrl()); } + if (detailsUpdated) { + _objectStoreDetailsDao.update(id, oldDetails); + } + throw new IllegalArgumentException(msg); } - if(cmd.getName() != null ) { - objectStoreVO.setName(cmd.getName()); - } - _objectStoreDao.update(id, objectStoreVO); logger.debug("Successfully updated object store: {}", objectStoreVO); return objectStoreVO; } diff --git a/server/src/main/java/com/cloud/tags/ResourceManagerUtilImpl.java b/server/src/main/java/com/cloud/tags/ResourceManagerUtilImpl.java index e6d0b737bbe9..12e98c176c08 100644 --- a/server/src/main/java/com/cloud/tags/ResourceManagerUtilImpl.java +++ b/server/src/main/java/com/cloud/tags/ResourceManagerUtilImpl.java @@ -25,6 +25,7 @@ import org.apache.cloudstack.api.Identity; import org.apache.cloudstack.api.InternalIdentity; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.storage.datastore.db.ObjectStoreVO; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.commons.lang3.StringUtils; @@ -118,6 +119,7 @@ public class ResourceManagerUtilImpl implements ResourceManagerUtil { s_typeMap.put(ResourceTag.ResourceObjectType.NetworkOffering, NetworkOfferingVO.class); s_typeMap.put(ResourceTag.ResourceObjectType.VpcOffering, VpcOfferingVO.class); s_typeMap.put(ResourceTag.ResourceObjectType.Domain, DomainVO.class); + s_typeMap.put(ResourceTag.ResourceObjectType.ObjectStore, ObjectStoreVO.class); s_typeMap.put(ResourceTag.ResourceObjectType.GuestOsCategory, GuestOsCategory.class); } diff --git a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java index fdc277404f60..097656f92c5c 100644 --- a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java +++ b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java @@ -39,7 +39,10 @@ import com.cloud.network.Network; import com.cloud.network.VNF; import com.cloud.network.dao.NetworkVO; +import com.cloud.server.ResourceManagerUtil; +import com.cloud.server.ResourceMetaDataService; import com.cloud.server.ResourceTag; +import com.cloud.server.ResourceTag.ResourceObjectType; import com.cloud.storage.BucketVO; import com.cloud.storage.VMTemplateVO; import com.cloud.storage.ScopeType; @@ -70,6 +73,7 @@ import org.apache.cloudstack.api.command.user.bucket.ListBucketsCmd; import org.apache.cloudstack.api.command.user.event.ListEventsCmd; import org.apache.cloudstack.api.command.user.resource.ListDetailOptionsCmd; +import org.apache.cloudstack.api.command.user.volume.ListResourceDetailsCmd; import org.apache.cloudstack.api.response.DetailOptionsResponse; import org.apache.cloudstack.api.response.EventResponse; import org.apache.cloudstack.api.response.ListResponse; @@ -105,6 +109,7 @@ import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) @@ -137,6 +142,12 @@ public class QueryManagerImplTest { @Mock SearchCriteria searchCriteriaMock; + @Mock + ResourceManagerUtil resourceManagerUtil; + + @Mock + ResourceMetaDataService _resourceMetaDataMgr; + @Mock ObjectStoreDao objectStoreDao; @@ -407,6 +418,32 @@ public void testSearchForBuckets() { queryManagerImplSpy.searchForBuckets(listBucketsCmd); } + @Test + public void testListResourceDetailsObjectStoreNonRootAdminBadParam() { + ListResourceDetailsCmd cmd = mock(ListResourceDetailsCmd.class); + when(cmd.getResourceType()).thenReturn(ResourceObjectType.ObjectStore); + + // Ensure we get a PermissionDenied exception if we are not RootAdmin. + // The setup currently sets isRootAdmin to false. Quick confirmation. + Assert.assertFalse(accountManager.isRootAdmin(ACCOUNT_ID)); + PermissionDeniedException thrown = Assert.assertThrows(PermissionDeniedException.class, () -> queryManagerImplSpy.listResourceDetails(cmd)); + Assert.assertNotNull(thrown); + Assert.assertTrue(thrown.getMessage().contains("is not authorized to list object store details")); + } + + @Test + public void testListResourceDetailsObjectStoreAsRootAdminBadParam() { + ListResourceDetailsCmd cmd = mock(ListResourceDetailsCmd.class); + when(cmd.getResourceType()).thenReturn(ResourceObjectType.ObjectStore); + when(accountManager.isRootAdmin(account.getId())).thenReturn(true); + + // This time, we are RootAdmin so we get passed the permission check but hit a bad parameter. + Assert.assertTrue(accountManager.isRootAdmin(ACCOUNT_ID)); + InvalidParameterValueException thrown = Assert.assertThrows(InvalidParameterValueException.class, () -> queryManagerImplSpy.listResourceDetails(cmd)); + Assert.assertNotNull(thrown); + Assert.assertTrue(thrown.getMessage().contains("Insufficient parameters")); + } + @Test public void testGetHostTagsFromTemplateForServiceOfferingsListingNoTemplateId() { Assert.assertTrue(CollectionUtils.isEmpty(queryManager.getHostTagsFromTemplateForServiceOfferingsListing(Mockito.mock(AccountVO.class), null))); diff --git a/server/src/test/java/com/cloud/storage/StorageManagerImplTest.java b/server/src/test/java/com/cloud/storage/StorageManagerImplTest.java index 01ab82c913dd..22774e67bcd7 100644 --- a/server/src/test/java/com/cloud/storage/StorageManagerImplTest.java +++ b/server/src/test/java/com/cloud/storage/StorageManagerImplTest.java @@ -31,6 +31,8 @@ import com.cloud.storage.dao.StoragePoolAndAccessGroupMapDao; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.admin.storage.ChangeStoragePoolScopeCmd; +import org.apache.cloudstack.api.command.admin.storage.UpdateObjectStoragePoolCmd; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.api.command.admin.storage.ConfigureStorageAccessCmd; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver; import org.apache.cloudstack.framework.config.ConfigDepot; @@ -38,10 +40,14 @@ import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao; import org.apache.cloudstack.storage.command.CheckDataStoreStoragePolicyComplainceCommand; +import org.apache.cloudstack.storage.datastore.db.ObjectStoreDao; +import org.apache.cloudstack.storage.datastore.db.ObjectStoreDetailsDao; +import org.apache.cloudstack.storage.datastore.db.ObjectStoreVO; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO; import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.object.ObjectStoreEntity; import org.apache.commons.collections.MapUtils; import org.junit.Assert; import org.junit.Test; @@ -118,6 +124,12 @@ public class StorageManagerImplTest { AccountManagerImpl accountMgr; @Mock StoragePoolDetailsDao storagePoolDetailsDao; + @Mock + DataStoreManager _dataStoreMgr; + @Mock + ObjectStoreDao _objectStoreDao; + @Mock + ObjectStoreDetailsDao _objectStoreDetailsDao; @Mock ClusterDao clusterDao; @@ -980,6 +992,261 @@ public void testGetStoragePoolIopsStats_UsedIopsNegative() { Assert.assertNull("Used IOPS should be null when usedIops <= 0", result.second()); } + @Test + public void testUpdateObjectStoreNotFound() { + long storeId = 1L; + UpdateObjectStoragePoolCmd updateCmd = Mockito.mock(UpdateObjectStoragePoolCmd.class); + Mockito.when(_objectStoreDao.findById(Mockito.anyLong())).thenReturn(null); + + IllegalArgumentException thrown = Assert.assertThrows(IllegalArgumentException.class, () -> storageManagerImpl.updateObjectStore(storeId, updateCmd)); + Assert.assertNotNull(thrown); + Assert.assertTrue(thrown.getMessage().contains("Unable to find object store")); + } + + @Test + public void testUpdateObjectStoreNotMe() { + long storeId = 1L; + UpdateObjectStoragePoolCmd updateCmd = Mockito.mock(UpdateObjectStoragePoolCmd.class); + ObjectStoreVO objectStoreVO = Mockito.mock(ObjectStoreVO.class); + + Mockito.when(objectStoreVO.getProviderName()).thenReturn("provider1"); + Mockito.when(updateCmd.getProviderName()).thenReturn("other provider"); + Mockito.when(_objectStoreDao.findById(Mockito.anyLong())).thenReturn(objectStoreVO); + + IllegalArgumentException thrown = Assert.assertThrows(IllegalArgumentException.class, () -> storageManagerImpl.updateObjectStore(storeId, updateCmd)); + Assert.assertNotNull(thrown); + Assert.assertTrue(thrown.getMessage().contains("Unexpected providerName")); + } + + @Test + public void testUpdateObjectStoreNothingChanged() { + UpdateObjectStoragePoolCmd updateCmd = Mockito.mock(UpdateObjectStoragePoolCmd.class); + ObjectStoreVO objectStoreVO = Mockito.mock(ObjectStoreVO.class); + + final String provider = "provider1"; + Mockito.when(objectStoreVO.getProviderName()).thenReturn(provider); + Mockito.when(updateCmd.getProviderName()).thenReturn(provider); + long storeId = 1L; + Mockito.when(_objectStoreDao.findById(Mockito.anyLong())).thenReturn(objectStoreVO); + + // Same Name - no change. + final String name = "store name"; + Mockito.when(updateCmd.getName()).thenReturn(name); + Mockito.when(objectStoreVO.getName()).thenReturn(name); + + // Same URL - no change. + final String url = "http://object"; + Mockito.when(updateCmd.getUrl()).thenReturn(url); + Mockito.when(objectStoreVO.getUrl()).thenReturn(url); + + // 2 detail maps but same contents (as nothing updated) + Map mapOld = new HashMap<>(); + Map mapNew = new HashMap<>(); + mapOld.put("key1", "value1"); + mapNew.put("key1", "value1"); + Mockito.when(updateCmd.getDetails()).thenReturn(mapNew); + Mockito.when(_objectStoreDetailsDao.getDetails(Mockito.anyLong())).thenReturn(mapOld); + + ObjectStoreEntity objectStoreEntity = Mockito.mock(ObjectStoreEntity.class); + Mockito.when(_dataStoreMgr.getDataStore(storeId, DataStoreRole.Object)).thenReturn(objectStoreEntity); + + // Test the update. + Assert.assertNotNull(storageManagerImpl.updateObjectStore(storeId, updateCmd)); + + // The object store should never get an update call as no change. + Mockito.verify(_objectStoreDao, Mockito.never()).update(Mockito.anyLong(), Mockito.any()); + Mockito.verify(_objectStoreDetailsDao, Mockito.never()).update(Mockito.anyLong(), Mockito.anyMap()); + // The Object Store connectivity should be confirmed + Mockito.verify(objectStoreEntity, Mockito.atLeastOnce()).verifyServiceConnectivity(); + } + + @Test + public void testUpdateObjectStoreNameChanged() { + UpdateObjectStoragePoolCmd updateCmd = Mockito.mock(UpdateObjectStoragePoolCmd.class); + ObjectStoreVO objectStoreVO = Mockito.mock(ObjectStoreVO.class); + + final String provider = "provider1"; + Mockito.when(objectStoreVO.getProviderName()).thenReturn(provider); + Mockito.when(updateCmd.getProviderName()).thenReturn(provider); + long storeId = 1L; + Mockito.when(_objectStoreDao.findById(Mockito.anyLong())).thenReturn(objectStoreVO); + + // Name Changed + final String nameOld = "name old"; + final String nameNew = "name new"; + Mockito.when(updateCmd.getName()).thenReturn(nameNew); + Mockito.when(objectStoreVO.getName()).thenReturn(nameOld); + + // Same URL - no change. + final String url = "http://object"; + Mockito.when(updateCmd.getUrl()).thenReturn(url); + Mockito.when(objectStoreVO.getUrl()).thenReturn(url); + + // 2 detail maps but same contents (as nothing updated) + Map mapOld = new HashMap<>(); + Map mapNew = new HashMap<>(); + mapOld.put("key1", "value1"); + mapNew.put("key1", "value1"); + Mockito.when(updateCmd.getDetails()).thenReturn(mapNew); + Mockito.when(_objectStoreDetailsDao.getDetails(Mockito.anyLong())).thenReturn(mapOld); + + ObjectStoreEntity objectStoreEntity = Mockito.mock(ObjectStoreEntity.class); + Mockito.when(_dataStoreMgr.getDataStore(storeId, DataStoreRole.Object)).thenReturn(objectStoreEntity); + + // Test the update. + Assert.assertNotNull(storageManagerImpl.updateObjectStore(storeId, updateCmd)); + + // Only the name should be updated in the VO + Mockito.verify(objectStoreVO, Mockito.times(1)).setName(nameNew); + Mockito.verify(objectStoreVO, Mockito.never()).setUrl(Mockito.anyString()); + // Only the Store should be updated in the Dao, not the details. + Mockito.verify(_objectStoreDao, Mockito.times(1)).update(Mockito.anyLong(), Mockito.any()); + Mockito.verify(_objectStoreDetailsDao, Mockito.never()).update(Mockito.anyLong(), Mockito.anyMap()); + // The Object Store connectivity should be confirmed + Mockito.verify(objectStoreEntity, Mockito.atLeastOnce()).verifyServiceConnectivity(); + } + + @Test + public void testUpdateObjectStoreUrlChanged() { + UpdateObjectStoragePoolCmd updateCmd = Mockito.mock(UpdateObjectStoragePoolCmd.class); + ObjectStoreVO objectStoreVO = Mockito.mock(ObjectStoreVO.class); + + final String provider = "provider1"; + Mockito.when(objectStoreVO.getProviderName()).thenReturn(provider); + Mockito.when(updateCmd.getProviderName()).thenReturn(provider); + long storeId = 1L; + Mockito.when(_objectStoreDao.findById(Mockito.anyLong())).thenReturn(objectStoreVO); + + // Name unchanged + final String name = "name"; + Mockito.when(updateCmd.getName()).thenReturn(name); + Mockito.when(objectStoreVO.getName()).thenReturn(name); + + // URL changed. + final String urlOld = "http://object"; + final String urlNew = "http://10.10.10.10"; + Mockito.when(updateCmd.getUrl()).thenReturn(urlNew); + Mockito.when(objectStoreVO.getUrl()).thenReturn(urlOld); + + // 2 detail maps but same contents (as nothing updated) + Map mapOld = new HashMap<>(); + Map mapNew = new HashMap<>(); + mapOld.put("key1", "value1"); + mapNew.put("key1", "value1"); + Mockito.when(updateCmd.getDetails()).thenReturn(mapNew); + Mockito.when(_objectStoreDetailsDao.getDetails(Mockito.anyLong())).thenReturn(mapOld); + + ObjectStoreEntity objectStoreEntity = Mockito.mock(ObjectStoreEntity.class); + Mockito.when(_dataStoreMgr.getDataStore(storeId, DataStoreRole.Object)).thenReturn(objectStoreEntity); + + // Test the update. + Assert.assertNotNull(storageManagerImpl.updateObjectStore(storeId, updateCmd)); + + // Only the url should be updated in the VO + Mockito.verify(objectStoreVO, Mockito.never()).setName(Mockito.anyString()); + Mockito.verify(objectStoreVO, Mockito.times(1)).setUrl(urlNew); + // Only the Store should be updated in the Dao, not the details. + Mockito.verify(_objectStoreDao, Mockito.times(1)).update(Mockito.anyLong(), Mockito.any()); + Mockito.verify(_objectStoreDetailsDao, Mockito.never()).update(Mockito.anyLong(), Mockito.anyMap()); + // The Object Store connectivity should be confirmed + Mockito.verify(objectStoreEntity, Mockito.atLeastOnce()).verifyServiceConnectivity(); + } + + @Test + public void testUpdateObjectStoreDetailsChanged() { + UpdateObjectStoragePoolCmd updateCmd = Mockito.mock(UpdateObjectStoragePoolCmd.class); + ObjectStoreVO objectStoreVO = Mockito.mock(ObjectStoreVO.class); + + final String provider = "provider1"; + Mockito.when(objectStoreVO.getProviderName()).thenReturn(provider); + Mockito.when(updateCmd.getProviderName()).thenReturn(provider); + long storeId = 1L; + Mockito.when(_objectStoreDao.findById(Mockito.anyLong())).thenReturn(objectStoreVO); + + // Name unchanged + final String name = "name"; + Mockito.when(updateCmd.getName()).thenReturn(name); + Mockito.when(objectStoreVO.getName()).thenReturn(name); + + // Same URL - no change. + final String url = "http://object"; + Mockito.when(updateCmd.getUrl()).thenReturn(url); + Mockito.when(objectStoreVO.getUrl()).thenReturn(url); + + // details changed + Map mapOld = new HashMap<>(); + Map mapNew = new HashMap<>(); + mapOld.put("key1", "oldValue"); + mapNew.put("key1", "newValue"); + Mockito.when(updateCmd.getDetails()).thenReturn(mapNew); + Mockito.when(_objectStoreDetailsDao.getDetails(Mockito.anyLong())).thenReturn(mapOld); + + ObjectStoreEntity objectStoreEntity = Mockito.mock(ObjectStoreEntity.class); + Mockito.when(_dataStoreMgr.getDataStore(storeId, DataStoreRole.Object)).thenReturn(objectStoreEntity); + + // Test the update. + Assert.assertNotNull(storageManagerImpl.updateObjectStore(storeId, updateCmd)); + + // Neither Url nor Name updated + Mockito.verify(objectStoreVO, Mockito.never()).setName(Mockito.anyString()); + Mockito.verify(objectStoreVO, Mockito.never()).setUrl(Mockito.anyString()); + // Store not updated, but details should be with the new map. + Mockito.verify(_objectStoreDao, Mockito.never()).update(Mockito.anyLong(), Mockito.any()); + Mockito.verify(_objectStoreDetailsDao, Mockito.times(1)).update(storeId, mapNew); + // The Object Store connectivity should be confirmed + Mockito.verify(objectStoreEntity, Mockito.atLeastOnce()).verifyServiceConnectivity(); + } + + @Test + public void testUpdateObjectStoreRollback() { + UpdateObjectStoragePoolCmd updateCmd = Mockito.mock(UpdateObjectStoragePoolCmd.class); + ObjectStoreVO objectStoreVO = Mockito.mock(ObjectStoreVO.class); + + final String provider = "provider1"; + Mockito.when(objectStoreVO.getProviderName()).thenReturn(provider); + Mockito.when(updateCmd.getProviderName()).thenReturn(provider); + long storeId = 1L; + Mockito.when(_objectStoreDao.findById(Mockito.anyLong())).thenReturn(objectStoreVO); + + // Name Changed + final String nameOld = "name old"; + final String nameNew = "name new"; + Mockito.when(updateCmd.getName()).thenReturn(nameNew); + Mockito.when(objectStoreVO.getName()).thenReturn(nameOld); + + // Same URL - no change. + final String url = "http://object"; + Mockito.when(updateCmd.getUrl()).thenReturn(url); + Mockito.when(objectStoreVO.getUrl()).thenReturn(url); + + // Details updated + Map mapOld = new HashMap<>(); + Map mapNew = new HashMap<>(); + mapOld.put("key1", "oldValue"); + mapNew.put("key1", "newValue"); + Mockito.when(updateCmd.getDetails()).thenReturn(mapNew); + Mockito.when(_objectStoreDetailsDao.getDetails(Mockito.anyLong())).thenReturn(mapOld); + + ObjectStoreEntity objectStoreEntity = Mockito.mock(ObjectStoreEntity.class); + Mockito.when(_dataStoreMgr.getDataStore(storeId, DataStoreRole.Object)).thenReturn(objectStoreEntity); + // Simulate failure in connectivity to trigger rollback + Mockito.doThrow(new CloudRuntimeException("connectivity failure")).when(objectStoreEntity).verifyServiceConnectivity(); + + // Test the update which should fail + IllegalArgumentException thrown = Assert.assertThrows(IllegalArgumentException.class, () -> storageManagerImpl.updateObjectStore(storeId, updateCmd)); + Assert.assertNotNull(thrown); + Assert.assertTrue(thrown.getMessage().contains("Unable to access Object Storage")); + + // Name updated with nameNew, then nameOld on rollback. + // url updated only on rollback (as name was update). + Mockito.verify(objectStoreVO, Mockito.times(2)).setName(Mockito.anyString()); + Mockito.verify(objectStoreVO, Mockito.times(1)).setUrl(url); + // Store updated and then updated again on rollback. + Mockito.verify(_objectStoreDao, Mockito.times(2)).update(Mockito.anyLong(), Mockito.any()); + // details updated and then update again on rollback + Mockito.verify(_objectStoreDetailsDao, Mockito.times(1)).update(storeId, mapNew); + Mockito.verify(_objectStoreDetailsDao, Mockito.times(1)).update(storeId, mapOld); + } @Test public void testNoStorageAccessGroupsOnHostAndStoragePool() { diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index a85ccc97cf0b..2a9fb0fcebc1 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -2433,6 +2433,7 @@ "label.update.ip.range": "Update IP range", "label.update.ipv4.subnet": "Update IPv4 subnet", "label.update.network": "Update Network", +"label.update.object.storage": "Update Object Storage", "label.update.physical.network": "Update physical Network", "label.update.project.role": "Update project role", "label.update.ssl": " SSL certificate", @@ -3653,6 +3654,7 @@ "message.success.update.ipv4.subnet": "Successfully updated IPv4 subnet", "message.success.update.kubeversion": "Successfully updated Kubernetes supported version", "message.success.update.network": "Successfully updated Network", +"message.success.update.object.storage": "Successfully updated Object Storage", "message.success.update.template": "Successfully updated Template", "message.success.update.user": "Successfully updated User", "message.success.upgrade.kubernetes": "Successfully upgraded Kubernetes cluster", diff --git a/ui/src/config/section/infra/objectStorages.js b/ui/src/config/section/infra/objectStorages.js index 821c1b2d9484..00634d1f1813 100644 --- a/ui/src/config/section/infra/objectStorages.js +++ b/ui/src/config/section/infra/objectStorages.js @@ -59,8 +59,9 @@ export default { api: 'updateObjectStoragePool', icon: 'edit-outlined', label: 'label.action.update.object.storage', - args: ['name', 'url'], - dataView: true + dataView: true, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/infra/UpdateObjectStorage.vue'))) }, { api: 'deleteObjectStoragePool', diff --git a/ui/src/views/infra/UpdateObjectStorage.vue b/ui/src/views/infra/UpdateObjectStorage.vue new file mode 100644 index 000000000000..8536bef6f22e --- /dev/null +++ b/ui/src/views/infra/UpdateObjectStorage.vue @@ -0,0 +1,210 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + + + +