/*
* Copyright 1999-2008 University of Chicago
*
* Licensed 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.globus.workspace.service.impls;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.globus.workspace.Counter;
import org.globus.workspace.Lager;
import org.globus.workspace.WorkspaceConstants;
import org.globus.workspace.WorkspaceException;
import org.globus.workspace.accounting.AccountingEventAdapter;
import org.globus.workspace.persistence.DataConvert;
import org.globus.workspace.persistence.PersistenceAdapter;
import org.globus.workspace.service.InstanceResource;
import org.globus.workspace.service.binding.BindNetwork;
import org.globus.workspace.service.binding.BindingAdapter;
import org.globus.workspace.service.binding.GlobalPolicies;
import org.globus.workspace.service.binding.authorization.CreationAuthorizationCallout;
import org.globus.workspace.service.binding.authorization.Decision;
import org.globus.workspace.service.binding.authorization.PostTaskAuthorization;
import org.globus.workspace.service.binding.vm.FileCopyNeed;
import org.globus.workspace.service.binding.vm.VirtualMachine;
import org.nimbustools.api.repr.ShutdownTasks;
import org.nimbustools.api.services.rm.AuthorizationException;
import org.nimbustools.api.services.rm.DestructionCallback;
import org.nimbustools.api.services.rm.DoesNotExistException;
import org.nimbustools.api.services.rm.ManageException;
import org.nimbustools.api.services.rm.OperationDisabledException;
import org.nimbustools.api.services.rm.StateChangeCallback;
import org.nimbustools.api.services.rm.CreationException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.DateFormat;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;
/**
* More organization (and docs) coming. This class is too tightly coupled.
*/
public abstract class InstanceResourceImpl implements InstanceResource {
// -------------------------------------------------------------------------
// STATIC VARIABLES
// -------------------------------------------------------------------------
private static final Log logger =
LogFactory.getLog(InstanceResourceImpl.class.getName());
private static DateFormat localFormat = DateFormat.getDateTimeInstance();
// -------------------------------------------------------------------------
// INSTANCE VARIABLES
// -------------------------------------------------------------------------
protected final PersistenceAdapter persistence;
protected final BindingAdapter binding;
protected final BindNetwork bindNetwork;
protected final GlobalPolicies globals;
protected final DataConvert dataConvert;
protected final Lager lager;
protected final Counter pendingNotifications = new Counter(0, null);
protected final List destructionListeners = new LinkedList();
protected final List stateListeners = new LinkedList();
// optionally set
protected AccountingEventAdapter accounting;
protected CreationAuthorizationCallout authzCallout;
protected int id = -1;
protected String name;
protected VirtualMachine vm;
protected String ensembleId;
protected String groupId;
protected boolean lastInGroup;
protected int groupSize = 1;
protected boolean partOfGroupRequest;
protected int launchIndex;
protected Calendar terminationTime;
protected Calendar startTime;
protected volatile int state = WorkspaceConstants.STATE_INVALID;
protected volatile int targetState = WorkspaceConstants.STATE_INVALID;
protected boolean opsEnabled;
protected boolean vmmAccessOK = true;
protected volatile Throwable throwableForState;
// currently, mgmt policy limited to one entity, the create() caller
protected String creatorID;
protected String clientToken;
protected double chargeRatio = 1.0;
// -------------------------------------------------------------------------
// CONSTRUCTOR
// -------------------------------------------------------------------------
protected InstanceResourceImpl(PersistenceAdapter persistenceImpl,
BindingAdapter bindingImpl,
GlobalPolicies globalsImpl,
DataConvert dataConvertImpl,
Lager lagerImpl,
BindNetwork bindNetworkImpl) {
if (lagerImpl == null) {
throw new IllegalArgumentException("lagerImpl may not be null");
}
this.lager = lagerImpl;
if (persistenceImpl == null) {
throw new IllegalArgumentException(
"persistenceImpl may not be null");
}
this.persistence = persistenceImpl;
if (bindingImpl == null) {
throw new IllegalArgumentException("bindingImpl may not be null");
}
this.binding = bindingImpl;
if (globalsImpl == null) {
throw new IllegalArgumentException("globalsImpl may not be null");
}
this.globals = globalsImpl;
if (dataConvertImpl == null) {
throw new IllegalArgumentException("da may not be null");
}
this.dataConvert = dataConvertImpl;
if (bindNetworkImpl == null) {
throw new IllegalArgumentException("bindNetworkImpl may not be null");
}
this.bindNetwork = bindNetworkImpl;
}
// -------------------------------------------------------------------------
// OPTIONAL MODULES SETTERS
// -------------------------------------------------------------------------
public void setAuthzCallout(CreationAuthorizationCallout callout) {
this.authzCallout = callout;
}
public void setAccountingEventAdapter(AccountingEventAdapter events) {
this.accounting = events;
}
// -------------------------------------------------------------------------
// CREATION
// -------------------------------------------------------------------------
public Calendar getStartTime() {
return this.startTime;
}
public void setStartTime(Calendar start) {
this.startTime = start;
}
/**
* Only called when this resource is created.
*
* @param id id#
* @param vm instantiation details
* @param startTime start, can be null
* @param termTime resource destruction time, can be null which mean either
* "never" or "no setting" (while using best-effort sched)
* @param node assigned node, can be null
* @param chargeRatio ratio to compute the real minutes charge, typically <= 1.0 and > 0
* @throws CreationException problem
*/
public void populate(int id,
VirtualMachine vm,
Calendar startTime,
Calendar termTime,
String node,
double chargeRatio) throws CreationException {
if (vm == null) {
throw new CreationException("null vm");
}
if (vm.getDeployment() == null) {
throw new CreationException("null deployment information");
}
this.id = id;
this.vm = vm;
this.chargeRatio = chargeRatio;
// could be null if best effort, infinite for timebeing
this.terminationTime = termTime;
// could be null if best effort
this.startTime = startTime;
this.name = vm.getName();
this.setInitialState(WorkspaceConstants.STATE_FIRST_LEGAL, null);
this.setInitialTargetState(
this.vm.getDeployment().getRequestedState());
this.vm.setNode(node);
if (lager.traceLog) {
String msg = "populating " + Lager.id(this.id) + ", resource " +
"termination time = ";
if (this.terminationTime != null) {
msg += localFormat.format(this.terminationTime.getTime());
} else {
msg += "not set";
}
logger.trace(msg);
}
}
// -------------------------------------------------------------------------
// GENERAL INFORMATION
// -------------------------------------------------------------------------
public int getID() {
return this.id;
}
public void setID(int idnum) {
this.id = idnum;
}
public String getName() {
return this.name;
}
public void setName(String resourceName) {
this.name = resourceName;
}
public String getEnsembleId() {
return this.ensembleId;
}
public void setEnsembleId(String ensembleId) {
this.ensembleId = ensembleId;
}
public String getGroupId() {
return this.groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public boolean isLastInGroup() {
return this.lastInGroup;
}
public void setLastInGroup(boolean lastInGroup) {
this.lastInGroup = lastInGroup;
}
public int getGroupSize() {
return this.groupSize;
}
public void setGroupSize(int groupSize) {
this.groupSize = groupSize;
}
public String getCreatorID() {
return this.creatorID;
}
public void setCreatorID(String ID) {
this.creatorID = ID;
}
public String getClientToken() {
return clientToken;
}
public void setClientToken(String clientToken) {
this.clientToken = clientToken;
}
public void setChargeRatio(double chargeRatio) {
if (chargeRatio < 0) {
throw new IllegalArgumentException("charge ratio can not be < 0");
}
this.chargeRatio = chargeRatio;
}
public double getChargeRatio() {
return this.chargeRatio;
}
public VirtualMachine getVM() {
return this.vm;
}
public boolean isPropagateRequired() {
return this.vm.isPropagateRequired();
}
public boolean isUnPropagateRequired() {
return this.vm.isUnPropagateRequired();
}
public boolean isPropagateStartOK() {
return this.vm.isPropagateStartOK();
}
public boolean isVMMaccessOK() {
return this.vmmAccessOK;
}
public void setVMMaccessOK(boolean accessOK) {
this.vmmAccessOK = accessOK;
try {
this.persistence.setVMMaccessOK(this.id, accessOK);
} catch (ManageException e) {
logger.error("",e);
}
}
public void setInitialVMMaccessOK(boolean accessOK) {
this.vmmAccessOK = accessOK;
}
public synchronized void newFileCopyNeed(FileCopyNeed need) {
if (this.vm == null) {
throw new IllegalStateException("vm is null");
}
this.vm.addFileCopyNeed(need);
try {
this.persistence.addCustomizationNeed(this.id, need);
} catch (ManageException e) {
logger.error("", e); // TODO
}
}
public synchronized void newHostname(String hostname) {
if (this.vm == null) {
logger.fatal(Lager.id(this.id) +
": could not set hostname, no vm");
return;
}
this.vm.setNode(hostname);
try {
this.persistence.setHostname(this.id, hostname);
} catch (ManageException e) {
logger.error("", e);
}
}
public synchronized void newStartTime(Calendar startTime) {
this.setStartTime(startTime);
try {
this.persistence.setStartTime(this.id, startTime);
} catch (ManageException e) {
logger.error("", e);
}
}
public synchronized void newStopTime(Calendar stopTime) {
// this only in fact sets term time, scheduler is the only thing
// that currently needs stop time, but todo: stop should be stored
// in workspace resource for future querying from client (since
// with best effort scheduler it is not always known at create time
// and therefore returned to client)
try {
stopTime.add(Calendar.SECOND,
this.globals.getTerminationOffsetSeconds());
} catch (Exception e) {
logger.error(e.getMessage(), e);
return;
}
this.terminationTime = stopTime;
try {
this.persistence.setTerminationTime(this.id, stopTime);
} catch (ManageException e) {
logger.error("", e);
}
}
public synchronized void newNetwork(String network) {
if (this.vm == null) {
logger.fatal(Lager.id(this.id) +
": could not set network, no vm");
return;
}
this.vm.setNetwork(network);
try {
this.persistence.setNetwork(this.id, network);
} catch (ManageException e) {
logger.error("", e);
}
}
public boolean isPartOfGroupRequest() {
return this.partOfGroupRequest;
}
public void setPartOfGroupRequest(boolean isInGroupRequest) {
this.partOfGroupRequest = isInGroupRequest;
}
public int getLaunchIndex() {
return this.launchIndex;
}
public void setLaunchIndex(int launchIndex) {
this.launchIndex = launchIndex;
}
// -------------------------------------------------------------------------
// REMOTE OPERATION POLICY
// -------------------------------------------------------------------------
/**
* @return true if client is permitted to invoke WS operations
* other than destroy
*/
public boolean isOpsEnabled() {
return this.opsEnabled;
}
/**
* For creating the object in the first place; setOpsEnabled calls
* persistenceAdapter.setOpsEnabled() - but entry for id does not
* exist yet when setInitialOpsEnabled() is needed.
*
* @see PersistenceAdapter#load(int, org.globus.workspace.service.InstanceResource) )
* @param enabled true if enabled
*/
public void setInitialOpsEnabled(boolean enabled) {
this.opsEnabled = enabled;
}
/**
* For regular changes during the life of the workspace.
*
* @param enabled true if enabled
*/
public synchronized void setOpsEnabled(boolean enabled) {
if (enabled != this.opsEnabled) {
this.opsEnabled = enabled;
try {
this.persistence.setOpsEnabled(this.id, enabled);
} catch (ManageException e) {
logger.error("",e);
}
if (lager.eventLog) {
String msg = "disabled";
if (enabled) {
msg = "enabled";
}
logger.info(Lager.ev(this.id) + " WS-operations " + msg);
}
}
}
// -------------------------------------------------------------------------
// REMOTE CLIENT MANIPULATION
// -------------------------------------------------------------------------
public void start() throws ManageException, OperationDisabledException {
if (this.isOpsEnabled()) {
start(WorkspaceConstants.STATE_STARTED);
} else {
throw new OperationDisabledException(
"start is currently disabled");
}
}
private void start(int target) throws ManageException {
if (lager.traceLog) {
logger.trace("_start(): " + Lager.id(this.id) +
"current state is " +
this.dataConvert.stateName(this.state) +
", current target is " +
this.dataConvert.stateName(this.targetState) +
", target is " +
this.dataConvert.stateName(target));
}
this.setTargetState(target);
}
public void shutdown(ShutdownTasks tasks)
throws ManageException, OperationDisabledException {
if (this.isOpsEnabled()) {
this.handlePostTasks(tasks, false);
this._shutdown(WorkspaceConstants.STATE_PROPAGATED);
} else {
throw new OperationDisabledException(
"Shutdown is currently disabled");
}
}
public void shutdownSave(ShutdownTasks tasks)
throws ManageException, OperationDisabledException {
if (this.isOpsEnabled()) {
this.handlePostTasks(tasks, true);
this._shutdown(WorkspaceConstants.STATE_READY_FOR_TRANSPORT);
} else {
throw new OperationDisabledException(
"ReadyForTransport (shutdown-save) is currently disabled");
}
}
public void pause(ShutdownTasks tasks)
throws ManageException, OperationDisabledException {
if (this.isOpsEnabled()) {
this.handlePostTasks(tasks, false);
this._shutdown(WorkspaceConstants.STATE_PAUSED);
} else {
throw new OperationDisabledException(
"Pause is currently disabled");
}
}
public void serialize(ShutdownTasks tasks)
throws ManageException, OperationDisabledException {
if (this.isOpsEnabled()) {
this.handlePostTasks(tasks, false);
this._shutdown(WorkspaceConstants.STATE_SERIALIZED);
} else {
throw new OperationDisabledException(
"Serialize is currently disabled");
}
}
public void reboot(ShutdownTasks tasks)
throws ManageException, OperationDisabledException {
if (this.isOpsEnabled()) {
this.handlePostTasks(tasks, false);
this._shutdown(WorkspaceConstants.STATE_REBOOT);
} else {
throw new OperationDisabledException(
"Reboot is currently disabled");
}
}
private void handlePostTasks(ShutdownTasks tasks,
boolean isReadyForTransport)
throws ManageException {
if (tasks == null) {
return; // *** EARLY RETURN ***
}
URI target = tasks.getBaseFileUnpropagationTarget();
if (target == null) {
return; // *** EARLY RETURN ***
}
// technically possible but only someone else's client implementation
// would allow this to happen
if (!isReadyForTransport) {
final String err = "Post-shutdown tasks are only compatible " +
"with a ReadyForTransport request.";
throw new ManageException(err);
}
if (this.authzCallout != null
&& this.authzCallout instanceof PostTaskAuthorization) {
// shortcut: currently we know owner is the caller at this point
final String dnToAuthorize = this.creatorID;
final Integer decision;
String newTargetName;
try {
decision = ((PostTaskAuthorization)this.authzCallout).
isRootPartitionUnpropTargetPermitted(target, dnToAuthorize);
} catch (AuthorizationException e) {
throw new ManageException(e.getMessage(), e);
}
if (!Decision.PERMIT.equals(decision)) {
throw new ManageException(
"request denied, no message for client");
}
}
final String trueTarget;
if (tasks.isAppendID()) {
trueTarget = target.toASCIIString() + "-" + this.id;
// check new uri syntax:
try {
new URI(trueTarget);
} catch (URISyntaxException e) {
throw new ManageException(e.getMessage(), e);
}
} else {
trueTarget = target.toASCIIString();
}
this.vm.overrideRootUnpropTarget(trueTarget, logger);
if (!this.isUnPropagateRequired()) {
this.vm.setUnPropagateRequired(true);
}
this.persistence.setRootUnpropTarget(this.id, trueTarget);
}
private void _shutdown(int target) throws ManageException {
if (lager.traceLog) {
logger.trace("shutdown(): " + Lager.id(this.id) + ", " +
"target state = " + target);
}
try {
this.setTargetState(target);
} catch (Exception e) {
final String err = "problem shutting down " + Lager.id(this.id) +
": " + e.getMessage();
logger.error(err, e);
// setting to corrupted and/or deciding to destroy depends on the
// situation and has already happened via the state machine
throw new ManageException(err, e);
}
}
// -------------------------------------------------------------------------
// LISTENERS
// -------------------------------------------------------------------------
public void registerStateChangeListener(StateChangeCallback listener) {
if (listener == null) {
logger.warn("Something sent null state change listener, ignoring");
return; // *** EARLY RETURN ***
}
// re-registrations expected after a container crash
synchronized(this.stateListeners) {
this.stateListeners.add(listener);
}
}
protected void expungeStateListeners() {
this.stateListeners.clear();
}
public void registerDestructionListener(DestructionCallback listener) {
if (listener == null) {
logger.warn("Something sent null destruction listener, ignoring");
return; // *** EARLY RETURN ***
}
// re-registrations expected after a container crash
synchronized(this.destructionListeners) {
this.destructionListeners.add(listener);
}
}
protected void resourceDestroyed() {
this.expungeStateListeners();
// todo: assuming listeners will quickly return from the notification,
// should not assume that about all future messaging layers, so in the
// future launch a separate thread to then make the callbacks (there
// are many articles about this to consult)
synchronized(this.destructionListeners) {
for (int i = 0; i < this.destructionListeners.size(); i++) {
try {
final DestructionCallback dc =
(DestructionCallback) this.destructionListeners.get(i);
if (dc != null) {
dc.destroyed();
}
} catch (Throwable t) {
final String err = "Problem with asynchronous " +
"destruction notification: " + t.getMessage();
logger.error(err, t);
}
}
// never happens again
this.destructionListeners.clear();
}
}
// -------------------------------------------------------------------------
// REMOVAL
// -------------------------------------------------------------------------
/**
* Called when termination time passes or if client invoked destroy
* operation.
*
* This invokes all it can to remove all traces of the workspace,
* including cancelling any current transfers or calling shutdown +
* trash on the backend for example.
*
* It currently blocks until finished.
*
* Don't call unless you are managing the instance cache (or not using
* one, perhaps).
*
* @return true if remove succeeded
* @throws ManageException problem with removal
*/
public synchronized boolean remove() throws ManageException {
if (this.lager.eventLog || this.lager.traceLog) {
if (this.lager.eventLog) {
logger.info(Lager.ev(this.id) + "destroy begins");
} else if (this.lager.traceLog) {
logger.trace(Lager.id(this.id) + "destroy begins");
}
}
this.setTargetStateUnderLockEvaluate(WorkspaceConstants.STATE_DESTROYING);
if (this.getState() != WorkspaceConstants.STATE_DESTROY_SUCCEEDED) {
logger.debug(Lager.id(this.id) + " destroy failed (state " + this.getState() + ")");
return false;
}
if (this.lager.eventLog) {
logger.info(Lager.ev(this.id) +
"destroyed ('" + this.name + "')");
} else if (this.lager.traceLog) {
logger.trace(Lager.id(this.id) +
" destroyed ('" + this.name + "')");
}
// inform any destruction listeners:
this.resourceDestroyed();
return true;
}
public synchronized void cleanup() {
do_remove();
}
protected void do_remove() {
// scheduler already notified by doStateChange()
logger.debug("removing " + Lager.id(this.id));
// notify accounting
if (this.accounting == null) {
if (lager.accounting) {
logger.debug("accounting not available, " +
"no stop event generated");
}
} else {
final String network;
final String resource;
if (this.vm != null) {
network = this.vm.getNetwork();
resource = this.vm.getNode();
} else {
network = null;
resource = null;
}
if (this.startTime == null) {
if (lager.accounting) {
logger.debug("no start time available (will happen with " +
"best effort scheduler with destruction coming " +
"before any resource assignment");
}
this.accounting.destroy(this.id,
this.getCreatorID(),
0L,
this.chargeRatio);
} else {
final long runningTimeMS =
Calendar.getInstance().getTimeInMillis() -
this.startTime.getTimeInMillis();
// convert milliseconds to minutes, take ceiling
long runningTime = runningTimeMS / 60000L;
if (runningTimeMS % 60000L > 0L) {
runningTime += 1L;
}
this.accounting.destroy(this.id,
this.getCreatorID(),
runningTime,
this.chargeRatio);
}
}
try {
logger.debug("backing out allocations for " + Lager.id(this.id));
this.bindNetwork.backOutIPAllocations(vm);
//this.binding.backOutAllocations(this.vm);
} catch (WorkspaceException e) {
// candidate for admin log/trigger of severe issues
final String err = "error retiring allocations, " +
Lager.id(this.id);
logger.error(err, e);
}
try {
this.persistence.remove(this.id, this);
} catch (Throwable t) {
// nothing more we can really do about this
// candidate for admin log/trigger of severe issues
logger.fatal("error removing " + Lager.id(this.id)
+ " from persistence layer: " + t.getMessage(), t);
}
// After remove is done, home notifies subscribers of termination
}
public void load(String key) throws ManageException,
DoesNotExistException {
final int keyInt;
try {
keyInt = Integer.parseInt(key);
} catch (Throwable t) {
final String err = "invalid key: " + key;
throw new ManageException(err, t);
}
this.id = keyInt;
if (lager.traceLog) {
logger.trace("load(): " + Lager.id(this.id));
}
this.persistence.load(keyInt, this);
}
public Calendar getTerminationTime() {
return this.terminationTime;
}
public void setTerminationTime(Calendar time) {
this.terminationTime = time;
}
}