mirror of https://github.com/apache/cloudstack.git
Merge 3aed82128a into 9bbd32a8ef
This commit is contained in:
commit
6045c3016c
|
|
@ -632,6 +632,11 @@
|
|||
<artifactId>cloud-plugin-storage-object-minio</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-plugin-storage-object-ecs</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-plugin-storage-object-ceph</artifactId>
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@
|
|||
<module>storage/object/ceph</module>
|
||||
<module>storage/object/cloudian</module>
|
||||
<module>storage/object/simulator</module>
|
||||
<module>storage/object/ECS</module>
|
||||
|
||||
|
||||
<module>storage-allocators/random</module>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
<!--
|
||||
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.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>cloud-plugin-storage-object-ecs</artifactId>
|
||||
<name>Apache CloudStack Plugin - ECS object storage provider</name>
|
||||
<parent>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloudstack-plugins</artifactId>
|
||||
<version>4.23.0.0-SNAPSHOT</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-engine-storage</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-engine-storage-object</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-engine-schema</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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() {}
|
||||
|
||||
// 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 NAMESPACE = "namespace";
|
||||
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-";
|
||||
|
||||
// Per-account keys
|
||||
public static final String AD_KEY_ACCESS = "ecs.accesskey";
|
||||
public static final String AD_KEY_SECRET = "ecs.secretkey";
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* 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;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.apache.http.auth.UsernamePasswordCredentials;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.impl.auth.BasicScheme;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
|
||||
public class EcsMgmtTokenManager {
|
||||
private static final long DEFAULT_TOKEN_MAX_AGE_SEC = 300;
|
||||
private static final long EXPIRY_SKEW_SEC = 30;
|
||||
|
||||
private static final ConcurrentHashMap<TokenKey, TokenEntry> TOKEN_CACHE = new ConcurrentHashMap<>();
|
||||
private static final ConcurrentHashMap<TokenKey, Object> TOKEN_LOCKS = new ConcurrentHashMap<>();
|
||||
|
||||
static final class EcsUnauthorizedException extends RuntimeException {
|
||||
EcsUnauthorizedException(final String msg) { super(msg); }
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface WithToken<T> { T run(String token) throws Exception; }
|
||||
|
||||
private static final class TokenKey {
|
||||
final String mgmtUrl;
|
||||
final String user;
|
||||
TokenKey(final String mgmtUrl, final String user) {
|
||||
this.mgmtUrl = mgmtUrl;
|
||||
this.user = user;
|
||||
}
|
||||
@Override public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof TokenKey)) return false;
|
||||
final TokenKey k = (TokenKey) o;
|
||||
return Objects.equals(mgmtUrl, k.mgmtUrl) && Objects.equals(user, k.user);
|
||||
}
|
||||
@Override public int hashCode() { return Objects.hash(mgmtUrl, user); }
|
||||
}
|
||||
|
||||
private static final class TokenEntry {
|
||||
final String token;
|
||||
final long expiresAtMs;
|
||||
TokenEntry(final String token, final long expiresAtMs) {
|
||||
this.token = token;
|
||||
this.expiresAtMs = expiresAtMs;
|
||||
}
|
||||
boolean validNow() {
|
||||
return token != null && !token.isBlank() && System.currentTimeMillis() < expiresAtMs;
|
||||
}
|
||||
}
|
||||
|
||||
public <T> T callWithRetry401(final EcsObjectStoreDriverImpl.EcsCfg cfg,
|
||||
final WithToken<T> op,
|
||||
final HttpClientFactory httpFactory) throws Exception {
|
||||
try {
|
||||
return op.run(getAuthToken(cfg, httpFactory));
|
||||
} catch (EcsUnauthorizedException u) {
|
||||
invalidate(cfg);
|
||||
return op.run(getAuthToken(cfg, httpFactory));
|
||||
}
|
||||
}
|
||||
|
||||
public void invalidate(final EcsObjectStoreDriverImpl.EcsCfg cfg) {
|
||||
TOKEN_CACHE.remove(new TokenKey(trimTail(cfg.mgmtUrl), cfg.saUser));
|
||||
}
|
||||
|
||||
public String getAuthToken(final EcsObjectStoreDriverImpl.EcsCfg cfg,
|
||||
final HttpClientFactory httpFactory) {
|
||||
final String mu = trimTail(cfg.mgmtUrl);
|
||||
final TokenKey key = new TokenKey(mu, cfg.saUser);
|
||||
|
||||
final TokenEntry cached = TOKEN_CACHE.get(key);
|
||||
if (cached != null && cached.validNow()) return cached.token;
|
||||
|
||||
final Object lock = TOKEN_LOCKS.computeIfAbsent(key, k -> new Object());
|
||||
synchronized (lock) {
|
||||
final TokenEntry cached2 = TOKEN_CACHE.get(key);
|
||||
if (cached2 != null && cached2.validNow()) return cached2.token;
|
||||
|
||||
final TokenEntry fresh = loginAndGetTokenFresh(mu, cfg.saUser, cfg.saPass, cfg.insecure, httpFactory);
|
||||
TOKEN_CACHE.put(key, fresh);
|
||||
return fresh.token;
|
||||
}
|
||||
}
|
||||
|
||||
private TokenEntry loginAndGetTokenFresh(final String mgmtUrl,
|
||||
final String user,
|
||||
final String pass,
|
||||
final boolean insecure,
|
||||
final HttpClientFactory httpFactory) {
|
||||
try (CloseableHttpClient http = httpFactory.build(insecure)) {
|
||||
final HttpGet get = new HttpGet(mgmtUrl + "/login");
|
||||
UsernamePasswordCredentials creds = new UsernamePasswordCredentials(user, pass);
|
||||
get.addHeader(new BasicScheme().authenticate(creds, get, null));
|
||||
|
||||
try (CloseableHttpResponse resp = http.execute(get)) {
|
||||
final int status = resp.getStatusLine().getStatusCode();
|
||||
if (status != 200 && status != 201) {
|
||||
final String body = resp.getEntity() != null
|
||||
? EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8)
|
||||
: "";
|
||||
throw new CloudRuntimeException("ECS /login failed: HTTP " + status + " body=" + body);
|
||||
}
|
||||
if (resp.getFirstHeader("X-SDS-AUTH-TOKEN") == null) {
|
||||
throw new CloudRuntimeException("ECS /login did not return X-SDS-AUTH-TOKEN header");
|
||||
}
|
||||
|
||||
final String token = resp.getFirstHeader("X-SDS-AUTH-TOKEN").getValue();
|
||||
|
||||
long maxAgeSec = DEFAULT_TOKEN_MAX_AGE_SEC;
|
||||
try {
|
||||
if (resp.getFirstHeader("X-SDS-AUTH-MAX-AGE") != null) {
|
||||
maxAgeSec = Long.parseLong(resp.getFirstHeader("X-SDS-AUTH-MAX-AGE").getValue().trim());
|
||||
}
|
||||
} catch (Exception ignore) { }
|
||||
|
||||
final long effectiveSec = Math.max(5, maxAgeSec - EXPIRY_SKEW_SEC);
|
||||
final long expiresAtMs = System.currentTimeMillis() + (effectiveSec * 1000L);
|
||||
return new TokenEntry(token, expiresAtMs);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new CloudRuntimeException("Failed to obtain ECS auth token: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String trimTail(final String s) {
|
||||
if (s == null) return null;
|
||||
return s.endsWith("/") ? s.substring(0, s.length() - 1) : s;
|
||||
}
|
||||
|
||||
/** Simple seam for testability; implemented by the driver using its existing buildHttpClient(). */
|
||||
@FunctionalInterface
|
||||
public interface HttpClientFactory {
|
||||
CloseableHttpClient build(boolean insecure);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class EcsXmlParser {
|
||||
|
||||
public Integer parseIntTag(final String xml, final String tag) {
|
||||
String v = extractTag(xml, tag);
|
||||
if (v == null) return null;
|
||||
try { return Integer.parseInt(v.trim()); } catch (NumberFormatException ignore) { return null; }
|
||||
}
|
||||
|
||||
public String extractTag(final String xml, final String tag) {
|
||||
if (xml == null) return null;
|
||||
final String open = "<" + tag + ">";
|
||||
final String close = "</" + tag + ">";
|
||||
int i = xml.indexOf(open);
|
||||
if (i < 0) return null;
|
||||
int j = xml.indexOf(close, i + open.length());
|
||||
if (j < 0) return null;
|
||||
return xml.substring(i + open.length(), j).trim();
|
||||
}
|
||||
|
||||
public List<String> extractAllTags(final String xml, final String tag) {
|
||||
final List<String> out = new ArrayList<>();
|
||||
if (xml == null) return out;
|
||||
|
||||
final String open = "<" + tag + ">";
|
||||
final String close = "</" + tag + ">";
|
||||
|
||||
int from = 0;
|
||||
while (true) {
|
||||
int i = xml.indexOf(open, from);
|
||||
if (i < 0) break;
|
||||
int j = xml.indexOf(close, i + open.length());
|
||||
if (j < 0) break;
|
||||
out.add(xml.substring(i + open.length(), j).trim());
|
||||
from = j + close.length();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public void extractKeysFromListBucketXml(final String xml, final List<String> keys) {
|
||||
if (xml == null) return;
|
||||
final String contentsOpen = "<Contents>";
|
||||
final String contentsClose = "</Contents>";
|
||||
int from = 0;
|
||||
while (true) {
|
||||
int i = xml.indexOf(contentsOpen, from);
|
||||
if (i < 0) break;
|
||||
int j = xml.indexOf(contentsClose, i + contentsOpen.length());
|
||||
if (j < 0) break;
|
||||
String block = xml.substring(i, j + contentsClose.length());
|
||||
String key = extractTag(block, "Key");
|
||||
if (key != null && !key.isEmpty()) keys.add(key.trim());
|
||||
from = j + contentsClose.length();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean looksLikeBucketAlreadyExists400(final String respBody) {
|
||||
final String lb = respBody == null ? "" : respBody.toLowerCase(Locale.ROOT);
|
||||
return lb.contains("already exist")
|
||||
|| lb.contains("already_exists")
|
||||
|| lb.contains("already-exists")
|
||||
|| lb.contains("name already in use")
|
||||
|| lb.contains("bucket exists")
|
||||
|| lb.contains("duplicate");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,321 @@
|
|||
/*
|
||||
* 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.lifecycle;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope;
|
||||
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
|
||||
import org.apache.cloudstack.engine.subsystem.api.storage.HostScope;
|
||||
import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope;
|
||||
import org.apache.cloudstack.storage.datastore.db.ObjectStoreVO;
|
||||
import org.apache.cloudstack.storage.datastore.driver.EcsConstants;
|
||||
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.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.http.auth.UsernamePasswordCredentials;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet; // change to POST if ECS needs it
|
||||
import org.apache.http.conn.ssl.NoopHostnameVerifier;
|
||||
import org.apache.http.impl.auth.BasicScheme;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.ssl.SSLContextBuilder;
|
||||
import org.apache.http.ssl.TrustStrategy;
|
||||
|
||||
import com.cloud.agent.api.StoragePoolInfo;
|
||||
import com.cloud.hypervisor.Hypervisor.HypervisorType;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
|
||||
public class EcsObjectStoreLifeCycleImpl implements ObjectStoreLifeCycle {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(EcsObjectStoreLifeCycleImpl.class);
|
||||
|
||||
private static final String PROVIDER_NAME = "ECS";
|
||||
|
||||
@Inject
|
||||
ObjectStoreHelper objectStoreHelper;
|
||||
|
||||
@Inject
|
||||
ObjectStoreProviderManager objectStoreMgr;
|
||||
|
||||
public EcsObjectStoreLifeCycleImpl() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataStore initialize(final Map<String, Object> dsInfos) {
|
||||
requireInjected();
|
||||
|
||||
final String url = getString(dsInfos, "url", true);
|
||||
final String name = getString(dsInfos, "name", true);
|
||||
final Long size = getLong(dsInfos, "size"); // optional
|
||||
final String providerName = getProviderName(dsInfos);
|
||||
|
||||
final Map<String, String> details = getDetails(dsInfos); // typed map, no unchecked cast
|
||||
|
||||
final EcsConfig cfg = verifyAndNormalize(details);
|
||||
|
||||
LOG.info("ECS initialize: provider='{}', name='{}', url='{}', mgmt_url='{}', insecure={}, s3_host='{}', namespace='{}'",
|
||||
providerName, name, url, cfg.mgmtUrl, cfg.insecure,
|
||||
details.get(EcsConstants.S3_HOST), details.get(EcsConstants.NAMESPACE));
|
||||
|
||||
// Try ECS login up-front so we fail fast on bad config
|
||||
loginAndGetToken(cfg.mgmtUrl, cfg.saUser, cfg.saPass, cfg.insecure);
|
||||
|
||||
// Put “canonical” values back into details (so DB keeps what we validated)
|
||||
applyCanonicalDetails(details, cfg);
|
||||
|
||||
final Map<String, Object> objectStoreParameters = buildObjectStoreParams(name, url, size, providerName);
|
||||
|
||||
try {
|
||||
LOG.info("ECS: creating ObjectStore in DB: name='{}', provider='{}', url='{}'",
|
||||
name, providerName, url);
|
||||
|
||||
final ObjectStoreVO objectStore = objectStoreHelper.createObjectStore(objectStoreParameters, details);
|
||||
if (objectStore == null) {
|
||||
throw new CloudRuntimeException("ECS: createObjectStore returned null");
|
||||
}
|
||||
|
||||
final DataStore store = objectStoreMgr.getObjectStore(objectStore.getId());
|
||||
if (store == null) {
|
||||
throw new CloudRuntimeException("ECS: getObjectStore returned null for id=" + objectStore.getId());
|
||||
}
|
||||
|
||||
LOG.info("ECS: object store created: id={}, name='{}'", objectStore.getId(), name);
|
||||
return store;
|
||||
} catch (RuntimeException e) {
|
||||
final String msg = "ECS: failed to persist object store '" + name + "': " + safeMsg(e);
|
||||
LOG.error(msg, e);
|
||||
throw new CloudRuntimeException(msg, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean attachCluster(final DataStore store, final ClusterScope scope) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean attachHost(final DataStore store, final HostScope scope, final StoragePoolInfo existingInfo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean attachZone(final DataStore dataStore, final ZoneScope scope, final HypervisorType hypervisorType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean maintain(final DataStore store) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancelMaintain(final DataStore store) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteDataStore(final DataStore store) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean migrateToObjectStore(final DataStore store) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
private static final class EcsConfig {
|
||||
final String mgmtUrl;
|
||||
final String saUser;
|
||||
final String saPass;
|
||||
final boolean insecure;
|
||||
|
||||
private EcsConfig(final String mgmtUrl, final String saUser, final String saPass, final boolean insecure) {
|
||||
this.mgmtUrl = mgmtUrl;
|
||||
this.saUser = saUser;
|
||||
this.saPass = saPass;
|
||||
this.insecure = insecure;
|
||||
}
|
||||
}
|
||||
|
||||
private void requireInjected() {
|
||||
if (objectStoreHelper == null) {
|
||||
throw new CloudRuntimeException("ECS: ObjectStoreHelper is not injected");
|
||||
}
|
||||
if (objectStoreMgr == null) {
|
||||
throw new CloudRuntimeException("ECS: ObjectStoreProviderManager is not injected");
|
||||
}
|
||||
}
|
||||
|
||||
private static String getString(final Map<String, Object> dsInfos, final String key, final boolean required) {
|
||||
final Object v = dsInfos.get(key);
|
||||
final String s = (v == null) ? null : v.toString().trim();
|
||||
if (required && (s == null || s.isEmpty())) {
|
||||
throw new CloudRuntimeException("ECS: missing required parameter '" + key + "'");
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private static Long getLong(final Map<String, Object> dsInfos, final String key) {
|
||||
final Object v = dsInfos.get(key);
|
||||
if (v == null) {
|
||||
return null;
|
||||
}
|
||||
if (v instanceof Number) {
|
||||
return ((Number) v).longValue();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(v.toString().trim());
|
||||
} catch (Exception e) {
|
||||
throw new CloudRuntimeException("ECS: invalid long for '" + key + "': " + v);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getProviderName(final Map<String, Object> dsInfos) {
|
||||
final String p = getString(dsInfos, "providerName", false);
|
||||
return (p == null || p.isEmpty()) ? PROVIDER_NAME : p;
|
||||
}
|
||||
|
||||
private static Map<String, String> getDetails(final Map<String, Object> dsInfos) {
|
||||
final Object v = dsInfos.get("details");
|
||||
if (!(v instanceof Map)) {
|
||||
throw new CloudRuntimeException("ECS: details map is missing");
|
||||
}
|
||||
|
||||
final Map<?, ?> raw = (Map<?, ?>) v;
|
||||
final Map<String, String> out = new HashMap<>();
|
||||
for (Map.Entry<?, ?> e : raw.entrySet()) {
|
||||
if (e.getKey() == null) {
|
||||
continue;
|
||||
}
|
||||
final String k = e.getKey().toString();
|
||||
final String val = e.getValue() == null ? null : e.getValue().toString();
|
||||
out.put(k, val);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static EcsConfig verifyAndNormalize(final Map<String, String> details) {
|
||||
final String mgmtUrl = trim(details.get(EcsConstants.MGMT_URL));
|
||||
final String saUser = safe(details.get(EcsConstants.SA_USER));
|
||||
final String saPass = safe(details.get(EcsConstants.SA_PASS));
|
||||
final boolean insecure = Boolean.parseBoolean(details.getOrDefault(EcsConstants.INSECURE, "false"));
|
||||
|
||||
verifyRequiredDetail(EcsConstants.MGMT_URL, mgmtUrl);
|
||||
verifyRequiredDetail(EcsConstants.SA_USER, saUser);
|
||||
verifyRequiredDetail(EcsConstants.SA_PASS, saPass);
|
||||
|
||||
return new EcsConfig(mgmtUrl, saUser, saPass, insecure);
|
||||
}
|
||||
|
||||
private static void verifyRequiredDetail(final String key, final String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
throw new CloudRuntimeException("ECS: missing required detail '" + key + "'");
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyCanonicalDetails(final Map<String, String> details, final EcsConfig cfg) {
|
||||
details.put(EcsConstants.MGMT_URL, cfg.mgmtUrl);
|
||||
details.put(EcsConstants.SA_USER, cfg.saUser);
|
||||
details.put(EcsConstants.SA_PASS, cfg.saPass);
|
||||
details.put(EcsConstants.INSECURE, Boolean.toString(cfg.insecure));
|
||||
// keep any optional keys already present (S3_HOST, NAMESPACE, etc.)
|
||||
}
|
||||
|
||||
private static Map<String, Object> buildObjectStoreParams(final String name,
|
||||
final String url,
|
||||
final Long size,
|
||||
final String providerName) {
|
||||
final Map<String, Object> p = new HashMap<>();
|
||||
p.put("name", name);
|
||||
p.put("url", url);
|
||||
p.put("size", size);
|
||||
p.put("providerName", providerName);
|
||||
return p;
|
||||
}
|
||||
|
||||
private static String safe(final String v) {
|
||||
return v == null ? "" : v.trim();
|
||||
}
|
||||
|
||||
private static String trim(final String v) {
|
||||
if (v == null) {
|
||||
return null;
|
||||
}
|
||||
final String s = v.trim();
|
||||
return s.endsWith("/") ? s.substring(0, s.length() - 1) : s;
|
||||
}
|
||||
|
||||
private static String safeMsg(final Throwable t) {
|
||||
if (t == null) {
|
||||
return "unknown";
|
||||
}
|
||||
final String m = t.getMessage();
|
||||
return (m == null || m.isEmpty()) ? t.getClass().getSimpleName() : m;
|
||||
}
|
||||
|
||||
private CloseableHttpClient buildHttpClient(final boolean insecure) {
|
||||
if (!insecure) {
|
||||
return HttpClients.createDefault();
|
||||
}
|
||||
try {
|
||||
final TrustStrategy trustAll = (chain, authType) -> true;
|
||||
final SSLContext sslContext = SSLContextBuilder.create()
|
||||
.loadTrustMaterial(null, trustAll)
|
||||
.build();
|
||||
return HttpClients.custom()
|
||||
.setSSLContext(sslContext)
|
||||
.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
throw new CloudRuntimeException("ECS: failed to build HttpClient", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String loginAndGetToken(final String mgmtUrl, final String user, final String pass, final boolean insecure) {
|
||||
try (CloseableHttpClient http = buildHttpClient(insecure)) {
|
||||
final HttpGet get = new HttpGet(mgmtUrl + "/login");
|
||||
get.addHeader(new BasicScheme().authenticate(
|
||||
new UsernamePasswordCredentials(user, pass), get, null));
|
||||
try (CloseableHttpResponse resp = http.execute(get)) {
|
||||
final int status = resp.getStatusLine().getStatusCode();
|
||||
if (status != 200 && status != 201) {
|
||||
throw new CloudRuntimeException("ECS /login failed: HTTP " + status);
|
||||
}
|
||||
if (resp.getFirstHeader("X-SDS-AUTH-TOKEN") == null) {
|
||||
throw new CloudRuntimeException("ECS /login missing X-SDS-AUTH-TOKEN");
|
||||
}
|
||||
return resp.getFirstHeader("X-SDS-AUTH-TOKEN").getValue();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
final String msg = "ECS: management login error: " + safeMsg(e);
|
||||
LOG.error(msg, e);
|
||||
throw new CloudRuntimeException(msg, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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.provider;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver;
|
||||
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreLifeCycle;
|
||||
import org.apache.cloudstack.engine.subsystem.api.storage.HypervisorHostListener;
|
||||
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectStoreProvider;
|
||||
import org.apache.cloudstack.storage.datastore.driver.EcsObjectStoreDriverImpl;
|
||||
import org.apache.cloudstack.storage.datastore.lifecycle.EcsObjectStoreLifeCycleImpl;
|
||||
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;
|
||||
|
||||
import com.cloud.utils.component.ComponentContext;
|
||||
|
||||
@Component
|
||||
public class EcsObjectStoreProviderImpl implements ObjectStoreProvider {
|
||||
|
||||
@Inject
|
||||
ObjectStoreProviderManager storeMgr;
|
||||
|
||||
@Inject
|
||||
ObjectStoreHelper helper;
|
||||
|
||||
private final String providerName = "ECS";
|
||||
|
||||
protected ObjectStoreLifeCycle lifeCycle;
|
||||
protected ObjectStoreDriver driver;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configure(Map<String, Object> params) {
|
||||
// Follow Ceph provider pattern
|
||||
lifeCycle = ComponentContext.inject(EcsObjectStoreLifeCycleImpl.class);
|
||||
driver = ComponentContext.inject(EcsObjectStoreDriverImpl.class);
|
||||
storeMgr.registerDriver(getName(), driver);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataStoreLifeCycle getDataStoreLifeCycle() {
|
||||
return lifeCycle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataStoreDriver getDataStoreDriver() {
|
||||
return driver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HypervisorHostListener getHostListener() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<DataStoreProviderType> getTypes() {
|
||||
Set<DataStoreProviderType> types = new HashSet<>();
|
||||
types.add(DataStoreProviderType.OBJECT);
|
||||
return types;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# 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.
|
||||
name=storage-object-ecs
|
||||
parent=storage
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<!--
|
||||
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.
|
||||
-->
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:context="http://www.springframework.org/schema/context"
|
||||
xmlns:aop="http://www.springframework.org/schema/aop"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans
|
||||
http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
|
||||
http://www.springframework.org/schema/context
|
||||
http://www.springframework.org/schema/context/spring-context.xsd"
|
||||
>
|
||||
<bean id="EcsObjectStoreProviderImpl"
|
||||
class="org.apache.cloudstack.storage.datastore.provider.EcsObjectStoreProviderImpl" />
|
||||
</beans>
|
||||
|
|
@ -31,6 +31,7 @@
|
|||
</template>
|
||||
<a-input v-model:value="form.name" v-focus="true" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="provider" ref="provider">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.providername')" :tooltip="apiParams.provider.description"/>
|
||||
|
|
@ -51,8 +52,8 @@
|
|||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Cloudian HyperStore Only Object Store Configuration -->
|
||||
<div v-if="form.provider === 'Cloudian HyperStore'">
|
||||
<!-- HyperStore Only Object Store Configuration -->
|
||||
<a-form-item name="url" ref="url" :label="$t('label.cloudian.admin.url')">
|
||||
<a-input v-model:value="form.url" placeholder="https://admin-hostname:19443" />
|
||||
</a-form-item>
|
||||
|
|
@ -67,16 +68,106 @@
|
|||
<!-- 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: this.$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: this.$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'">
|
||||
<!-- 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'"
|
||||
/>
|
||||
</template>
|
||||
<a-input v-model:value="form.url" placeholder="https://ecs.example.com" />
|
||||
</a-form-item>
|
||||
|
||||
<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'"
|
||||
/>
|
||||
</template>
|
||||
<a-input v-model:value="form.mgmtUrl" placeholder="https://ecs-api.elcld.net" />
|
||||
</a-form-item>
|
||||
|
||||
<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.'"
|
||||
/>
|
||||
</template>
|
||||
<a-input v-model:value="form.s3Host" placeholder="ecs.example.com or ecs.example.com:9020" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="accessKey" ref="accessKey">
|
||||
<template #label>
|
||||
<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>
|
||||
|
||||
<a-form-item name="secretKey" ref="secretKey">
|
||||
<template #label>
|
||||
<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>
|
||||
|
||||
<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'"
|
||||
/>
|
||||
</template>
|
||||
<a-input v-model:value="form.namespace" placeholder="cloudstack" />
|
||||
</a-form-item>
|
||||
|
||||
<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-'"
|
||||
/>
|
||||
</template>
|
||||
<a-input v-model:value="form.userPrefix" placeholder="cs-" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="insecure" ref="insecure">
|
||||
<a-checkbox v-model:checked="form.insecure">
|
||||
Allow insecure HTTPS (set insecure=true)
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<!-- Non-HyperStore, non-ECS Object Stores -->
|
||||
<div v-else>
|
||||
<!-- Non-HyperStore Object Stores -->
|
||||
<a-form-item name="url" ref="url">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.url')" :tooltip="apiParams.url.description"/>
|
||||
|
|
@ -96,6 +187,7 @@
|
|||
<a-input v-model:value="form.size" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<div :span="24" class="action-button">
|
||||
<a-button @click="closeModal">{{ $t('label.cancel') }}</a-button>
|
||||
<a-button type="primary" ref="submit" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
|
||||
|
|
@ -104,6 +196,7 @@
|
|||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, toRaw } from 'vue'
|
||||
import { getAPI } from '@/api'
|
||||
|
|
@ -127,7 +220,7 @@ export default {
|
|||
inject: ['parentFetchData'],
|
||||
data () {
|
||||
return {
|
||||
providers: ['MinIO', 'Ceph', 'Cloudian HyperStore', 'Simulator'],
|
||||
providers: ['MinIO', 'Ceph', 'Cloudian HyperStore', 'Simulator', 'ECS'],
|
||||
zones: [],
|
||||
loading: false
|
||||
}
|
||||
|
|
@ -144,7 +237,13 @@ export default {
|
|||
this.formRef = ref()
|
||||
this.form = reactive({
|
||||
provider: 'MinIO',
|
||||
validateSSL: true
|
||||
validateSSL: true,
|
||||
// ECS defaults
|
||||
mgmtUrl: '',
|
||||
s3Host: '',
|
||||
namespace: '',
|
||||
userPrefix: 'cs-',
|
||||
insecure: false
|
||||
})
|
||||
this.rules = reactive({
|
||||
url: [{ required: true, message: this.$t('label.required') }],
|
||||
|
|
@ -158,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
|
||||
|
|
@ -165,27 +330,15 @@ export default {
|
|||
const formRaw = toRaw(this.form)
|
||||
const values = this.handleRemoveFields(formRaw)
|
||||
|
||||
var data = {
|
||||
const data = {
|
||||
name: values.name,
|
||||
size: values.size
|
||||
}
|
||||
var provider = values.provider
|
||||
|
||||
data.provider = provider
|
||||
data.provider = values.provider
|
||||
data.url = values.url
|
||||
data['details[0].key'] = 'accesskey'
|
||||
data['details[0].value'] = values.accessKey
|
||||
data['details[1].key'] = 'secretkey'
|
||||
data['details[1].value'] = values.secretKey
|
||||
|
||||
if (provider === 'Cloudian HyperStore') {
|
||||
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
|
||||
}
|
||||
this.buildDetailsByProvider(data, values)
|
||||
|
||||
this.loading = true
|
||||
|
||||
|
|
@ -207,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)
|
||||
|
|
@ -219,6 +373,7 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-layout {
|
||||
width: 85vw;
|
||||
|
|
|
|||
|
|
@ -24,10 +24,11 @@
|
|||
:rules="rules"
|
||||
layout="vertical"
|
||||
@finish="handleSubmit"
|
||||
>
|
||||
>
|
||||
<a-form-item name="name" ref="name" :label="$t('label.name')">
|
||||
<a-input v-model:value="form.name" v-focus="true" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="objectstore" ref="objectstore" :label="$t('label.object.storage')">
|
||||
<a-select
|
||||
v-model:value="form.objectstore"
|
||||
|
|
@ -36,35 +37,40 @@
|
|||
optionFilterProp="value"
|
||||
:filterOption="(input, option) => {
|
||||
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}" >
|
||||
<a-select-option :value="objectstore.id" v-for="objectstore in objectstores" :key="objectstore.id" :label="objectstore.name">
|
||||
}">
|
||||
<a-select-option
|
||||
:value="objectstore.id"
|
||||
v-for="objectstore in objectstores"
|
||||
:key="objectstore.id"
|
||||
:label="objectstore.name">
|
||||
{{ objectstore.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="quota" ref="quota" :label="$t('label.quotagib')">
|
||||
<a-input
|
||||
v-model:value="form.quota"
|
||||
:placeholder="$t('label.quota')"/>
|
||||
:placeholder="$t('label.quota')" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="encryption" ref="encryption" :label="$t('label.encryption')">
|
||||
<a-switch
|
||||
v-model:checked="form.encryption"
|
||||
:checked="encryption"
|
||||
@change="val => { encryption = val }"/>
|
||||
<a-switch v-model:checked="form.encryption" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="versioning" ref="versioning" :label="$t('label.versioning')">
|
||||
<a-switch
|
||||
v-model:checked="form.versioning"
|
||||
:checked="versioning"
|
||||
@change="val => { versioning = val }"/>
|
||||
<a-switch v-model:checked="form.versioning" />
|
||||
</a-form-item>
|
||||
<a-form-item name="objectlocking" ref="objectlocking" :label="$t('label.objectlocking')">
|
||||
<a-switch
|
||||
v-model:checked="form.objectlocking"
|
||||
:checked="objectlocking"
|
||||
@change="val => { objectlocking = val }"/>
|
||||
|
||||
<!-- Object Lock is hidden when the selected object store is ECS -->
|
||||
<a-form-item
|
||||
v-if="showObjectLocking"
|
||||
name="objectlocking"
|
||||
ref="objectlocking"
|
||||
:label="$t('label.objectlocking')">
|
||||
<a-switch v-model: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"
|
||||
|
|
@ -73,14 +79,15 @@
|
|||
optionFilterProp="value"
|
||||
:filterOption="(input, option) => {
|
||||
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}" >
|
||||
}">
|
||||
<a-select-option
|
||||
:value="policy"
|
||||
v-for="(policy,idx) in policyList"
|
||||
v-for="(policy, idx) in policyList"
|
||||
:key="idx"
|
||||
>{{ policy }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<div :span="24" class="action-button">
|
||||
<a-button @click="closeModal">{{ $t('label.cancel') }}</a-button>
|
||||
<a-button type="primary" ref="submit" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
|
||||
|
|
@ -89,6 +96,7 @@
|
|||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, toRaw } from 'vue'
|
||||
import { getAPI, postAPI } from '@/api'
|
||||
|
|
@ -110,18 +118,43 @@ export default {
|
|||
inject: ['parentFetchData'],
|
||||
data () {
|
||||
return {
|
||||
loading: false
|
||||
loading: false,
|
||||
objectstores: [],
|
||||
policyList: ['Public', 'Private']
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isEcsObjectStore () {
|
||||
const selectedId = this.form?.objectstore
|
||||
const stores = this.objectstores || []
|
||||
const selected = stores.find(os => os.id === selectedId)
|
||||
if (!selected) return false
|
||||
|
||||
const provider = (selected.providername || '')
|
||||
.toString()
|
||||
.toUpperCase()
|
||||
|
||||
return provider.includes('ECS')
|
||||
},
|
||||
showObjectLocking () {
|
||||
return !this.isEcsObjectStore
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.initForm()
|
||||
this.policyList = ['Public', 'Private']
|
||||
this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
initForm () {
|
||||
this.formRef = ref()
|
||||
this.form = reactive({
|
||||
name: '',
|
||||
objectstore: null,
|
||||
quota: null,
|
||||
encryption: false,
|
||||
versioning: false,
|
||||
objectlocking: false,
|
||||
policy: 'Private'
|
||||
})
|
||||
this.rules = reactive({
|
||||
name: [{ required: true, message: this.$t('label.required') }],
|
||||
|
|
@ -153,7 +186,7 @@ export default {
|
|||
const formRaw = toRaw(this.form)
|
||||
const values = this.handleRemoveFields(formRaw)
|
||||
|
||||
var data = {
|
||||
const data = {
|
||||
name: values.name,
|
||||
objectstorageid: values.objectstore,
|
||||
quota: values.quota,
|
||||
|
|
@ -179,13 +212,14 @@ export default {
|
|||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}).catch((error) => {
|
||||
}).catch(error => {
|
||||
this.formRef.value.scrollToField(error.errorFields[0].name)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-layout {
|
||||
width: 85vw;
|
||||
|
|
|
|||
|
|
@ -24,23 +24,41 @@
|
|||
layout="vertical"
|
||||
@finish="handleSubmit"
|
||||
>
|
||||
|
||||
<a-form-item name="quota" ref="quota" :label="$t('label.quotagib')">
|
||||
<a-input
|
||||
v-model:value="form.quota"
|
||||
:placeholder="$t('label.quota')"/>
|
||||
</a-form-item>
|
||||
<a-form-item name="encryption" ref="encryption" :label="$t('label.encryption')">
|
||||
|
||||
<!-- Encryption hidden when object store provider is ECS -->
|
||||
<a-form-item
|
||||
v-if="showEncryption"
|
||||
name="encryption"
|
||||
ref="encryption"
|
||||
:label="$t('label.encryption')">
|
||||
<a-switch
|
||||
v-model:checked="form.encryption"
|
||||
:checked="encryption"
|
||||
@change="val => { encryption = val }"/>
|
||||
:checked="form.encryption"/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="versioning" ref="versioning" :label="$t('label.versioning')">
|
||||
<a-switch
|
||||
v-model:checked="form.versioning"
|
||||
:checked="versioning"
|
||||
@change="val => { versioning = val }"/>
|
||||
: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"
|
||||
|
|
@ -48,8 +66,8 @@
|
|||
showSearch
|
||||
optionFilterProp="value"
|
||||
:filterOption="(input, option) => {
|
||||
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}" >
|
||||
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}">
|
||||
<a-select-option
|
||||
:value="policy"
|
||||
v-for="(policy,idx) in policyList"
|
||||
|
|
@ -57,13 +75,16 @@
|
|||
>{{ policy }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<div :span="24" class="action-button">
|
||||
<a-button @click="closeModal">{{ $t('label.cancel') }}</a-button>
|
||||
<a-button :loading="loading" type="primary" ref="submit" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
|
||||
</div>
|
||||
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, toRaw } from 'vue'
|
||||
import { postAPI } from '@/api'
|
||||
|
|
@ -80,10 +101,28 @@ export default {
|
|||
},
|
||||
data () {
|
||||
return {
|
||||
offerings: [],
|
||||
customDiskOffering: false,
|
||||
loading: false,
|
||||
customDiskOfferingIops: false
|
||||
policyList: ['Public', 'Private']
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isEcsObjectStore () {
|
||||
const r = this.resource || {}
|
||||
const provider = (
|
||||
r.providername ||
|
||||
r.objectstoreprovider ||
|
||||
r.objectStoreProvider ||
|
||||
r.objectStoreprovider ||
|
||||
r.provider ||
|
||||
''
|
||||
).toString().toUpperCase()
|
||||
return provider.includes('ECS')
|
||||
},
|
||||
showEncryption () {
|
||||
return !this.isEcsObjectStore
|
||||
},
|
||||
showObjectLocking () {
|
||||
return !this.isEcsObjectStore
|
||||
}
|
||||
},
|
||||
beforeCreate () {
|
||||
|
|
@ -91,15 +130,13 @@ export default {
|
|||
},
|
||||
created () {
|
||||
this.initForm()
|
||||
this.policyList = ['Public', 'Private']
|
||||
this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
initForm () {
|
||||
this.formRef = ref()
|
||||
this.form = reactive({})
|
||||
this.rules = reactive({
|
||||
})
|
||||
this.rules = reactive({})
|
||||
},
|
||||
fetchData () {
|
||||
this.loading = false
|
||||
|
|
@ -110,7 +147,6 @@ export default {
|
|||
this.loading = true
|
||||
Object.keys(this.apiParams).forEach(item => {
|
||||
const field = this.apiParams[item]
|
||||
let fieldValue = null
|
||||
let fieldName = null
|
||||
|
||||
if (field.type === 'list' || field.name === 'account') {
|
||||
|
|
@ -118,30 +154,36 @@ export default {
|
|||
} else {
|
||||
fieldName = field.name
|
||||
}
|
||||
fieldValue = this.resource[fieldName] ? this.resource[fieldName] : null
|
||||
if (fieldValue) {
|
||||
|
||||
const fieldValue = this.resource?.[fieldName]
|
||||
if (fieldValue !== null && fieldValue !== undefined) {
|
||||
form[field.name] = fieldValue
|
||||
}
|
||||
})
|
||||
this.loading = false
|
||||
},
|
||||
handleSubmit (e) {
|
||||
if (e?.preventDefault) e.preventDefault()
|
||||
if (this.loading) return
|
||||
this.formRef.value.validate().then(() => {
|
||||
const formRaw = toRaw(this.form)
|
||||
const values = this.handleRemoveFields(formRaw)
|
||||
|
||||
var data = {
|
||||
const data = {
|
||||
id: this.resource.id,
|
||||
quota: values.quota,
|
||||
encryption: values.encryption,
|
||||
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'),
|
||||
|
|
@ -149,16 +191,15 @@ export default {
|
|||
})
|
||||
this.closeModal()
|
||||
}).catch(error => {
|
||||
console.log(error)
|
||||
this.$notification.error({
|
||||
message: `${this.$t('label.bucket.update')} ${this.$t('label.error')}`,
|
||||
description: error.response.data.updatebucketresponse.errortext,
|
||||
description: error.response?.data?.updatebucketresponse?.errortext || 'Error',
|
||||
duration: 0
|
||||
})
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}).catch((error) => {
|
||||
}).catch(error => {
|
||||
this.formRef.value.scrollToField(error.errorFields[0].name)
|
||||
})
|
||||
},
|
||||
|
|
@ -169,10 +210,10 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-layout {
|
||||
width: 85vw;
|
||||
|
||||
@media (min-width: 760px) {
|
||||
width: 500px;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue