package com.sixsq.slipstream.run;
/*
* +=================================================================+
* SlipStream Server (WAR)
* =====
* Copyright (C) 2013 SixSq Sarl (sixsq.com)
* =====
* 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.
* -=================================================================-
*/
import com.sixsq.slipstream.exceptions.CannotAdvanceFromTerminalStateException;
import com.sixsq.slipstream.exceptions.InvalidStateException;
import com.sixsq.slipstream.exceptions.NotFoundException;
import com.sixsq.slipstream.exceptions.SlipStreamClientException;
import com.sixsq.slipstream.exceptions.SlipStreamDatabaseException;
import com.sixsq.slipstream.exceptions.ValidationException;
import com.sixsq.slipstream.metrics.Metrics;
import com.sixsq.slipstream.metrics.MetricsTimer;
import com.sixsq.slipstream.persistence.PersistenceUtil;
import com.sixsq.slipstream.persistence.Run;
import com.sixsq.slipstream.persistence.RuntimeParameter;
import com.sixsq.slipstream.statemachine.StateMachine;
import com.sixsq.slipstream.statemachine.States;
import org.restlet.data.MediaType;
import org.restlet.data.Status;
import org.restlet.representation.Representation;
import org.restlet.resource.Delete;
import org.restlet.resource.Get;
import org.restlet.resource.Post;
import org.restlet.resource.Put;
import org.restlet.resource.ResourceException;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.persistence.PersistenceException;
import java.io.IOException;
import java.util.logging.Logger;
public class RuntimeParameterResource extends RunBaseResource {
private RuntimeParameter runtimeParameter;
private String key;
@Override
public void initializeSubResource() throws ResourceException {
MetricsTimer loadMetric = Metrics.getTimer(this, "runtimeParameter_loadFromDb");
loadMetric.start();
try {
long start = System.currentTimeMillis();
long before;
before = System.currentTimeMillis();
parseRequest();
logTimeDiff("parseRequest", before);
before = System.currentTimeMillis();
fetchRepresentation();
logTimeDiff("fetchRepresentation", before);
before = System.currentTimeMillis();
raiseConflictIfAbortIsSet();
logTimeDiff("raiseConflictIfAbortIsSet", before);
logTimeDiff("initialize on runtime parameter", start);
} catch(ResourceException e) {
throw e;
} catch(RuntimeException e) {
throw e;
} finally {
loadMetric.stop();
}
}
private MetricsTimer getMetricsTimer() {
return Metrics.getTimer(this, "runtimeParameter_" + getRequest().getMethod().getName());
}
private void parseRequest() {
extractAndSetIgnoreAbort();
key = getAttribute("key");
}
private void fetchRepresentation() {
runtimeParameter = loadRuntimeParameter(key);
setExisting(runtimeParameter != null);
}
private void raiseConflictIfAbortIsSet() {
if (!getIgnoreAbort()) {
if (isAbortSet()) {
throw (new ResourceException(Status.CLIENT_ERROR_CONFLICT,
"Abort flag raised!"));
}
}
}
private void abortOrReset(String abortMessage, EntityManager em) {
String nodename = RuntimeParameter.extractNodeNamePart(key);
Run.abortOrReset(abortMessage, nodename, em, getUuid());
}
@Delete
public void resetRuntimeParameter() throws ResourceException {
runtimeParameter.setValue("");
runtimeParameter.setIsSet(false);
runtimeParameter.store();
EntityManager em = PersistenceUtil.createEntityManager();
EntityTransaction transaction = em.getTransaction();
try {
transaction.begin();
abortOrReset(null, em);
transaction.commit();
} catch (PersistenceException e) {
transaction.rollback();
throw new SlipStreamDatabaseException(e.getMessage());
} finally {
em.close();
}
getResponse().setStatus(Status.SUCCESS_NO_CONTENT);
}
@Get
public String represent() throws ResourceException, NotFoundException,
ValidationException {
getMetricsTimer().start();
try {
long start = System.currentTimeMillis();
if (!runtimeParameter.isSet()) {
throw new ResourceException(
Status.CLIENT_ERROR_PRECONDITION_FAILED, "key " + key
+ " not yet set");
}
logTimeDiff("processing get on runtime parameter", start);
} finally {
getMetricsTimer().stop();
}
return runtimeParameter.getValue();
}
private String truncateMiddle(int maxLength, String text, String truncateMessage) {
if (text != null && text.length() > maxLength) {
int partsize = (maxLength - truncateMessage.length()) / 2;
text = text.substring(0, partsize-1) + truncateMessage + text.substring(text.length() - partsize);
}
return text;
}
@Put
public void update(Representation entity) throws ResourceException,
NotFoundException, ValidationException {
EntityManager em = PersistenceUtil.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
runtimeParameter = em.merge(runtimeParameter);
String value = (entity == null ? "" : extractValueFromEntity(entity));
boolean isGlobalAbort = RuntimeParameter.GLOBAL_ABORT_KEY.equals(key);
boolean isNodeAbort = (runtimeParameter.getNodeName()
+ RuntimeParameter.NODE_PROPERTY_SEPARATOR + RuntimeParameter.ABORT_KEY)
.equals(key);
if (isGlobalAbort || isNodeAbort) {
if (!runtimeParameter.isSet()) {
value = truncateMiddle(RuntimeParameter.VALUE_MAX_LENGTH, value, "\n(truncated)\n");
abortOrReset(value, em);
setValue(value);
}
} else if (isGlobalStateParameter()) {
States newState = attemptChangeGlobalState(value);
setValue(newState.toString());
} else {
setValue(value);
}
transaction.commit();
em.close();
getResponse().setEntity(null, MediaType.ALL);
}
private void setValue(String value) {
if (value != null && value.length() > RuntimeParameter.VALUE_MAX_LENGTH) {
throwClientBadRequest("Value too long (" + value.length() + "). " +
"Maximum length allowed: " + RuntimeParameter.VALUE_MAX_LENGTH + ".");
}
runtimeParameter.setValue(value);
RuntimeParameterMediator.processSpecialValue(runtimeParameter);
}
private String extractValueFromEntity(Representation entity) {
try {
return entity.getText();
} catch (IOException e) {
throw (new ResourceException(Status.CLIENT_ERROR_BAD_REQUEST,
"Bad value for key: " + key + " with details: "
+ e.getMessage()));
}
}
private States attemptChangeGlobalState(String toState) {
States toNewState = null;
try {
toNewState = States.valueOf(toState);
} catch (IllegalArgumentException e) {
throwClientBadRequest("Requested transition to unknown state: " + toState);
}
States newState = null;
// Jumping between states with an abort flag set on the run is not a good practice.
if (isAbortSet()) {
throwClientBadRequest("For any state transition via API abort should be cleared first.");
} else {
Run run = Run.loadFromUuid(getUuid());
States currentState = run.getState();
if (run.isMutable() && States.Ready == currentState && States.Provisioning == toNewState) {
newState = attemptChangeGlobalStateToProvisioning();
} else if (getUser().isSuper()) {
newState = toNewState;
} else {
throwClientBadRequest(String.format(
"Via API state can be advanced only on a mutable run and only from %s to %s", States.Ready,
States.Provisioning));
}
}
return newState;
}
private States attemptChangeGlobalStateToProvisioning() {
String fromToState = String.format("from %s to %s", States.Ready, States.Provisioning);
StateMachine sm;
EntityManager em = PersistenceUtil.createEntityManager();
Run run = Run.loadFromUuid(getUuid(), em);
EntityTransaction transaction = em.getTransaction();
try {
transaction.begin();
sm = StateMachine.createStateMachine(run);
try {
sm.tryAdvanceToProvisionning();
} catch (InvalidStateException | CannotAdvanceFromTerminalStateException e) {
e.printStackTrace();
throwClientBadRequest(String.format("Failed to advance state %s: %s", fromToState, e.getMessage()));
}
transaction.commit();
} catch (Exception ex) {
if (transaction.isActive()) {
transaction.rollback();
}
throw ex;
} finally {
em.close();
}
States newState = sm.getState();
if (States.Provisioning != newState) {
throwServerError(String.format("Failed to advance state %s: requested doesn't match reached %s.",
fromToState, newState));
}
return newState;
}
@Post
public void completeCurrentNodeStateOrChangeGlobalState(Representation entity) {
String nodeName = runtimeParameter.getNodeName();
States newState = attemptCompleteCurrentNodeState(nodeName);
getResponse().setEntity(newState.toString(), MediaType.TEXT_PLAIN);
}
private States attemptCompleteCurrentNodeState(String nodeName) {
StateMachine sm = createStateMachine();
return completeCurrentNodeState(nodeName, sm);
}
public States completeCurrentNodeState(String nodeName, StateMachine sc) {
States state;
try {
state = sc.updateState(nodeName);
} catch (InvalidStateException | SlipStreamClientException e) {
throw new ResourceException(Status.CLIENT_ERROR_CONFLICT, e.getMessage());
}
return state;
}
@Override
protected String getPageRepresentation() {
// TODO Stub
return null;
}
private void logTimeDiff(String msg, long before, long after) {
Logger.getLogger("Timing").finest("took to execute " + msg + ": " + (after - before));
}
protected void logTimeDiff(String msg, long before) {
logTimeDiff(msg, before, System.currentTimeMillis());
}
private boolean isGlobalStateParameter() {
return RuntimeParameter.GLOBAL_STATE_KEY.equals(key);
}
private StateMachine createStateMachine() {
EntityManager em = PersistenceUtil.createEntityManager();
Run run = Run.loadFromUuid(getUuid(), em);
StateMachine sm = StateMachine.createStateMachine(run);
em.close();
return sm;
}
}