Update versioning logic, adress the reviwer comment for the UI

This commit is contained in:
MHK 2026-01-04 12:47:47 +03:00
parent 7c86383f11
commit b1d5ecdfc0
8 changed files with 238 additions and 332 deletions

View File

@ -1,34 +1,15 @@
/*
* 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.
*/
package org.apache.cloudstack.storage.datastore.driver;
public final class EcsConstants {
private EcsConstants() {
}
private EcsConstants() {}
// Object store details keys
public static final String MGMT_URL = "mgmt_url";
public static final String SA_USER = "sa_user";
public static final String SA_PASS = "sa_password";
public static final String MGMT_URL = "mgmt_url";
public static final String SA_USER = "sa_user";
public static final String SA_PASS = "sa_password";
public static final String NAMESPACE = "namespace";
public static final String INSECURE = "insecure";
public static final String S3_HOST = "s3_host";
public static final String INSECURE = "insecure";
public static final String S3_HOST = "s3_host";
public static final String USER_PREFIX = "user_prefix";
public static final String DEFAULT_USER_PREFIX = "cs-";

View File

@ -1,21 +1,3 @@
/*
* 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.
*/
package org.apache.cloudstack.storage.datastore.driver;
import java.nio.charset.StandardCharsets;

View File

@ -23,7 +23,6 @@ import java.util.Base64;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.inject.Inject;
import javax.net.ssl.SSLContext;
@ -65,16 +64,20 @@ import com.cloud.utils.exception.CloudRuntimeException;
public class EcsObjectStoreDriverImpl extends BaseObjectStoreDriverImpl {
// ---- Injected dependencies ----
@Inject private AccountDao accountDao;
@Inject private AccountDetailsDao accountDetailsDao;
@Inject private BucketDao bucketDao;
@Inject private ObjectStoreDetailsDao storeDetailsDao;
@Inject
private AccountDao accountDao;
@Inject
private AccountDetailsDao accountDetailsDao;
@Inject
private BucketDao bucketDao;
@Inject
private ObjectStoreDetailsDao storeDetailsDao;
private final EcsMgmtTokenManager tokenManager = new EcsMgmtTokenManager();
private final EcsXmlParser xml = new EcsXmlParser();
// Versioning retry (ECS can be eventually consistent)
private static final int VERSIONING_MAX_TRIES = 45;
private static final int VERSIONING_MAX_TRIES = 10;
private static final long VERSIONING_RETRY_SLEEP_MS = 1000L;
public EcsObjectStoreDriverImpl() {
@ -622,113 +625,79 @@ public class EcsObjectStoreDriverImpl extends BaseObjectStoreDriverImpl {
return setOrSuspendVersioning(bucket, storeId, false);
}
private boolean setOrSuspendVersioning(final BucketTO bucket, final long storeId, final boolean enable) {
private boolean setOrSuspendVersioning(final BucketTO bucket,
final long storeId,
final boolean enable) {
final Map<String, String> ds = storeDetailsDao.getDetails(storeId);
final S3Endpoint ep = resolveS3Endpoint(ds, storeId);
final boolean insecure = "true".equalsIgnoreCase(ds.getOrDefault(EcsConstants.INSECURE, "false"));
if (ep == null || StringUtils.isBlank(ep.host)) {
logger.warn("ECS: {}BucketVersioning requested but S3 endpoint is not resolvable; skipping.",
enable ? "set" : "delete");
return true;
logger.warn("ECS: S3 endpoint not resolvable; skipping bucket versioning.");
return true; // best-effort
}
final String bucketName = bucket.getName();
final String desired = enable ? "Enabled" : "Suspended";
// First try: use calling account (normal API usage)
final CallContext ctx = CallContext.current();
// Resolve accountId
long accountId = -1L;
final CallContext ctx = CallContext.current();
if (ctx != null && ctx.getCallingAccount() != null) {
accountId = ctx.getCallingAccount().getId();
}
// Fallback: bucket VO may contain accountId (depends on CloudStack version & call path)
if (accountId <= 0) {
final BucketVO vo = resolveBucketVO(bucket, storeId);
final BucketVO vo = resolveBucketVO(bucket);
if (vo != null) {
try { accountId = vo.getAccountId(); } catch (Throwable ignore) { }
}
}
// Fallback: reflection on BucketTO (if present in this branch)
if (accountId <= 0) {
accountId = getLongFromGetter(bucket, "getAccountId", -1L);
}
// Fallback: query ECS mgmt API for owner -> account
if (accountId <= 0) {
final Long aid = resolveAccountIdViaMgmt(bucketName, ds, insecure);
if (aid != null && aid > 0) {
accountId = aid;
accountId = vo.getAccountId();
}
}
if (accountId <= 0) {
logger.warn("ECS: cannot resolve accountId for bucket='{}'; skipping versioning request.", bucketName);
logger.warn("ECS: cannot resolve accountId for bucket='{}'; skipping versioning.", bucketName);
return true;
}
for (int attempt = 1; attempt <= VERSIONING_MAX_TRIES; attempt++) {
String accessKey = valueOrNull(accountDetailsDao.findDetail(accountId, EcsConstants.AD_KEY_ACCESS));
String secretKey = valueOrNull(accountDetailsDao.findDetail(accountId, EcsConstants.AD_KEY_SECRET));
String accessKey = valueOrNull(accountDetailsDao.findDetail(accountId, EcsConstants.AD_KEY_ACCESS));
String secretKey = valueOrNull(accountDetailsDao.findDetail(accountId, EcsConstants.AD_KEY_SECRET));
// If missing, try to provision now
if (StringUtils.isBlank(accessKey) || StringUtils.isBlank(secretKey)) {
try {
final EcsCfg cfg = ecsCfgFromDetails(ds, storeId);
final Account acct = accountDao.findById(accountId);
if (acct != null) {
final String ownerUser = getUserPrefix(ds) + acct.getUuid();
ensureAccountUserAndSecret(accountId, ownerUser, cfg.mgmtUrl, cfg.saUser, cfg.saPass, cfg.ns, cfg.insecure);
accessKey = valueOrNull(accountDetailsDao.findDetail(accountId, EcsConstants.AD_KEY_ACCESS));
secretKey = valueOrNull(accountDetailsDao.findDetail(accountId, EcsConstants.AD_KEY_SECRET));
}
} catch (Exception e) {
logger.debug("ECS: ensureAccountUserAndSecret failed during versioning (attempt {}): {}", attempt, e.getMessage());
}
}
if (!StringUtils.isBlank(accessKey) && !StringUtils.isBlank(secretKey)) {
try (CloseableHttpClient http = buildHttpClient(insecure)) {
setS3BucketVersioningWithVerify(http, ep.scheme, ep.host, bucketName, accessKey, secretKey, desired);
logger.info("ECS: S3 versioning {} for bucket='{}' (accountId={}) succeeded on attempt {}/{}.",
desired, bucketName, accountId, attempt, VERSIONING_MAX_TRIES);
return true;
} catch (Exception e) {
logger.warn("ECS: versioning {} for '{}' failed on attempt {}/{}: {}",
desired, bucketName, attempt, VERSIONING_MAX_TRIES, e.getMessage());
}
} else {
logger.debug("ECS: missing S3 keys for accountId={} (attempt {}/{}).", accountId, attempt, VERSIONING_MAX_TRIES);
}
if (attempt < VERSIONING_MAX_TRIES) {
try {
Thread.sleep(VERSIONING_RETRY_SLEEP_MS);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return true;
}
}
if (StringUtils.isBlank(accessKey) || StringUtils.isBlank(secretKey)) {
logger.warn("ECS: missing S3 credentials for accountId={}; skipping versioning.", accountId);
return true;
}
logger.warn("ECS: versioning {} for '{}' gave up after {} attempts; leaving as-is.",
desired, bucketName, VERSIONING_MAX_TRIES);
return true;
try (CloseableHttpClient http = buildHttpClient(insecure)) {
putBucketVersioningSigV2(
http,
ep.scheme,
ep.host,
bucketName,
accessKey,
secretKey,
desired
);
logger.info("ECS: bucket versioning {} succeeded for '{}'", desired, bucketName);
return true;
} catch (Exception e) {
logger.warn("ECS: bucket versioning {} failed for '{}': {}",
desired, bucketName, e.getMessage());
return true; // best-effort (do NOT break createBucket)
}
}
// ----- S3 Versioning (SigV2 path-style) -----
// ----- S3 Versioning (SigV2, EXACTLY matches bash script) -----
private void setS3BucketVersioning(final CloseableHttpClient http,
final String scheme,
final String host,
final String bucketName,
final String accessKey,
final String secretKey,
final String status) throws Exception {
private void putBucketVersioningSigV2(final CloseableHttpClient http,
final String scheme,
final String host,
final String bucketName,
final String accessKey,
final String secretKey,
final String status) throws Exception {
// EXACT XML (no namespace, matches bash)
final String body =
"<VersioningConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">"
"<VersioningConfiguration>"
+ "<Status>" + status + "</Status>"
+ "</VersioningConfiguration>";
@ -737,100 +706,41 @@ public class EcsObjectStoreDriverImpl extends BaseObjectStoreDriverImpl {
final String contentMd5 = base64Md5(bodyBytes);
final String dateHdr = rfc1123Now();
// IMPORTANT: include trailing slash before subresource
final String canonicalResource = "/" + bucketName + "/?versioning";
final String sts = "PUT\n" + contentMd5 + "\n" + contentType + "\n" + dateHdr + "\n" + canonicalResource;
final String signature = hmacSha1Base64(sts, secretKey);
// IMPORTANT: NO trailing slash before ?versioning
final String canonicalResource = "/" + bucketName + "?versioning";
final String stringToSign =
"PUT\n"
+ contentMd5 + "\n"
+ contentType + "\n"
+ dateHdr + "\n"
+ canonicalResource;
final String signature = hmacSha1Base64(stringToSign, secretKey);
final String url = scheme + "://" + host + "/" + bucketName + "?versioning";
final String url = scheme + "://" + host + "/" + bucketName + "/?versioning";
final HttpPut put = new HttpPut(url);
put.setHeader("Host", host);
put.setHeader("Date", dateHdr);
put.setHeader("Authorization", "AWS " + accessKey + ":" + signature);
put.setHeader("Content-Type", contentType);
put.setHeader("Content-MD5", contentMd5);
put.setHeader("Authorization", "AWS " + accessKey + ":" + signature);
put.setEntity(new StringEntity(body, StandardCharsets.UTF_8));
try (CloseableHttpResponse resp = http.execute(put)) {
final int st = resp.getStatusLine().getStatusCode();
final String rb = resp.getEntity() != null
final int statusCode = resp.getStatusLine().getStatusCode();
final String respBody = resp.getEntity() != null
? EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8)
: "";
if (st != 200 && st != 204) {
throw new CloudRuntimeException("S3 versioning " + status + " failed: HTTP " + st + " body=" + rb);
if (statusCode != 200 && statusCode != 204) {
throw new CloudRuntimeException(
"S3 versioning failed: HTTP " + statusCode + " body=" + respBody
);
}
}
}
private String getS3BucketVersioningStatus(final CloseableHttpClient http,
final String scheme,
final String host,
final String bucketName,
final String accessKey,
final String secretKey) throws Exception {
final String dateHdr = rfc1123Now();
final String canonicalResource = "/" + bucketName + "/?versioning";
final String sts = "GET\n\n\n" + dateHdr + "\n" + canonicalResource;
final String signature = hmacSha1Base64(sts, secretKey);
final String url = scheme + "://" + host + "/" + bucketName + "/?versioning";
final HttpGet get = new HttpGet(url);
get.setHeader("Host", host);
get.setHeader("Date", dateHdr);
get.setHeader("Authorization", "AWS " + accessKey + ":" + signature);
try (CloseableHttpResponse resp = http.execute(get)) {
final int st = resp.getStatusLine().getStatusCode();
final String rb = resp.getEntity() != null
? EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8)
: "";
if (st != 200 && st != 204) {
throw new CloudRuntimeException("S3 get versioning failed: HTTP " + st + " body=" + rb);
}
final String status = xml.extractTag(rb, "Status");
return status != null ? status.trim() : "";
}
}
private void setS3BucketVersioningWithVerify(final CloseableHttpClient http,
final String scheme,
final String host,
final String bucketName,
final String accessKey,
final String secretKey,
final String desired) throws Exception {
setS3BucketVersioning(http, scheme, host, bucketName, accessKey, secretKey, desired);
// Verify (best-effort; ECS may be eventually consistent)
for (int i = 1; i <= 10; i++) {
try {
final String got = getS3BucketVersioningStatus(http, scheme, host, bucketName, accessKey, secretKey);
if (desired.equalsIgnoreCase(got)) {
logger.info("ECS: versioning verify OK for '{}': {}", bucketName, got);
return;
}
logger.warn("ECS: versioning verify mismatch for '{}': desired={} got={} (try {}/10)",
bucketName, desired, got, i);
} catch (Exception e) {
logger.debug("ECS: versioning verify error for '{}': {} (try {}/10)",
bucketName, e.getMessage(), i);
}
try {
Thread.sleep(500L);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return;
}
}
logger.warn("ECS: versioning verify FAILED for '{}': desired={} (backend may be eventually consistent)",
bucketName, desired);
}
// ---------------- Quota ----------------
@Override
@ -1355,15 +1265,15 @@ public class EcsObjectStoreDriverImpl extends BaseObjectStoreDriverImpl {
}
private BucketVO resolveBucketVO(final BucketTO bucket) {
if (bucket == null) {
return null;
}
if (bucket == null) {
return null;
}
final long id = getLongFromGetter(bucket, "getId", -1L);
if (id > 0) {
return bucketDao.findById(id);
}
return null;
final long id = getLongFromGetter(bucket, "getId", -1L);
if (id > 0) {
return bucketDao.findById(id);
}
return null;
}
private static String base64Md5(final byte[] data) throws Exception {

View File

@ -1,21 +1,3 @@
/*
* 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.
*/
package org.apache.cloudstack.storage.datastore.driver;
import java.util.ArrayList;

View File

@ -28,4 +28,4 @@
>
<bean id="EcsObjectStoreProviderImpl"
class="org.apache.cloudstack.storage.datastore.provider.EcsObjectStoreProviderImpl" />
</beans>
</beans>

View File

@ -68,73 +68,97 @@
<!-- Use secretKey field for the password to make provider shared configuration easier -->
<a-input-password v-model:value="form.secretKey" autocomplete="off"/>
</a-form-item>
<a-form-item name="s3Url" ref="s3Url" :label="$t('label.cloudian.s3.url')" :rules="[{ required: true, message: $t('label.required') }]">
<a-form-item
name="s3Url"
ref="s3Url"
:label="$t('label.cloudian.s3.url')"
:rules="[{ required: true, message: $t('label.required') }]"
>
<a-input v-model:value="form.s3Url" placeholder="https://s3-hostname or http://s3-hostname"/>
</a-form-item>
<a-form-item name="iamUrl" ref="iamUrl" :label="$t('label.cloudian.iam.url')" :rules="[{ required: true, message: $t('label.required') }]">
<a-form-item
name="iamUrl"
ref="iamUrl"
:label="$t('label.cloudian.iam.url')"
:rules="[{ required: true, message: $t('label.required') }]"
>
<a-input v-model:value="form.iamUrl" placeholder="https://iam-hostname:16443 or http://iam-hostname:16080"/>
</a-form-item>
</div>
<!-- ECS Object Store Configuration -->
<div v-else-if="form.provider === 'ECS'">
<!-- S3 URL (for UI: this becomes addObjectStoragePool url=...) -->
<!-- Keep old (existing) user-facing labels/strings here to avoid missing i18n keys -->
<a-form-item name="url" ref="url">
<template #label>
<tooltip-label :title="'ECS Public URL'" :tooltip="'The S3-compatible endpoint URL that clients use to connect to ECS'"/>
<tooltip-label
:title="'ECS Public URL'"
:tooltip="'The S3-compatible endpoint URL that clients use to connect to ECS'"
/>
</template>
<a-input v-model:value="form.url" placeholder="https://ecs.example.com" />
</a-form-item>
<!-- Management API URL -> details[0].value (mgmt_url) -->
<a-form-item name="mgmtUrl" ref="mgmtUrl" :rules="[{ required: true, message: $t('label.required') }]">
<template #label>
<tooltip-label :title="'ECS API URL'" :tooltip="'ECS management API URL'"/>
<tooltip-label
:title="'ECS API URL'"
:tooltip="'ECS management API URL'"
/>
</template>
<a-input v-model:value="form.mgmtUrl" placeholder="https://ecs-api.elcld.net" />
</a-form-item>
<!-- S3 host (hostname[:port], no scheme) -> details[1].value (s3_host) -->
<a-form-item name="s3Host" ref="s3Host" :rules="[{ required: true, message: $t('label.required') }]">
<template #label>
<tooltip-label :title="'ECS Private URL'" :tooltip="'The internal S3 endpoint URL used by CloudStack to communicate with ECS. May be the same as the Public URL.'"/>
<tooltip-label
:title="'ECS Private URL'"
:tooltip="'The internal S3 endpoint URL used by CloudStack to communicate with ECS. May be the same as the Public URL.'"
/>
</template>
<a-input v-model:value="form.s3Host" placeholder="ecs.example.com or ecs.example.com:9020" />
</a-form-item>
<!-- Service account user -> details[2].value (sa_user) -->
<a-form-item name="accessKey" ref="accessKey">
<template #label>
<tooltip-label :title="'ECS service account user'" :tooltip="'Service account user, e.g. cloudstack'"/>
<tooltip-label
:title="'ECS service account user'"
:tooltip="'Service account user, e.g. cloudstack'"
/>
</template>
<a-input v-model:value="form.accessKey" placeholder="cloudstack" />
</a-form-item>
<!-- Service account password -> details[3].value (sa_password) -->
<a-form-item name="secretKey" ref="secretKey">
<template #label>
<tooltip-label :title="'ECS service account password'" :tooltip="'Service account password (sa_password)'"/>
<tooltip-label
:title="'ECS service account password'"
:tooltip="'Service account password (sa_password)'"
/>
</template>
<a-input-password v-model:value="form.secretKey" autocomplete="off" />
</a-form-item>
<!-- Namespace -> details[4].value (namespace) -->
<a-form-item name="namespace" ref="namespace" :rules="[{ required: true, message: $t('label.required') }]">
<template #label>
<tooltip-label :title="'Namespace'" :tooltip="'ECS namespace (namespace), e.g. cloudstack'"/>
<tooltip-label
:title="'Namespace'"
:tooltip="'ECS namespace (namespace), e.g. cloudstack'"
/>
</template>
<a-input v-model:value="form.namespace" placeholder="cloudstack" />
</a-form-item>
<!-- User prefix -> details[5].value (user_prefix) -->
<a-form-item name="userPrefix" ref="userPrefix">
<template #label>
<tooltip-label :title="'User prefix'" :tooltip="'Prefix used for ECS user creation. Default is cs- (user_prefix). Example: cs-'"/>
<tooltip-label
:title="'User prefix'"
:tooltip="'Prefix used for ECS user creation. Default is cs- (user_prefix). Example: cs-'"
/>
</template>
<a-input v-model:value="form.userPrefix" placeholder="cs-" />
</a-form-item>
<!-- Insecure -> details[6].value (insecure) -->
<a-form-item name="insecure" ref="insecure">
<a-checkbox v-model:checked="form.insecure">
Allow insecure HTTPS (set insecure=true)
@ -233,6 +257,72 @@ export default {
closeModal () {
this.$emit('close-action')
},
buildCloudianDetails (data, values) {
data['details[0].key'] = 'accesskey'
data['details[0].value'] = values.accessKey
data['details[1].key'] = 'secretkey'
data['details[1].value'] = values.secretKey
data['details[2].key'] = 'validateSSL'
data['details[2].value'] = values.validateSSL
data['details[3].key'] = 's3Url'
data['details[3].value'] = values.s3Url
data['details[4].key'] = 'iamUrl'
data['details[4].value'] = values.iamUrl
},
buildEcsDetails (data, values) {
data['details[0].key'] = 'mgmt_url'
data['details[0].value'] = values.mgmtUrl
data['details[1].key'] = 's3_host'
data['details[1].value'] = values.s3Host
data['details[2].key'] = 'sa_user'
data['details[2].value'] = values.accessKey
data['details[3].key'] = 'sa_password'
data['details[3].value'] = values.secretKey
data['details[4].key'] = 'namespace'
data['details[4].value'] = values.namespace
data['details[5].key'] = 'user_prefix'
data['details[5].value'] =
values.userPrefix && values.userPrefix.trim() !== ''
? values.userPrefix.trim()
: 'cs-'
data['details[6].key'] = 'insecure'
data['details[6].value'] = values.insecure ? 'true' : 'false'
},
buildGenericDetails (data, values) {
data['details[0].key'] = 'accesskey'
data['details[0].value'] = values.accessKey
data['details[1].key'] = 'secretkey'
data['details[1].value'] = values.secretKey
if (values.size) {
data.size = values.size
}
},
buildDetailsByProvider (data, values) {
const provider = values.provider
if (provider === 'Cloudian HyperStore') {
this.buildCloudianDetails(data, values)
return
}
if (provider === 'ECS') {
this.buildEcsDetails(data, values)
return
}
this.buildGenericDetails(data, values)
},
handleSubmit (e) {
e.preventDefault()
if (this.loading) return
@ -241,66 +331,14 @@ export default {
const values = this.handleRemoveFields(formRaw)
const data = {
name: values.name
name: values.name,
size: values.size
}
const provider = values.provider
data.provider = provider
data.provider = values.provider
data.url = values.url
if (provider === 'Cloudian HyperStore') {
// Cloudian HyperStore details
data['details[0].key'] = 'accesskey'
data['details[0].value'] = values.accessKey
data['details[1].key'] = 'secretkey'
data['details[1].value'] = values.secretKey
data['details[2].key'] = 'validateSSL'
data['details[2].value'] = values.validateSSL
data['details[3].key'] = 's3Url'
data['details[3].value'] = values.s3Url
data['details[4].key'] = 'iamUrl'
data['details[4].value'] = values.iamUrl
} else if (provider === 'ECS') {
// ECS details:
// details[0]=mgmt_url, [1]=s3_host, [2]=sa_user, [3]=sa_password, [4]=namespace, [5]=user_prefix, [6]=insecure
data['details[0].key'] = 'mgmt_url'
data['details[0].value'] = values.mgmtUrl
data['details[1].key'] = 's3_host'
data['details[1].value'] = values.s3Host
data['details[2].key'] = 'sa_user'
data['details[2].value'] = values.accessKey
data['details[3].key'] = 'sa_password'
data['details[3].value'] = values.secretKey
data['details[4].key'] = 'namespace'
data['details[4].value'] = values.namespace
// Optional; only send if user entered something (driver defaults to cs- when missing)
if (values.userPrefix && values.userPrefix.trim() !== '') {
data['details[5].key'] = 'user_prefix'
data['details[5].value'] = values.userPrefix.trim()
} else {
// keep ordering stable for insecure when prefix omitted
data['details[5].key'] = 'user_prefix'
data['details[5].value'] = 'cs-'
}
data['details[6].key'] = 'insecure'
data['details[6].value'] = values.insecure ? 'true' : 'false'
} else {
// Generic non-Cloudian, non-ECS object stores
data['details[0].key'] = 'accesskey'
data['details[0].value'] = values.accessKey
data['details[1].key'] = 'secretkey'
data['details[1].value'] = values.secretKey
if (values.size) {
data.size = values.size
}
}
this.buildDetailsByProvider(data, values)
this.loading = true
@ -322,9 +360,10 @@ export default {
this.formRef.value.scrollToField(error.errorFields[0].name)
})
},
addObjectStore (params) {
return new Promise((resolve, reject) => {
getAPI('addObjectStoragePool', params).then(json => {
getAPI('addObjectStoragePool', params).then(() => {
resolve()
}).catch(error => {
reject(error)

View File

@ -128,26 +128,13 @@ export default {
const selectedId = this.form?.objectstore
const stores = this.objectstores || []
const selected = stores.find(os => os.id === selectedId)
if (!selected) {
return false
}
if (!selected) return false
const provider = (
selected.objectstoreprovider ||
selected.objectStoreProvider ||
selected.provider ||
''
).toString().toUpperCase()
const provider = (selected.providername || '')
.toString()
.toUpperCase()
const name = (selected.name || '').toString().toUpperCase()
if (provider.includes('ECS')) {
return true
}
if (name.includes('ECS')) {
return true
}
return false
return provider.includes('ECS')
},
showObjectLocking () {
return !this.isEcsObjectStore

View File

@ -31,7 +31,7 @@
:placeholder="$t('label.quota')"/>
</a-form-item>
<!-- Encryption toggle hidden only when object store provider is ECS -->
<!-- Encryption hidden when object store provider is ECS -->
<a-form-item
v-if="showEncryption"
name="encryption"
@ -48,6 +48,17 @@
:checked="form.versioning"/>
</a-form-item>
<!-- Object Lock hidden when object store provider is ECS -->
<a-form-item
v-if="showObjectLocking"
name="objectlocking"
ref="objectlocking"
:label="$t('label.objectlocking')">
<a-switch
v-model:checked="form.objectlocking"
:checked="form.objectlocking"/>
</a-form-item>
<a-form-item name="Bucket Policy" ref="policy" :label="$t('label.bucket.policy')">
<a-select
v-model:value="form.policy"
@ -98,16 +109,20 @@ export default {
isEcsObjectStore () {
const r = this.resource || {}
const provider = (
r.providername ||
r.objectstoreprovider ||
r.objectStoreProvider ||
r.objectStoreprovider ||
r.provider ||
''
).toString().toUpperCase()
return provider === 'ECS'
return provider.includes('ECS')
},
showEncryption () {
return !this.isEcsObjectStore
},
showObjectLocking () {
return !this.isEcsObjectStore
}
},
beforeCreate () {
@ -132,33 +147,43 @@ export default {
this.loading = true
Object.keys(this.apiParams).forEach(item => {
const field = this.apiParams[item]
let fieldName = field.name
let fieldName = null
if (field.type === 'list' || field.name === 'account') {
fieldName = field.name.replace('ids', 'name').replace('id', 'name')
} else {
fieldName = field.name
}
const fieldValue = this.resource[fieldName] ?? null
const fieldValue = this.resource?.[fieldName]
if (fieldValue !== null && fieldValue !== undefined) {
form[field.name] = fieldValue
}
})
this.loading = false
},
handleSubmit () {
handleSubmit (e) {
if (e?.preventDefault) e.preventDefault()
if (this.loading) return
this.formRef.value.validate().then(() => {
const values = toRaw(this.form)
const formRaw = toRaw(this.form)
const values = this.handleRemoveFields(formRaw)
const data = {
id: this.resource.id,
quota: values.quota,
versioning: values.versioning,
objectlocking: values.objectlocking,
policy: values.policy
}
// Hide + do not send encryption/objectlocking for ECS
if (!this.isEcsObjectStore) {
data.encryption = values.encryption
data.objectlocking = values.objectlocking
}
this.loading = true
postAPI('updateBucket', data).then(response => {
postAPI('updateBucket', data).then(() => {
this.$emit('refresh-data')
this.$notification.success({
message: this.$t('label.bucket.update'),