当前位置: 首页 > 工具软件 > OpenStack4j > 使用案例 >

openstack4j 源码_【踩坑】OpenStack4j使用过程中关于OSClientSession被更改的问题记录...

袁鸿畴
2023-12-01

OpenStack4j是一个OpenStack的Java SDK。

问题描述

在同一个代码处理线程中,首先获取了 projectA 的 OSClient 对象 OSClientA,然后又获取了 projectB 的 OSClient 对象 OSClientB。

后续在用 OSClientA 去调用某个 service(比如 BlockVolumeService)去创建资源(比如 volume)的时候,期望创建在 projectA 下面,结果创建的资源却在 projectB 下面。

查找原因

经过跟踪 OpenStack4j 中获取 OSClient 和 调用具体 service 的相关源码后,发现问题,在于 OSClientSession 类中使用 ThreadLocal 变量 sessions 将获取的 OSClient 存下来。

后续在创建资源的时候,使用从 sessions 中取出的OSClient ,调用 OpenStack 的 API 接口。

关键在于,第二次获取 OSClientB 的时候,会将 sessions 中存的 OSClient 更新,将原先的 OSClientA 给替换为 OSClientB。

也就造成了,尽管是用 OSClientA 去创建资源,但是实际使用的 OSClient 已经被改了,也就是是用 OSClientB 的相关参数去创建的。

源码分析

获取 OSClient 的代码在 OSAuthenticator#authenticateV3(默认使用的是v3版本)。

......

private static OSClientV3 authenticateV3(KeystoneAuth auth, SessionInfo info, Config config) {

if (auth.getType().equals(Type.TOKENLESS)){

......

}

# 调用 OpenStack keystone 的认证接口

HttpRequest request = HttpRequest.builder(KeystoneToken.class)

.header(ClientConstants.HEADER_OS4J_AUTH, TOKEN_INDICATOR).endpoint(info.endpoint)

.method(HttpMethod.POST).path("/auth/tokens").config(config).entity(auth).build();

HttpResponse response = HttpExecutor.create().execute(request);

if (response.getStatus() >= 400) {

try {

throw mapException(response.getStatusMessage(), response.getStatus());

} finally {

HttpEntityHandler.closeQuietly(response);

}

}

KeystoneToken token = response.getEntity(KeystoneToken.class);

token.setId(response.header(ClientConstants.HEADER_X_SUBJECT_TOKEN));

.......

String reqId = response.header(ClientConstants.X_OPENSTACK_REQUEST_ID);

# info.reLinkToExistingSession 在前面的调用过程中传参是 false。

if (!info.reLinkToExistingSession) {

# 创建了一个 OSClient,OSClientSessionV3 是 v3 版本的实现类

OSClientSessionV3 v3 = OSClientSessionV3.createSession(token, info.perspective, info.provider, config);

v3.reqId = reqId;

return v3;

}

OSClientSessionV3 current = (OSClientSessionV3) OSClientSessionV3.getCurrent();

current.token = token;

current.reqId = reqId;

return current;

}

OSClientSessionV3#createSession,也是关键的地方。

public static OSClientSessionV3 createSession(Token token, Facing perspective, CloudProvider provider, Config config) {

return new OSClientSessionV3(token, token.getEndpoint(), perspective, provider, config);

}

......

