// 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(); } }