blob: fb83f8689143bd0dfd853f9ae581e417ebc46c7e [file] [log] [blame]
// 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.context;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.UUID;
import com.cloud.projects.Project;
import org.apache.log4j.Logger;
import org.apache.log4j.NDC;
import org.apache.cloudstack.managed.threadlocal.ManagedThreadLocal;
import com.cloud.exception.CloudAuthenticationException;
import com.cloud.user.Account;
import com.cloud.user.User;
import com.cloud.utils.UuidUtils;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.exception.CloudRuntimeException;
/**
* CallContext records information about the environment the call is made. This
* class must be always be available in all CloudStack code. Every thread
* entry point must set the context and remove it when the thread finishes.
*/
public class CallContext {
private static final Logger s_logger = Logger.getLogger(CallContext.class);
private static ManagedThreadLocal<CallContext> s_currentContext = new ManagedThreadLocal<CallContext>();
private static ManagedThreadLocal<Stack<CallContext>> s_currentContextStack = new ManagedThreadLocal<Stack<CallContext>>() {
@Override
protected Stack<CallContext> initialValue() {
return new Stack<CallContext>();
}
};
private String contextId;
private Account account;
private long accountId;
private long startEventId = 0;
private String eventDescription;
private String eventDetails;
private String eventType;
private boolean isEventDisplayEnabled = true; // default to true unless specifically set
private User user;
private long userId;
private final Map<Object, Object> context = new HashMap<Object, Object>();
private Project project;
static EntityManager s_entityMgr;
public static void init(EntityManager entityMgr) {
s_entityMgr = entityMgr;
}
protected CallContext() {
}
protected CallContext(long userId, long accountId, String contextId) {
this.userId = userId;
this.accountId = accountId;
this.contextId = contextId;
}
protected CallContext(User user, Account account, String contextId) {
this.user = user;
userId = user.getId();
this.account = account;
accountId = account.getId();
this.contextId = contextId;
}
public void putContextParameter(Object key, Object value) {
context.put(key, value);
}
/**
* @param key any not null key object
* @return the value of the key from context map
* @throws NullPointerException if the specified key is nul
*/
public Object getContextParameter(Object key) {
Object value = context.get(key);
//check if the value is present in the toString value of the key
//due to a bug in the way we update the key by serializing and deserializing, it sometimes gets toString value of the key. @see com.cloud.api.ApiAsyncJobDispatcher#runJob
if(value == null ) {
value = context.get(key.toString());
}
return value;
}
public long getCallingUserId() {
return userId;
}
public User getCallingUser() {
if (user == null) {
user = s_entityMgr.findById(User.class, userId);
}
return user;
}
public String getContextId() {
return contextId;
}
public Account getCallingAccount() {
if (account == null) {
account = s_entityMgr.findById(Account.class, accountId);
}
return account;
}
public static CallContext current() {
CallContext context = s_currentContext.get();
// TODO other than async job and api dispatches, there are many system background running threads
// that do not setup CallContext at all, however, many places in code that are touched by these background tasks
// assume not-null CallContext. Following is a fix to address therefore caused NPE problems
//
// There are security implications with this. It assumes that all system background running threads are
// indeed have no problem in running under system context.
//
if (context == null) {
context = registerSystemCallContextOnceOnly();
}
return context;
}
/**
* This method should only be called if you can propagate the context id
* from another CallContext.
*
* @param callingUser calling user
* @param callingAccount calling account
* @param contextId context id propagated from another call context
* @return CallContext
*/
public static CallContext register(User callingUser, Account callingAccount, String contextId) {
return register(callingUser, callingAccount, null, null, contextId);
}
protected static CallContext register(User callingUser, Account callingAccount, Long userId, Long accountId, String contextId) {
/*
Unit tests will have multiple times of setup/tear-down call to this, remove assertions to all unit test to run
assert s_currentContext.get() == null : "There's a context already so what does this new register context mean? " + s_currentContext.get().toString();
if (s_currentContext.get() != null) { // FIXME: This should be removed soon. I added this check only to surface all the places that have this problem.
throw new CloudRuntimeException("There's a context already so what does this new register context mean? " + s_currentContext.get().toString());
}
*/
CallContext callingContext = null;
if (userId == null || accountId == null) {
callingContext = new CallContext(callingUser, callingAccount, contextId);
} else {
callingContext = new CallContext(userId, accountId, contextId);
}
s_currentContext.set(callingContext);
NDC.push("ctx-" + UuidUtils.first(contextId));
if (s_logger.isTraceEnabled()) {
s_logger.trace("Registered: " + callingContext);
}
s_currentContextStack.get().push(callingContext);
return callingContext;
}
public static CallContext registerPlaceHolderContext() {
CallContext context = new CallContext(0, 0, UUID.randomUUID().toString());
s_currentContext.set(context);
s_currentContextStack.get().push(context);
return context;
}
public static CallContext register(User callingUser, Account callingAccount) {
return register(callingUser, callingAccount, UUID.randomUUID().toString());
}
public static CallContext registerSystemCallContextOnceOnly() {
try {
CallContext context = s_currentContext.get();
if (context == null) {
return register(null, null, User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM, UUID.randomUUID().toString());
}
assert context.getCallingUserId() == User.UID_SYSTEM : "You are calling a very specific method that registers a one time system context. This method is meant for background threads that does processing.";
return context;
} catch (Exception e) {
s_logger.error("Failed to register the system call context.", e);
throw new CloudRuntimeException("Failed to register system call context", e);
}
}
public static CallContext register(String callingUserUuid, String callingAccountUuid) {
Account account = s_entityMgr.findByUuid(Account.class, callingAccountUuid);
if (account == null) {
throw new CloudAuthenticationException("The account is no longer current.").add(Account.class, callingAccountUuid);
}
User user = s_entityMgr.findByUuid(User.class, callingUserUuid);
if (user == null) {
throw new CloudAuthenticationException("The user is no longer current.").add(User.class, callingUserUuid);
}
return register(user, account);
}
public static CallContext register(long callingUserId, long callingAccountId) throws CloudAuthenticationException {
Account account = s_entityMgr.findById(Account.class, callingAccountId);
if (account == null) {
throw new CloudAuthenticationException("The account is no longer current.").add(Account.class, Long.toString(callingAccountId));
}
User user = s_entityMgr.findById(User.class, callingUserId);
if (user == null) {
throw new CloudAuthenticationException("The user is no longer current.").add(User.class, Long.toString(callingUserId));
}
return register(user, account);
}
public static CallContext register(long callingUserId, long callingAccountId, String contextId) throws CloudAuthenticationException {
Account account = s_entityMgr.findById(Account.class, callingAccountId);
if (account == null) {
throw new CloudAuthenticationException("The account is no longer current.").add(Account.class, Long.toString(callingAccountId));
}
User user = s_entityMgr.findById(User.class, callingUserId);
if (user == null) {
throw new CloudAuthenticationException("The user is no longer current.").add(User.class, Long.toString(callingUserId));
}
return register(user, account, contextId);
}
public static void unregisterAll() {
while (unregister() != null) {
// NOOP
}
}
public static CallContext unregister() {
CallContext context = s_currentContext.get();
if (context == null) {
return null;
}
s_currentContext.remove();
if (s_logger.isTraceEnabled()) {
s_logger.trace("Unregistered: " + context);
}
String contextId = context.getContextId();
String sessionIdOnStack = null;
String sessionIdPushedToNDC = "ctx-" + UuidUtils.first(contextId);
while ((sessionIdOnStack = NDC.pop()) != null) {
if (sessionIdOnStack.isEmpty() || sessionIdPushedToNDC.equals(sessionIdOnStack)) {
break;
}
if (s_logger.isTraceEnabled()) {
s_logger.trace("Popping from NDC: " + contextId);
}
}
Stack<CallContext> stack = s_currentContextStack.get();
stack.pop();
if (!stack.isEmpty()) {
s_currentContext.set(stack.peek());
} else {
s_currentContext.set(null);
}
return context;
}
public void setStartEventId(long startEventId) {
this.startEventId = startEventId;
}
public long getStartEventId() {
return startEventId;
}
public long getCallingAccountId() {
return accountId;
}
public String getCallingAccountUuid() {
return getCallingAccount().getUuid();
}
public String getCallingUserUuid() {
return getCallingUser().getUuid();
}
public void setEventDetails(String eventDetails) {
this.eventDetails = eventDetails;
}
public String getEventDetails() {
return eventDetails;
}
public String getEventType() {
return eventType;
}
public void setEventType(String eventType) {
this.eventType = eventType;
}
public String getEventDescription() {
return eventDescription;
}
public void setEventDescription(String eventDescription) {
this.eventDescription = eventDescription;
}
public Project getProject() {
return this.project;
}
public void setProject(Project project) {
this.project = project;
}
/**
* Whether to display the event to the end user.
* @return true - if the event is to be displayed to the end user, false otherwise.
*/
public boolean isEventDisplayEnabled() {
return isEventDisplayEnabled;
}
public void setEventDisplayEnabled(boolean eventDisplayEnabled) {
isEventDisplayEnabled = eventDisplayEnabled;
}
public Map<Object, Object> getContextParameters() {
return context;
}
public void putContextParameters(Map<Object, Object> details){
if (details == null) return;
for(Map.Entry<Object,Object>entry : details.entrySet()){
putContextParameter(entry.getKey(), entry.getValue());
}
}
public static void setActionEventInfo(String eventType, String description) {
CallContext context = CallContext.current();
if (context != null) {
context.setEventType(eventType);
context.setEventDescription(description);
}
}
@Override
public String toString() {
return new StringBuilder("CCtxt[acct=").append(getCallingAccountId())
.append("; user=")
.append(getCallingUserId())
.append("; id=")
.append(contextId)
.append("]")
.toString();
}
}