public static class OSClientSessionV3 extends OSClientSession implements OSClientV3 {

Token token;

protected String reqId;

private OSClientSessionV3(Token token, String endpoint, Facing perspective, CloudProvider provider, Config config) {

this.token = token;

this.config = config;

this.perspective = perspective;

this.provider = provider;

# 重点在这里

sessions.set(this);

}

......

接着看一下 sessions 这个变量

public abstract class OSClientSession> implements EndpointTokenProvider {

private static final Logger LOG = LoggerFactory.getLogger(OSClientSession.class);

# 可以看到 sessions 是一个 ThreadLocal 变量,而每一次创建新的 OSClientSession,会调用 set 方法,

# 覆盖之前的 OSClientSession。

@SuppressWarnings("rawtypes")

private static final ThreadLocal sessions = new ThreadLocal();

Config config;

Facing perspective;

String region;

Set supports;

CloudProvider provider;

Map headers;

EndpointURLResolver fallbackEndpointUrlResolver = new DefaultEndpointURLResolver();

由以上的源码知道,每次获取 OSClient(即创建新的OSClientSessionV3)的时候,会在 sessions 中覆盖之前的。

看完获取的过程,再去确认一下调用具体 service 创建资源的时候,是否是从 sessions 中取出的 OSClient。

无论哪个 service 中方法,最终都是使用统一的 http 调用方法 HttpExecutorServiceImpl#invokeRequest。

......

private HttpResponse invokeRequest(HttpCommand command) throws Exception {

Response response = command.execute();

if (command.getRetries() == 0 && response.getStatus() == 401 && !command.getRequest().getHeaders().containsKey(ClientConstants.HEADER_OS4J_AUTH))

{

# 重点看这个方法的实现,同样是 OSAuthenticator 类中的

OSAuthenticator.reAuthenticate();

command.getRequest().getHeaders().put(ClientConstants.HEADER_X_AUTH_TOKEN, OSClientSession.getCurrent().getTokenId());

return invokeRequest(command.incrementRetriesAndReturn());

}

return HttpResponseImpl.wrap(response);

}

OSAuthenticator.reAuthenticate()

/**

* Re-authenticates/renews the token for the current Session

*/

@SuppressWarnings("rawtypes")

public static void reAuthenticate() {

LOG.debug("Re-Authenticating session due to expired Token or invalid response");

OSClientSession session = OSClientSession.getCurrent();

switch (session.getAuthVersion()) {

case V2:

KeystoneAccess access = ((OSClientSessionV2) session).getAccess().unwrap();

SessionInfo info = new SessionInfo(access.getEndpoint(), session.getPerspective(), true,

session.getProvider());

Auth auth = (Auth) ((access.isCredentialType()) ? access.getCredentials() : access.getTokenAuth());

authenticateV2((org.openstack4j.openstack.identity.v2.domain.Auth) auth, info, session.getConfig());

break;

case V3:

default:

Token token = ((OSClientSessionV3) session).getToken();

info = new SessionInfo(token.getEndpoint(), session.getPerspective(), true, session.getProvider());

# 从 sessions 中获取 OSClientSessionV3 之后,同样调用 authenticateV3 认证

authenticateV3((KeystoneAuth) token.getCredentials(), info, session.getConfig());

break;

}

}

虽然和获取 OSClient 的时候一样,都调用了OSAuthenticator#authenticateV3。但是需要注意,上面说到 info.reLinkToExistingSession 这个参数在获取的时候传参为 false,而这里的传参是 true。

代表它会重新连接已经存在的 Session。

......

private static OSClientV3 authenticateV3(KeystoneAuth auth, SessionInfo info, Config config) {

......

# info.reLinkToExistingSession 在这里传参是 true,所以不会再创建新的。

if (!info.reLinkToExistingSession) {

# 创建了一个 OSClient,OSClientSessionV3 是 v3 版本的实现类

OSClientSessionV3 v3 = OSClientSessionV3.createSession(token, info.perspective, info.provider, config);

v3.reqId = reqId;

return v3;

}

# 取出当前的 OSClient,直接返回。

OSClientSessionV3 current = (OSClientSessionV3) OSClientSessionV3.getCurrent();

current.token = token;

current.reqId = reqId;

return current;

}

结论

从上面的源码分析结合我的问题可以得知,在 OSAuthenticator.reAuthenticate() 取出的当前的 OSClient 是 OSClientB,而不是 OSClientA,所以导致了资源创建在了 projectB 下面。

分享一下自己踩坑的问题分析,希望大家都可以及时发现并避免因此出现意想不到的 Bug。

 类似资料: