/*************************************************************************
* Copyright 2009-2015 Eucalyptus Systems, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
* Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
* CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
* additional information or have any questions.
************************************************************************/
package com.eucalyptus.cloudformation.resources.standard.actions;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectInputStream;
import com.amazonaws.services.s3.model.S3VersionSummary;
import com.amazonaws.services.s3.model.VersionListing;
import com.eucalyptus.cloudformation.Limits;
import com.eucalyptus.cloudformation.ValidationErrorException;
import com.eucalyptus.cloudformation.bootstrap.CloudFormationAWSCredentialsProvider;
import com.eucalyptus.cloudformation.entity.SignalEntity;
import com.eucalyptus.cloudformation.entity.SignalEntityManager;
import com.eucalyptus.cloudformation.entity.StackEventEntityManager;
import com.eucalyptus.cloudformation.entity.StackResourceEntity;
import com.eucalyptus.cloudformation.entity.StackResourceEntityManager;
import com.eucalyptus.cloudformation.resources.ResourceAction;
import com.eucalyptus.cloudformation.resources.ResourceInfo;
import com.eucalyptus.cloudformation.resources.ResourceProperties;
import com.eucalyptus.cloudformation.resources.standard.info.AWSCloudFormationWaitConditionHandleResourceInfo;
import com.eucalyptus.cloudformation.resources.standard.info.AWSCloudFormationWaitConditionResourceInfo;
import com.eucalyptus.cloudformation.resources.standard.propertytypes.AWSCloudFormationWaitConditionProperties;
import com.eucalyptus.cloudformation.template.CreationPolicy;
import com.eucalyptus.cloudformation.template.JsonHelper;
import com.eucalyptus.cloudformation.workflow.ResourceFailureException;
import com.eucalyptus.cloudformation.workflow.RetryAfterConditionCheckFailedException;
import com.eucalyptus.cloudformation.workflow.steps.Step;
import com.eucalyptus.cloudformation.workflow.steps.StepBasedResourceAction;
import com.eucalyptus.cloudformation.workflow.updateinfo.UpdateType;
import com.eucalyptus.objectstorage.client.EucaS3Client;
import com.eucalyptus.objectstorage.client.EucaS3ClientFactory;
import com.eucalyptus.util.Json;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
import org.apache.log4j.Logger;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.eucalyptus.cloudformation.Limits.DEFAULT_MAX_LENGTH_WAIT_CONDITION_SIGNAL;
/**
* Created by ethomas on 2/3/14.
*/
public class AWSCloudFormationWaitConditionResourceAction extends StepBasedResourceAction {
private static final Logger LOG = Logger.getLogger(AWSCloudFormationWaitConditionResourceAction.class);
private AWSCloudFormationWaitConditionProperties properties = new AWSCloudFormationWaitConditionProperties();
private AWSCloudFormationWaitConditionResourceInfo info = new AWSCloudFormationWaitConditionResourceInfo();
public AWSCloudFormationWaitConditionResourceAction() {
super(fromEnum(CreateSteps.class), fromEnum(DeleteSteps.class), null, null);
}
@Override
public ResourceProperties getResourceProperties() {
return properties;
}
@Override
public void setResourceProperties(ResourceProperties resourceProperties) {
properties = (AWSCloudFormationWaitConditionProperties) resourceProperties;
}
@Override
public ResourceInfo getResourceInfo() {
return info;
}
@Override
public void setResourceInfo(ResourceInfo resourceInfo) {
info = (AWSCloudFormationWaitConditionResourceInfo) resourceInfo;
}
@Override
public UpdateType getUpdateType(ResourceAction resourceAction, boolean stackTagsChanged) {
return UpdateType.UNSUPPORTED;
}
private static class BucketAndKey {
String bucket;
String key;
public BucketAndKey(String bucket, String key) {
this.bucket = bucket;
this.key = key;
}
public String getKey() {
return key;
}
public String getBucket() {
return bucket;
}
}
private static BucketAndKey getBucketAndKey(AWSCloudFormationWaitConditionResourceAction action) throws Exception {
LOG.trace("Looking for handle : " + action.properties.getHandle());
List<StackResourceEntity> stackResourceEntityList = StackResourceEntityManager.getStackResources(action.getStackEntity().getStackId(), action.info.getAccountId(), action.getStackEntity().getStackVersion());
StackResourceEntity handleEntity = null;
for (StackResourceEntity stackResourceEntity : stackResourceEntityList) {
if (stackResourceEntity.getPhysicalResourceId() != null && stackResourceEntity.getPhysicalResourceId().equals(action.properties.getHandle())) {
LOG.trace("found something with the same physical id, type:" + stackResourceEntity.getResourceType());
if (stackResourceEntity.getResourceType().equals("AWS::CloudFormation::WaitConditionHandle")) {
handleEntity = stackResourceEntity;
break;
}
}
}
if (handleEntity == null) {
throw new Exception("Handle URL:" + action.properties.getHandle() + " does not match a WaitConditionHandle from this stack");
}
AWSCloudFormationWaitConditionHandleResourceInfo handleResourceInfo = (AWSCloudFormationWaitConditionHandleResourceInfo) StackResourceEntityManager.getResourceInfo(handleEntity);
ObjectNode objectNode = (ObjectNode) JsonHelper.getJsonNodeFromString(handleResourceInfo.getEucaParts());
if (!"1.0".equals(objectNode.get("version").asText())) throw new Exception("Invalid version for eucaParts");
String bucketName = objectNode.get("bucket").asText();
LOG.trace("bucketName=" + bucketName);
String keyName = objectNode.get("key").asText();
return new BucketAndKey(bucketName, keyName);
}
private enum CreateSteps implements Step {
INIT_CREATE_WAIT_CONDITION {
@Override
public ResourceAction perform(ResourceAction resourceAction) throws Exception {
AWSCloudFormationWaitConditionResourceAction action = (AWSCloudFormationWaitConditionResourceAction) resourceAction;
CreationPolicy creationPolicy = CreationPolicy.parse(action.info.getCreationPolicyJson());
if (creationPolicy != null && creationPolicy.getResourceSignal() != null) {
action.info.setPhysicalResourceId(action.getDefaultPhysicalResourceId());
} else {
BucketAndKey bucketAndKey = getBucketAndKey(action);
String bucketName = bucketAndKey.getBucket();
String keyName = bucketAndKey.getKey();;
action.info.setPhysicalResourceId(keyName);
}
action.info.setCreatedEnoughToDelete(true);
action.info.setReferenceValueJson(JsonHelper.getStringFromJsonNode(new TextNode(action.info.getPhysicalResourceId())));
action.info.setEucaCreateStartTime(JsonHelper.getStringFromJsonNode(new TextNode("" + System.currentTimeMillis())));
return action;
}
@Nullable
@Override
public Integer getTimeout() {
return null;
}
},
CHECK_SIGNALS {
@Override
public ResourceAction perform(ResourceAction resourceAction) throws Exception {
LOG.trace("Checking for signals");
AWSCloudFormationWaitConditionResourceAction action = (AWSCloudFormationWaitConditionResourceAction) resourceAction;
CreationPolicy creationPolicy = CreationPolicy.parse(action.info.getCreationPolicyJson());
if (creationPolicy != null && creationPolicy.getResourceSignal() != null) {
// check for signals
Collection<SignalEntity> signals = SignalEntityManager.getSignals(action.getStackEntity().getStackId(), action.info.getAccountId(), action.info.getLogicalResourceId(),
action.getStackEntity().getStackVersion());
int numSuccessSignals = 0;
if (signals != null) {
for (SignalEntity signal : signals) {
if (signal.getStatus() == SignalEntity.Status.FAILURE) {
throw new ResourceFailureException("Received FAILURE signal with UniqueId " + signal.getUniqueId());
}
if (!signal.getProcessed()) {
StackEventEntityManager.addSignalStackEvent(signal);
signal.setProcessed(true);
SignalEntityManager.updateSignal(signal);
}
numSuccessSignals++;
}
}
if (numSuccessSignals < creationPolicy.getResourceSignal().getCount()) {
long durationMs = System.currentTimeMillis() - Long.valueOf(JsonHelper.getJsonNodeFromString(action.info.getEucaCreateStartTime()).asText());
if (TimeUnit.MILLISECONDS.toSeconds(durationMs) > creationPolicy.getResourceSignal().getTimeout()) {
throw new ResourceFailureException("Failed to receive " + creationPolicy.getResourceSignal().getCount() + " resource signal(s) within the specified duration");
}
throw new RetryAfterConditionCheckFailedException("Not enough success signals yet");
}
ObjectNode dataNode = JsonHelper.createObjectNode();
action.info.setData(JsonHelper.getStringFromJsonNode(new TextNode(dataNode.toString())));
return action;
} else {
if (action.properties.getTimeout() == null) {
throw new ValidationErrorException("Timeout is a required field");
}
if (action.properties.getHandle() == null) {
throw new ValidationErrorException("Handle is a required field");
}
int numSignals = action.properties.getCount() != null && action.properties.getCount() > 0 ? action.properties.getCount() : 1;
LOG.trace("num signals = " + numSignals);
if (action.properties.getTimeout() > 43200) {
throw new ValidationErrorException("timeout can not be more than 43200");
}
LOG.trace("Timeout = " + action.properties.getTimeout());
BucketAndKey bucketAndKey = getBucketAndKey(action);
String bucketName = bucketAndKey.getBucket();
String keyName = bucketAndKey.getKey();;
boolean foundFailure = false;
final Map<String, String> dataMap = Maps.newHashMap();
try (final EucaS3Client s3c = EucaS3ClientFactory.getEucaS3Client(new CloudFormationAWSCredentialsProvider())) {
LOG.trace("Handle:" + action.properties.getHandle());
VersionListing versionListing = s3c.listVersions(bucketName, "");
LOG.trace("Found " + versionListing.getVersionSummaries() + " versions to check");
for (S3VersionSummary versionSummary : versionListing.getVersionSummaries()) {
LOG.trace("Key:" + versionSummary.getKey());
if (!versionSummary.getKey().equals(keyName)) {
continue;
}
LOG.trace("Getting version: " + versionSummary.getVersionId());
try {
GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, keyName, versionSummary.getVersionId());
S3Object s3Object = s3c.getObject(getObjectRequest);
JsonNode jsonNode = null;
try (S3ObjectInputStream s3ObjectInputStream = s3Object.getObjectContent()) {
long maxLength = DEFAULT_MAX_LENGTH_WAIT_CONDITION_SIGNAL;
try {
maxLength = Long.parseLong(System.getProperty("cloudformation.max_length_wait_condition_signal"));
} catch (Exception ignore) {
}
jsonNode = Json.parse(ByteStreams.limit(s3ObjectInputStream, maxLength));
}
if (!jsonNode.isObject()) {
LOG.trace("Read object, json but not object..skipping file");
continue;
}
ObjectNode localObjectNode = (ObjectNode) jsonNode;
String status = localObjectNode.get("Status").asText();
if (status == null) {
LOG.trace("Null status, skipping");
continue;
}
String data = localObjectNode.get("Data").asText();
if (data == null) {
LOG.trace("Null data, skipping");
continue;
}
String uniqueId = localObjectNode.get("UniqueId").asText();
if (uniqueId == null) {
LOG.trace("Null uniqueId, skipping");
continue;
}
if ("FAILURE".equals(status)) {
foundFailure = true;
LOG.trace("found failure, gonna die");
break;
} else if (!"SUCCESS".equals(status)) {
LOG.trace("weird status...skipping");
continue;
} else {
LOG.trace("found success, uniqueId=" + uniqueId);
dataMap.put(uniqueId, data);
}
} catch (Exception ex) {
LOG.error(ex, ex);
LOG.trace("Exception while going through the objects, will skip this one.");
}
}
}
if (foundFailure) {
throw new ResourceFailureException("Found failure signal");
}
LOG.trace("Have " + dataMap.size() + " success signals, need " + numSignals);
if (dataMap.size() >= numSignals) {
LOG.trace("Success");
ObjectNode dataNode = JsonHelper.createObjectNode();
for (String uniqueId : dataMap.keySet()) {
dataNode.put(uniqueId, dataMap.get(uniqueId));
}
action.info.setData(JsonHelper.getStringFromJsonNode(new TextNode(dataNode.toString())));
return action;
} else {
long durationMs = System.currentTimeMillis() - Long.valueOf(JsonHelper.getJsonNodeFromString(action.info.getEucaCreateStartTime()).asText());
if (TimeUnit.MILLISECONDS.toSeconds(durationMs) > action.properties.getTimeout()) {
throw new ResourceFailureException("Timeout exeeded waiting for success signals");
}
throw new RetryAfterConditionCheckFailedException("Not enough success signals yet");
}
}
}
@Nullable
@Override
public Integer getTimeout() {
return (int) TimeUnit.HOURS.toSeconds(12);
}
};
}
private enum DeleteSteps implements Step {
DO_NOTHING {
@Override
public ResourceAction perform(ResourceAction resourceAction) throws Exception {
return resourceAction; // nothing to do really
}
};
@Nullable
@Override
public Integer getTimeout() {
return null;
}
}
}