2323import java .util .List ;
2424import java .util .Locale ;
2525import java .util .Map ;
26-
2726import javax .inject .Inject ;
2827import javax .net .ssl .SSLContext ;
2928
6564public 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