Skip to content

Commit 6019139

Browse files
author
MHK
committed
Update ECS Plugin: address reviewer comments for ECS Plugin
1 parent a10e06e commit 6019139

File tree

6 files changed

+931
-688
lines changed

6 files changed

+931
-688
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.apache.cloudstack.storage.datastore.driver;
2+
3+
public final class EcsConstants {
4+
private EcsConstants() {}
5+
6+
// Object store details keys
7+
public static final String MGMT_URL = "mgmt_url";
8+
public static final String SA_USER = "sa_user";
9+
public static final String SA_PASS = "sa_password";
10+
public static final String NAMESPACE = "namespace";
11+
public static final String INSECURE = "insecure";
12+
public static final String S3_HOST = "s3_host";
13+
public static final String USER_PREFIX = "user_prefix";
14+
public static final String DEFAULT_USER_PREFIX = "cs-";
15+
16+
// Per-account keys
17+
public static final String AD_KEY_ACCESS = "ecs.accesskey";
18+
public static final String AD_KEY_SECRET = "ecs.secretkey";
19+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package org.apache.cloudstack.storage.datastore.driver;
2+
3+
import java.nio.charset.StandardCharsets;
4+
import java.util.Objects;
5+
import java.util.concurrent.ConcurrentHashMap;
6+
7+
import org.apache.http.auth.UsernamePasswordCredentials;
8+
import org.apache.http.client.methods.CloseableHttpResponse;
9+
import org.apache.http.client.methods.HttpGet;
10+
import org.apache.http.impl.auth.BasicScheme;
11+
import org.apache.http.impl.client.CloseableHttpClient;
12+
import org.apache.http.util.EntityUtils;
13+
14+
import com.cloud.utils.exception.CloudRuntimeException;
15+
16+
public class EcsMgmtTokenManager {
17+
private static final long DEFAULT_TOKEN_MAX_AGE_SEC = 300;
18+
private static final long EXPIRY_SKEW_SEC = 30;
19+
20+
private static final ConcurrentHashMap<TokenKey, TokenEntry> TOKEN_CACHE = new ConcurrentHashMap<>();
21+
private static final ConcurrentHashMap<TokenKey, Object> TOKEN_LOCKS = new ConcurrentHashMap<>();
22+
23+
static final class EcsUnauthorizedException extends RuntimeException {
24+
EcsUnauthorizedException(final String msg) { super(msg); }
25+
}
26+
27+
@FunctionalInterface
28+
public interface WithToken<T> { T run(String token) throws Exception; }
29+
30+
private static final class TokenKey {
31+
final String mgmtUrl;
32+
final String user;
33+
TokenKey(final String mgmtUrl, final String user) {
34+
this.mgmtUrl = mgmtUrl;
35+
this.user = user;
36+
}
37+
@Override public boolean equals(final Object o) {
38+
if (this == o) return true;
39+
if (!(o instanceof TokenKey)) return false;
40+
final TokenKey k = (TokenKey) o;
41+
return Objects.equals(mgmtUrl, k.mgmtUrl) && Objects.equals(user, k.user);
42+
}
43+
@Override public int hashCode() { return Objects.hash(mgmtUrl, user); }
44+
}
45+
46+
private static final class TokenEntry {
47+
final String token;
48+
final long expiresAtMs;
49+
TokenEntry(final String token, final long expiresAtMs) {
50+
this.token = token;
51+
this.expiresAtMs = expiresAtMs;
52+
}
53+
boolean validNow() {
54+
return token != null && !token.isBlank() && System.currentTimeMillis() < expiresAtMs;
55+
}
56+
}
57+
58+
public <T> T callWithRetry401(final EcsObjectStoreDriverImpl.EcsCfg cfg,
59+
final WithToken<T> op,
60+
final HttpClientFactory httpFactory) throws Exception {
61+
try {
62+
return op.run(getAuthToken(cfg, httpFactory));
63+
} catch (EcsUnauthorizedException u) {
64+
invalidate(cfg);
65+
return op.run(getAuthToken(cfg, httpFactory));
66+
}
67+
}
68+
69+
public void invalidate(final EcsObjectStoreDriverImpl.EcsCfg cfg) {
70+
TOKEN_CACHE.remove(new TokenKey(trimTail(cfg.mgmtUrl), cfg.saUser));
71+
}
72+
73+
public String getAuthToken(final EcsObjectStoreDriverImpl.EcsCfg cfg,
74+
final HttpClientFactory httpFactory) {
75+
final String mu = trimTail(cfg.mgmtUrl);
76+
final TokenKey key = new TokenKey(mu, cfg.saUser);
77+
78+
final TokenEntry cached = TOKEN_CACHE.get(key);
79+
if (cached != null && cached.validNow()) return cached.token;
80+
81+
final Object lock = TOKEN_LOCKS.computeIfAbsent(key, k -> new Object());
82+
synchronized (lock) {
83+
final TokenEntry cached2 = TOKEN_CACHE.get(key);
84+
if (cached2 != null && cached2.validNow()) return cached2.token;
85+
86+
final TokenEntry fresh = loginAndGetTokenFresh(mu, cfg.saUser, cfg.saPass, cfg.insecure, httpFactory);
87+
TOKEN_CACHE.put(key, fresh);
88+
return fresh.token;
89+
}
90+
}
91+
92+
private TokenEntry loginAndGetTokenFresh(final String mgmtUrl,
93+
final String user,
94+
final String pass,
95+
final boolean insecure,
96+
final HttpClientFactory httpFactory) {
97+
try (CloseableHttpClient http = httpFactory.build(insecure)) {
98+
final HttpGet get = new HttpGet(mgmtUrl + "/login");
99+
UsernamePasswordCredentials creds = new UsernamePasswordCredentials(user, pass);
100+
get.addHeader(new BasicScheme().authenticate(creds, get, null));
101+
102+
try (CloseableHttpResponse resp = http.execute(get)) {
103+
final int status = resp.getStatusLine().getStatusCode();
104+
if (status != 200 && status != 201) {
105+
final String body = resp.getEntity() != null
106+
? EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8)
107+
: "";
108+
throw new CloudRuntimeException("ECS /login failed: HTTP " + status + " body=" + body);
109+
}
110+
if (resp.getFirstHeader("X-SDS-AUTH-TOKEN") == null) {
111+
throw new CloudRuntimeException("ECS /login did not return X-SDS-AUTH-TOKEN header");
112+
}
113+
114+
final String token = resp.getFirstHeader("X-SDS-AUTH-TOKEN").getValue();
115+
116+
long maxAgeSec = DEFAULT_TOKEN_MAX_AGE_SEC;
117+
try {
118+
if (resp.getFirstHeader("X-SDS-AUTH-MAX-AGE") != null) {
119+
maxAgeSec = Long.parseLong(resp.getFirstHeader("X-SDS-AUTH-MAX-AGE").getValue().trim());
120+
}
121+
} catch (Exception ignore) { }
122+
123+
final long effectiveSec = Math.max(5, maxAgeSec - EXPIRY_SKEW_SEC);
124+
final long expiresAtMs = System.currentTimeMillis() + (effectiveSec * 1000L);
125+
return new TokenEntry(token, expiresAtMs);
126+
}
127+
} catch (Exception e) {
128+
throw new CloudRuntimeException("Failed to obtain ECS auth token: " + e.getMessage(), e);
129+
}
130+
}
131+
132+
private static String trimTail(final String s) {
133+
if (s == null) return null;
134+
return s.endsWith("/") ? s.substring(0, s.length() - 1) : s;
135+
}
136+
137+
/** Simple seam for testability; implemented by the driver using its existing buildHttpClient(). */
138+
@FunctionalInterface
139+
public interface HttpClientFactory {
140+
CloseableHttpClient build(boolean insecure);
141+
}
142+
}

0 commit comments

Comments
 (0)