/************************************************************************* * 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.eucalyptus.cloudformation.ValidationErrorException; 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.AWSEC2VolumeAttachmentResourceInfo; import com.eucalyptus.cloudformation.resources.standard.propertytypes.AWSEC2VolumeAttachmentProperties; import com.eucalyptus.cloudformation.template.JsonHelper; import com.eucalyptus.cloudformation.util.MessageHelper; 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.component.ServiceConfiguration; import com.eucalyptus.component.Topology; import com.eucalyptus.compute.common.AttachVolumeResponseType; import com.eucalyptus.compute.common.AttachVolumeType; import com.eucalyptus.compute.common.AttachedVolume; import com.eucalyptus.compute.common.Compute; import com.eucalyptus.compute.common.DescribeInstancesResponseType; import com.eucalyptus.compute.common.DescribeInstancesType; import com.eucalyptus.compute.common.DescribeVolumesResponseType; import com.eucalyptus.compute.common.DescribeVolumesType; import com.eucalyptus.compute.common.DetachVolumeResponseType; import com.eucalyptus.compute.common.DetachVolumeType; import com.eucalyptus.compute.common.Filter; import com.eucalyptus.configurable.ConfigurableClass; import com.eucalyptus.configurable.ConfigurableField; import com.eucalyptus.util.async.AsyncRequests; import com.fasterxml.jackson.databind.node.TextNode; import javax.annotation.Nullable; import java.util.Objects; import static com.eucalyptus.util.async.AsyncExceptions.asWebServiceErrorMessage; /** * Created by ethomas on 2/3/14. */ @ConfigurableClass( root = "cloudformation", description = "Parameters controlling cloud formation") public class AWSEC2VolumeAttachmentResourceAction extends StepBasedResourceAction { private AWSEC2VolumeAttachmentProperties properties = new AWSEC2VolumeAttachmentProperties(); private AWSEC2VolumeAttachmentResourceInfo info = new AWSEC2VolumeAttachmentResourceInfo(); @ConfigurableField(initial = "300", description = "The amount of time (in seconds) to wait for a volume to be attached during create)") public static volatile Integer VOLUME_ATTACHMENT_MAX_CREATE_RETRY_SECS = 300; @ConfigurableField(initial = "300", description = "The amount of time (in seconds) to wait for a volume to detach during delete)") public static volatile Integer VOLUME_DETACHMENT_MAX_DELETE_RETRY_SECS = 300; public AWSEC2VolumeAttachmentResourceAction() { // update not supported super(fromEnum(CreateSteps.class), fromEnum(DeleteSteps.class), null, null); } @Override public UpdateType getUpdateType(ResourceAction resourceAction, boolean stackTagsChanged) { UpdateType updateType = info.supportsTags() && stackTagsChanged ? UpdateType.NO_INTERRUPTION : UpdateType.NONE; AWSEC2VolumeAttachmentResourceAction otherAction = (AWSEC2VolumeAttachmentResourceAction) resourceAction; if (!Objects.equals(properties.getDevice(), otherAction.properties.getDevice())) { updateType = UpdateType.max(updateType, UpdateType.UNSUPPORTED); } if (!Objects.equals(properties.getInstanceId(), otherAction.properties.getInstanceId())) { updateType = UpdateType.max(updateType, UpdateType.UNSUPPORTED); } if (!Objects.equals(properties.getVolumeId(), otherAction.properties.getVolumeId())) { updateType = UpdateType.max(updateType, UpdateType.UNSUPPORTED); } return updateType; } private enum CreateSteps implements Step { ATTACH_VOLUME { @Override public ResourceAction perform(ResourceAction resourceAction) throws Exception { AWSEC2VolumeAttachmentResourceAction action = (AWSEC2VolumeAttachmentResourceAction) resourceAction; ServiceConfiguration configuration = Topology.lookup(Compute.class); DescribeInstancesType describeInstancesType = MessageHelper.createMessage(DescribeInstancesType.class, action.info.getEffectiveUserId()); describeInstancesType.getFilterSet( ).add( Filter.filter( "instance-id", action.properties.getInstanceId( ) ) ); DescribeInstancesResponseType describeInstancesResponseType = AsyncRequests.sendSync( configuration, describeInstancesType ); if (describeInstancesResponseType.getReservationSet() == null || describeInstancesResponseType.getReservationSet().isEmpty()) { throw new ValidationErrorException("No such instance " + action.properties.getInstanceId()); } DescribeVolumesType describeVolumesType = MessageHelper.createMessage(DescribeVolumesType.class, action.info.getEffectiveUserId()); describeVolumesType.getFilterSet( ).add( Filter.filter( "volume-id", action.properties.getVolumeId( ) ) ); DescribeVolumesResponseType describeVolumesResponseType; try { describeVolumesResponseType = AsyncRequests.sendSync( configuration, describeVolumesType ); } catch ( Exception e ) { throw new ValidationErrorException("Error describing volume " + action.properties.getVolumeId() + ":" + asWebServiceErrorMessage( e, e.getMessage() ) ); } if (describeVolumesResponseType.getVolumeSet().size()==0) throw new ValidationErrorException("No such volume " + action.properties.getVolumeId()); if (!"available".equals(describeVolumesResponseType.getVolumeSet().get(0).getStatus())) { throw new ValidationErrorException("Volume " + action.properties.getVolumeId() + " not available"); } AttachVolumeType attachVolumeType = MessageHelper.createMessage(AttachVolumeType.class, action.info.getEffectiveUserId()); attachVolumeType.setInstanceId(action.properties.getInstanceId()); attachVolumeType.setVolumeId(action.properties.getVolumeId()); attachVolumeType.setDevice(action.properties.getDevice()); AsyncRequests.<AttachVolumeType, AttachVolumeResponseType> sendSync(configuration, attachVolumeType); return action; } }, WAIT_UNTIL_ATTACHED { @Override public ResourceAction perform(ResourceAction resourceAction) throws Exception { AWSEC2VolumeAttachmentResourceAction action = (AWSEC2VolumeAttachmentResourceAction) resourceAction; ServiceConfiguration configuration = Topology.lookup(Compute.class); boolean attached = false; DescribeVolumesType describeVolumesType = MessageHelper.createMessage(DescribeVolumesType.class, action.info.getEffectiveUserId()); describeVolumesType.getFilterSet( ).add( Filter.filter( "volume-id", action.properties.getVolumeId( ) ) ); DescribeVolumesResponseType describeVolumesResponseType; try { describeVolumesResponseType = AsyncRequests.sendSync( configuration, describeVolumesType ); } catch ( Exception e ) { throw new ValidationErrorException("Error describing volume " + action.properties.getVolumeId() + ":" + asWebServiceErrorMessage( e, e.getMessage() ) ); } if (describeVolumesResponseType.getVolumeSet().size() == 0) { throwNotAttachedMessage(action.properties.getVolumeId(), action.properties.getInstanceId()); } if (describeVolumesResponseType.getVolumeSet().get(0).getAttachmentSet() == null || describeVolumesResponseType.getVolumeSet().get(0).getAttachmentSet().isEmpty()) { throwNotAttachedMessage(action.properties.getVolumeId(), action.properties.getInstanceId()); } for (AttachedVolume attachedVolume: describeVolumesResponseType.getVolumeSet().get(0).getAttachmentSet()) { if (attachedVolume.getInstanceId().equals(action.properties.getInstanceId()) && attachedVolume.getDevice().equals(action.properties.getDevice()) && attachedVolume.getStatus().equals("attached")) { attached = true; break; } } if (!attached) { throwNotAttachedMessage(action.properties.getVolumeId(), action.properties.getInstanceId()); } return action; } @Override public Integer getTimeout() { return VOLUME_ATTACHMENT_MAX_CREATE_RETRY_SECS; } private RetryAfterConditionCheckFailedException throwNotAttachedMessage(String volumeId, String instanceId) throws RetryAfterConditionCheckFailedException { throw new RetryAfterConditionCheckFailedException("Volume " + volumeId + " not yet attached to instance " + instanceId); } }, POPULATE_FIELDS { @Override public ResourceAction perform(ResourceAction resourceAction) throws Exception { AWSEC2VolumeAttachmentResourceAction action = (AWSEC2VolumeAttachmentResourceAction) resourceAction; ServiceConfiguration configuration = Topology.lookup(Compute.class); action.info.setPhysicalResourceId(action.getDefaultPhysicalResourceId()); action.info.setCreatedEnoughToDelete(true); action.info.setReferenceValueJson(JsonHelper.getStringFromJsonNode(new TextNode(action.info.getPhysicalResourceId()))); return action; } }; @Nullable @Override public Integer getTimeout() { return null; } } private enum DeleteSteps implements Step { DETACH_VOLUME { @Override public ResourceAction perform(ResourceAction resourceAction) throws Exception { AWSEC2VolumeAttachmentResourceAction action = (AWSEC2VolumeAttachmentResourceAction) resourceAction; ServiceConfiguration configuration = Topology.lookup(Compute.class); if (notCreatedOrNoInstanceOrNoVolume(action, configuration)) return action; DetachVolumeType detachVolumeType = MessageHelper.createMessage(DetachVolumeType.class, action.info.getEffectiveUserId()); detachVolumeType.setInstanceId(action.properties.getInstanceId()); detachVolumeType.setVolumeId(action.properties.getVolumeId()); detachVolumeType.setDevice(action.properties.getDevice()); AsyncRequests.<DetachVolumeType, DetachVolumeResponseType> sendSync(configuration, detachVolumeType); return action; } }, WAIT_UNTIL_DETACHED { @Override public ResourceAction perform(ResourceAction resourceAction) throws Exception { AWSEC2VolumeAttachmentResourceAction action = (AWSEC2VolumeAttachmentResourceAction) resourceAction; ServiceConfiguration configuration = Topology.lookup(Compute.class); if (notCreatedOrNoInstanceOrNoVolume(action, configuration)) return action; boolean detached = false; DescribeVolumesType describeVolumesType = MessageHelper.createMessage(DescribeVolumesType.class, action.info.getEffectiveUserId()); describeVolumesType.getFilterSet( ).add( Filter.filter( "volume-id", action.properties.getVolumeId( ) ) ); DescribeVolumesResponseType describeVolumesResponseType; try { describeVolumesResponseType = AsyncRequests.sendSync( configuration, describeVolumesType ); } catch ( Exception e ) { throw new ValidationErrorException("Error describing volume " + action.properties.getVolumeId() + ":" + asWebServiceErrorMessage( e, e.getMessage() ) ); } if (describeVolumesResponseType.getVolumeSet().size() == 0) { return action; // volume is gone } if (describeVolumesResponseType.getVolumeSet().get(0).getAttachmentSet() == null || describeVolumesResponseType.getVolumeSet().get(0).getAttachmentSet().isEmpty()) { return action; // volume not attached to anything } for (AttachedVolume attachedVolume: describeVolumesResponseType.getVolumeSet().get(0).getAttachmentSet()) { if (attachedVolume.getInstanceId().equals(action.properties.getInstanceId()) && attachedVolume.getDevice().equals(action.properties.getDevice()) && attachedVolume.getStatus().equals("detached")) { detached = true; break; } } if (detached == true) return action; throw new RetryAfterConditionCheckFailedException("Volume " + action.properties.getVolumeId() + " is not yet detached from instance " + action.properties.getInstanceId()); } @Override public Integer getTimeout() { return VOLUME_DETACHMENT_MAX_DELETE_RETRY_SECS; } }; @Nullable @Override public Integer getTimeout() { return null; } private static boolean notCreatedOrNoInstanceOrNoVolume(AWSEC2VolumeAttachmentResourceAction action, ServiceConfiguration configuration) throws Exception { if (!Boolean.TRUE.equals(action.info.getCreatedEnoughToDelete())) return true; DescribeInstancesType describeInstancesType = MessageHelper.createMessage(DescribeInstancesType.class, action.info.getEffectiveUserId()); describeInstancesType.getFilterSet( ).add( Filter.filter( "instance-id", action.properties.getInstanceId( ) ) ); DescribeInstancesResponseType describeInstancesResponseType = AsyncRequests.sendSync( configuration, describeInstancesType ); if (describeInstancesResponseType.getReservationSet() == null || describeInstancesResponseType.getReservationSet().isEmpty()) { return true; // can't be attached to a nonexistent instance; } DescribeVolumesType describeVolumesType = MessageHelper.createMessage(DescribeVolumesType.class, action.info.getEffectiveUserId()); describeVolumesType.getFilterSet( ).add( Filter.filter( "volume-id", action.properties.getVolumeId( ) ) ); DescribeVolumesResponseType describeVolumesResponseType; try { describeVolumesResponseType = AsyncRequests.sendSync( configuration, describeVolumesType ); } catch ( Exception e ) { throw new ValidationErrorException("Error describing volume " + action.properties.getVolumeId() + ":" + asWebServiceErrorMessage( e, e.getMessage() ) ); } if (describeVolumesResponseType.getVolumeSet().size()==0) { return true; // volume can't be attached if it doesn't exist } return false; } } @Override public ResourceProperties getResourceProperties() { return properties; } @Override public void setResourceProperties(ResourceProperties resourceProperties) { properties = (AWSEC2VolumeAttachmentProperties) resourceProperties; } @Override public ResourceInfo getResourceInfo() { return info; } @Override public void setResourceInfo(ResourceInfo resourceInfo) { info = (AWSEC2VolumeAttachmentResourceInfo) resourceInfo; } }