Skip to content

Commit b1d5ecd

Browse files
MHKMHK
authored andcommitted
Update versioning logic, adress the reviwer comment for the UI
1 parent 7c86383 commit b1d5ecd

File tree

8 files changed

+237
-331
lines changed

8 files changed

+237
-331
lines changed

plugins/storage/object/ECS/src/main/java/org/apache/cloudstack/storage/datastore/driver/EcsConstants.java

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,15 @@
1-
/*
2-
* Licensed to the Apache Software Foundation (ASF) under one
3-
* or more contributor license agreements. See the NOTICE file
4-
* distributed with this work for additional information
5-
* regarding copyright ownership. The ASF licenses this file
6-
* to you under the Apache License, Version 2.0 (the
7-
* "License"); you may not use this file except in compliance
8-
* with the License. You may obtain a copy of the License at
9-
*
10-
* http://www.apache.org/licenses/LICENSE-2.0
11-
*
12-
* Unless required by applicable law or agreed to in writing,
13-
* software distributed under the License is distributed on an
14-
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15-
* KIND, either express or implied. See the License for the
16-
* specific language governing permissions and limitations
17-
* under the License.
18-
*/
191
package org.apache.cloudstack.storage.datastore.driver;
202

213
public final class EcsConstants {
22-
private EcsConstants() {
23-
}
4+
private EcsConstants() {}
245

256
// Object store details keys
26-
public static final String MGMT_URL = "mgmt_url";
27-
public static final String SA_USER = "sa_user";
28-
public static final String SA_PASS = "sa_password";
7+
public static final String MGMT_URL = "mgmt_url";
8+
public static final String SA_USER = "sa_user";
9+
public static final String SA_PASS = "sa_password";
2910
public static final String NAMESPACE = "namespace";
30-
public static final String INSECURE = "insecure";
31-
public static final String S3_HOST = "s3_host";
11+
public static final String INSECURE = "insecure";
12+
public static final String S3_HOST = "s3_host";
3213
public static final String USER_PREFIX = "user_prefix";
3314
public static final String DEFAULT_USER_PREFIX = "cs-";
3415

plugins/storage/object/ECS/src/main/java/org/apache/cloudstack/storage/datastore/driver/EcsMgmtTokenManager.java

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,3 @@
1-
/*
2-
* Licensed to the Apache Software Foundation (ASF) under one
3-
* or more contributor license agreements. See the NOTICE file
4-
* distributed with this work for additional information
5-
* regarding copyright ownership. The ASF licenses this file
6-
* to you under the Apache License, Version 2.0 (the
7-
* "License"); you may not use this file except in compliance
8-
* with the License. You may obtain a copy of the License at
9-
*
10-
* http://www.apache.org/licenses/LICENSE-2.0
11-
*
12-
* Unless required by applicable law or agreed to in writing,
13-
* software distributed under the License is distributed on an
14-
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15-
* KIND, either express or implied. See the License for the
16-
* specific language governing permissions and limitations
17-
* under the License.
18-
*/
191
package org.apache.cloudstack.storage.datastore.driver;
202

213
import java.nio.charset.StandardCharsets;

plugins/storage/object/ECS/src/main/java/org/apache/cloudstack/storage/datastore/driver/EcsObjectStoreDriverImpl.java

Lines changed: 79 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import java.util.List;
2424
import java.util.Locale;
2525
import java.util.Map;
26-
2726
import javax.inject.Inject;
2827
import javax.net.ssl.SSLContext;
2928

@@ -65,16 +64,20 @@
6564
public class EcsObjectStoreDriverImpl extends BaseObjectStoreDriverImpl {
6665

6766
// ---- Injected dependencies ----
68-
@Inject private AccountDao accountDao;
69-
@Inject private AccountDetailsDao accountDetailsDao;
70-
@Inject private BucketDao bucketDao;
71-
@Inject private ObjectStoreDetailsDao storeDetailsDao;
67+
@Inject
68+
private AccountDao accountDao;
69+
@Inject
70+
private AccountDetailsDao accountDetailsDao;
71+
@Inject
72+
private BucketDao bucketDao;
73+
@Inject
74+
private ObjectStoreDetailsDao storeDetailsDao;
7275

7376
private final EcsMgmtTokenManager tokenManager = new EcsMgmtTokenManager();
7477
private final EcsXmlParser xml = new EcsXmlParser();
7578

7679
// Versioning retry (ECS can be eventually consistent)
77-
private static final int VERSIONING_MAX_TRIES = 45;
80+
private static final int VERSIONING_MAX_TRIES = 10;
7881
private static final long VERSIONING_RETRY_SLEEP_MS = 1000L;
7982

8083
public EcsObjectStoreDriverImpl() {
@@ -622,113 +625,79 @@ public boolean deleteBucketVersioning(final BucketTO bucket, final long storeId)
622625
return setOrSuspendVersioning(bucket, storeId, false);
623626
}
624627

625-
private boolean setOrSuspendVersioning(final BucketTO bucket, final long storeId, final boolean enable) {
628+
private boolean setOrSuspendVersioning(final BucketTO bucket,
629+
final long storeId,
630+
final boolean enable) {
626631
final Map<String, String> ds = storeDetailsDao.getDetails(storeId);
627632
final S3Endpoint ep = resolveS3Endpoint(ds, storeId);
628633
final boolean insecure = "true".equalsIgnoreCase(ds.getOrDefault(EcsConstants.INSECURE, "false"));
629634

630635
if (ep == null || StringUtils.isBlank(ep.host)) {
631-
logger.warn("ECS: {}BucketVersioning requested but S3 endpoint is not resolvable; skipping.",
632-
enable ? "set" : "delete");
633-
return true;
636+
logger.warn("ECS: S3 endpoint not resolvable; skipping bucket versioning.");
637+
return true; // best-effort
634638
}
635639

636640
final String bucketName = bucket.getName();
637641
final String desired = enable ? "Enabled" : "Suspended";
638642

639-
// First try: use calling account (normal API usage)
640-
final CallContext ctx = CallContext.current();
643+
// Resolve accountId
641644
long accountId = -1L;
645+
final CallContext ctx = CallContext.current();
642646
if (ctx != null && ctx.getCallingAccount() != null) {
643647
accountId = ctx.getCallingAccount().getId();
644648
}
645-
646-
// Fallback: bucket VO may contain accountId (depends on CloudStack version & call path)
647649
if (accountId <= 0) {
648-
final BucketVO vo = resolveBucketVO(bucket, storeId);
650+
final BucketVO vo = resolveBucketVO(bucket);
649651
if (vo != null) {
650-
try { accountId = vo.getAccountId(); } catch (Throwable ignore) { }
652+
accountId = vo.getAccountId();
651653
}
652654
}
653655

654-
// Fallback: reflection on BucketTO (if present in this branch)
655656
if (accountId <= 0) {
656-
accountId = getLongFromGetter(bucket, "getAccountId", -1L);
657+
logger.warn("ECS: cannot resolve accountId for bucket='{}'; skipping versioning.", bucketName);
658+
return true;
657659
}
658660

659-
// Fallback: query ECS mgmt API for owner -> account
660-
if (accountId <= 0) {
661-
final Long aid = resolveAccountIdViaMgmt(bucketName, ds, insecure);
662-
if (aid != null && aid > 0) {
663-
accountId = aid;
664-
}
665-
}
661+
String accessKey = valueOrNull(accountDetailsDao.findDetail(accountId, EcsConstants.AD_KEY_ACCESS));
662+
String secretKey = valueOrNull(accountDetailsDao.findDetail(accountId, EcsConstants.AD_KEY_SECRET));
666663

667-
if (accountId <= 0) {
668-
logger.warn("ECS: cannot resolve accountId for bucket='{}'; skipping versioning request.", bucketName);
664+
if (StringUtils.isBlank(accessKey) || StringUtils.isBlank(secretKey)) {
665+
logger.warn("ECS: missing S3 credentials for accountId={}; skipping versioning.", accountId);
669666
return true;
670667
}
671668

672-
for (int attempt = 1; attempt <= VERSIONING_MAX_TRIES; attempt++) {
673-
String accessKey = valueOrNull(accountDetailsDao.findDetail(accountId, EcsConstants.AD_KEY_ACCESS));
674-
String secretKey = valueOrNull(accountDetailsDao.findDetail(accountId, EcsConstants.AD_KEY_SECRET));
675-
676-
// If missing, try to provision now
677-
if (StringUtils.isBlank(accessKey) || StringUtils.isBlank(secretKey)) {
678-
try {
679-
final EcsCfg cfg = ecsCfgFromDetails(ds, storeId);
680-
final Account acct = accountDao.findById(accountId);
681-
if (acct != null) {
682-
final String ownerUser = getUserPrefix(ds) + acct.getUuid();
683-
ensureAccountUserAndSecret(accountId, ownerUser, cfg.mgmtUrl, cfg.saUser, cfg.saPass, cfg.ns, cfg.insecure);
684-
accessKey = valueOrNull(accountDetailsDao.findDetail(accountId, EcsConstants.AD_KEY_ACCESS));
685-
secretKey = valueOrNull(accountDetailsDao.findDetail(accountId, EcsConstants.AD_KEY_SECRET));
686-
}
687-
} catch (Exception e) {
688-
logger.debug("ECS: ensureAccountUserAndSecret failed during versioning (attempt {}): {}", attempt, e.getMessage());
689-
}
690-
}
691-
692-
if (!StringUtils.isBlank(accessKey) && !StringUtils.isBlank(secretKey)) {
693-
try (CloseableHttpClient http = buildHttpClient(insecure)) {
694-
setS3BucketVersioningWithVerify(http, ep.scheme, ep.host, bucketName, accessKey, secretKey, desired);
695-
logger.info("ECS: S3 versioning {} for bucket='{}' (accountId={}) succeeded on attempt {}/{}.",
696-
desired, bucketName, accountId, attempt, VERSIONING_MAX_TRIES);
697-
return true;
698-
} catch (Exception e) {
699-
logger.warn("ECS: versioning {} for '{}' failed on attempt {}/{}: {}",
700-
desired, bucketName, attempt, VERSIONING_MAX_TRIES, e.getMessage());
701-
}
702-
} else {
703-
logger.debug("ECS: missing S3 keys for accountId={} (attempt {}/{}).", accountId, attempt, VERSIONING_MAX_TRIES);
704-
}
705-
706-
if (attempt < VERSIONING_MAX_TRIES) {
707-
try {
708-
Thread.sleep(VERSIONING_RETRY_SLEEP_MS);
709-
} catch (InterruptedException ie) {
710-
Thread.currentThread().interrupt();
711-
return true;
712-
}
713-
}
669+
try (CloseableHttpClient http = buildHttpClient(insecure)) {
670+
putBucketVersioningSigV2(
671+
http,
672+
ep.scheme,
673+
ep.host,
674+
bucketName,
675+
accessKey,
676+
secretKey,
677+
desired
678+
);
679+
logger.info("ECS: bucket versioning {} succeeded for '{}'", desired, bucketName);
680+
return true;
681+
} catch (Exception e) {
682+
logger.warn("ECS: bucket versioning {} failed for '{}': {}",
683+
desired, bucketName, e.getMessage());
684+
return true; // best-effort (do NOT break createBucket)
714685
}
715-
716-
logger.warn("ECS: versioning {} for '{}' gave up after {} attempts; leaving as-is.",
717-
desired, bucketName, VERSIONING_MAX_TRIES);
718-
return true;
719686
}
720687

721-
// ----- S3 Versioning (SigV2 path-style) -----
688+
// ----- S3 Versioning (SigV2, EXACTLY matches bash script) -----
689+
690+
private void putBucketVersioningSigV2(final CloseableHttpClient http,
691+
final String scheme,
692+
final String host,
693+
final String bucketName,
694+
final String accessKey,
695+
final String secretKey,
696+
final String status) throws Exception {
722697

723-
private void setS3BucketVersioning(final CloseableHttpClient http,
724-
final String scheme,
725-
final String host,
726-
final String bucketName,
727-
final String accessKey,
728-
final String secretKey,
729-
final String status) throws Exception {
698+
// EXACT XML (no namespace, matches bash)
730699
final String body =
731-
"<VersioningConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">"
700+
"<VersioningConfiguration>"
732701
+ "<Status>" + status + "</Status>"
733702
+ "</VersioningConfiguration>";
734703

@@ -737,100 +706,41 @@ private void setS3BucketVersioning(final CloseableHttpClient http,
737706
final String contentMd5 = base64Md5(bodyBytes);
738707
final String dateHdr = rfc1123Now();
739708

740-
// IMPORTANT: include trailing slash before subresource
741-
final String canonicalResource = "/" + bucketName + "/?versioning";
742-
final String sts = "PUT\n" + contentMd5 + "\n" + contentType + "\n" + dateHdr + "\n" + canonicalResource;
743-
final String signature = hmacSha1Base64(sts, secretKey);
709+
// IMPORTANT: NO trailing slash before ?versioning
710+
final String canonicalResource = "/" + bucketName + "?versioning";
711+
712+
final String stringToSign =
713+
"PUT\n"
714+
+ contentMd5 + "\n"
715+
+ contentType + "\n"
716+
+ dateHdr + "\n"
717+
+ canonicalResource;
718+
719+
final String signature = hmacSha1Base64(stringToSign, secretKey);
720+
721+
final String url = scheme + "://" + host + "/" + bucketName + "?versioning";
744722

745-
final String url = scheme + "://" + host + "/" + bucketName + "/?versioning";
746723
final HttpPut put = new HttpPut(url);
747-
put.setHeader("Host", host);
748724
put.setHeader("Date", dateHdr);
749-
put.setHeader("Authorization", "AWS " + accessKey + ":" + signature);
750725
put.setHeader("Content-Type", contentType);
751726
put.setHeader("Content-MD5", contentMd5);
727+
put.setHeader("Authorization", "AWS " + accessKey + ":" + signature);
752728
put.setEntity(new StringEntity(body, StandardCharsets.UTF_8));
753729

754730
try (CloseableHttpResponse resp = http.execute(put)) {
755-
final int st = resp.getStatusLine().getStatusCode();
756-
final String rb = resp.getEntity() != null
731+
final int statusCode = resp.getStatusLine().getStatusCode();
732+
final String respBody = resp.getEntity() != null
757733
? EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8)
758734
: "";
759735

760-
if (st != 200 && st != 204) {
761-
throw new CloudRuntimeException("S3 versioning " + status + " failed: HTTP " + st + " body=" + rb);
736+
if (statusCode != 200 && statusCode != 204) {
737+
throw new CloudRuntimeException(
738+
"S3 versioning failed: HTTP " + statusCode + " body=" + respBody
739+
);
762740
}
763741
}
764742
}
765743

766-
private String getS3BucketVersioningStatus(final CloseableHttpClient http,
767-
final String scheme,
768-
final String host,
769-
final String bucketName,
770-
final String accessKey,
771-
final String secretKey) throws Exception {
772-
final String dateHdr = rfc1123Now();
773-
final String canonicalResource = "/" + bucketName + "/?versioning";
774-
final String sts = "GET\n\n\n" + dateHdr + "\n" + canonicalResource;
775-
final String signature = hmacSha1Base64(sts, secretKey);
776-
777-
final String url = scheme + "://" + host + "/" + bucketName + "/?versioning";
778-
final HttpGet get = new HttpGet(url);
779-
get.setHeader("Host", host);
780-
get.setHeader("Date", dateHdr);
781-
get.setHeader("Authorization", "AWS " + accessKey + ":" + signature);
782-
783-
try (CloseableHttpResponse resp = http.execute(get)) {
784-
final int st = resp.getStatusLine().getStatusCode();
785-
final String rb = resp.getEntity() != null
786-
? EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8)
787-
: "";
788-
789-
if (st != 200 && st != 204) {
790-
throw new CloudRuntimeException("S3 get versioning failed: HTTP " + st + " body=" + rb);
791-
}
792-
793-
final String status = xml.extractTag(rb, "Status");
794-
return status != null ? status.trim() : "";
795-
}
796-
}
797-
798-
private void setS3BucketVersioningWithVerify(final CloseableHttpClient http,
799-
final String scheme,
800-
final String host,
801-
final String bucketName,
802-
final String accessKey,
803-
final String secretKey,
804-
final String desired) throws Exception {
805-
setS3BucketVersioning(http, scheme, host, bucketName, accessKey, secretKey, desired);
806-
807-
// Verify (best-effort; ECS may be eventually consistent)
808-
for (int i = 1; i <= 10; i++) {
809-
try {
810-
final String got = getS3BucketVersioningStatus(http, scheme, host, bucketName, accessKey, secretKey);
811-
if (desired.equalsIgnoreCase(got)) {
812-
logger.info("ECS: versioning verify OK for '{}': {}", bucketName, got);
813-
return;
814-
}
815-
logger.warn("ECS: versioning verify mismatch for '{}': desired={} got={} (try {}/10)",
816-
bucketName, desired, got, i);
817-
} catch (Exception e) {
818-
logger.debug("ECS: versioning verify error for '{}': {} (try {}/10)",
819-
bucketName, e.getMessage(), i);
820-
}
821-
822-
try {
823-
Thread.sleep(500L);
824-
} catch (InterruptedException ie) {
825-
Thread.currentThread().interrupt();
826-
return;
827-
}
828-
}
829-
830-
logger.warn("ECS: versioning verify FAILED for '{}': desired={} (backend may be eventually consistent)",
831-
bucketName, desired);
832-
}
833-
834744
// ---------------- Quota ----------------
835745

836746
@Override
@@ -1355,15 +1265,15 @@ private static long getLongFromGetter(final Object o, final String getter, final
13551265
}
13561266

13571267
private BucketVO resolveBucketVO(final BucketTO bucket) {
1358-
if (bucket == null) {
1359-
return null;
1360-
}
1268+
if (bucket == null) {
1269+
return null;
1270+
}
13611271

1362-
final long id = getLongFromGetter(bucket, "getId", -1L);
1363-
if (id > 0) {
1364-
return bucketDao.findById(id);
1365-
}
1366-
return null;
1272+
final long id = getLongFromGetter(bucket, "getId", -1L);
1273+
if (id > 0) {
1274+
return bucketDao.findById(id);
1275+
}
1276+
return null;
13671277
}
13681278

13691279
private static String base64Md5(final byte[] data) throws Exception {

0 commit comments

Comments
 (0)