Permalink
Browse files

add a support for an existing AWS image recreation:

it can be controlled with AwsMachineImageBuilder 'recreate' property (false by default)
+ add AWS operations timeout properties: waitingCount and waitingDelay
  • Loading branch information...
Stanislav Tiurikov
Stanislav Tiurikov committed Oct 28, 2017
1 parent 40be77b commit b27c762dd5cf87fefcefeb46dc748d55ea3fefec
@@ -1,16 +1,12 @@
package io.infrastructor.aws.inventory
import com.amazonaws.services.ec2.AmazonEC2
import com.amazonaws.services.ec2.model.CreateImageRequest
import com.amazonaws.services.ec2.model.CreateImageResult
import com.amazonaws.services.ec2.model.StopInstancesRequest
import io.infrastructor.core.inventory.Inventory
import javax.validation.constraints.NotNull
import javax.validation.constraints.Min
import static io.infrastructor.aws.inventory.utils.AmazonEC2Utils.amazonEC2
import static io.infrastructor.aws.inventory.utils.AmazonEC2Utils.waitForImageState
import static io.infrastructor.aws.inventory.utils.AmazonEC2Utils.waitForInstanceState
import static io.infrastructor.aws.inventory.utils.AmazonEC2Utils.*
import static io.infrastructor.core.logging.ConsoleLogger.*
import static io.infrastructor.core.logging.status.TextStatusLogger.withTextStatus
import static io.infrastructor.core.processing.ProvisioningContext.provision
@@ -30,7 +26,14 @@ class AwsMachineImageBuilder {
@NotNull
def terminateInstance = true
@NotNull
def recreate = false
@NotNull
def AwsNode awsNode
@Min(0l)
int waitingCount = 100
@Min(0l)
int waitingDelay = 3000
def node(Map params) { node(params, {}) }
def node(Closure closure) { node([:], closure) }
@@ -49,31 +52,47 @@ class AwsMachineImageBuilder {
def build(Closure closure) {
withTextStatus { statusLine ->
statusLine "> Aws Machine Image Builder: creating a temporary EC2 instance"
AmazonEC2 amazonEC2 = amazonEC2(awsAccessKeyId, awsAccessSecretKey, awsRegion)
statusLine "> aws machine image builder: checking for an existing image with name '$imageName'"
def oldImageId = findImageId(amazonEC2, imageName)
if (oldImageId && recreate) {
info "image '$imageName' - '$oldImageId' already exists, deregistering"
deregisterImage(amazonEC2, oldImageId)
} else if (oldImageId && !recreate) {
error "Image '$imageName' - '$oldImageId' already exists."
throw new AwsMachineImageBuilderException("Image '$imageName' - '$oldImageId' already exists. Please use property 'recreate = true' if you want to rebuild the image.")
} else {
info "image '$imageName' is not available yet, moving on"
}
statusLine "> aws machine image builder: creating a temporary EC2 instance"
awsNode.create(amazonEC2, usePublicIp)
statusLine "> Aws Machine Image Builder: provisioning the instance '$awsNode.id'"
statusLine "> aws machine image builder: provisioning the instance '$awsNode.id'"
provision([awsNode], closure)
statusLine "> Aws Machine Image Builder: stopping the instance '$awsNode.id' to speed up image build"
statusLine "> aws machine image builder: stopping the instance '$awsNode.id' to speed up image build"
awsNode.stop(amazonEC2)
waitForInstanceState(amazonEC2, awsNode.id, 20, 3000, 'stopped')
waitForInstanceState(amazonEC2, awsNode.id, waitingCount, waitingDelay, 'stopped')
statusLine "> Aws Machine Image Builder: creating an image '$imageName'"
CreateImageRequest createImageRequest = new CreateImageRequest()
createImageRequest.withInstanceId(awsNode.id)
createImageRequest.withName(imageName)
CreateImageResult result = amazonEC2.createImage(createImageRequest)
statusLine "> aws machine image builder: creating an image '$imageName'"
def newImageId = createImage(amazonEC2, imageName, awsNode.id)
statusLine "> Aws Machine Image Builder: waiting for image '$imageName' - '$result.imageId' is available"
waitForImageState(amazonEC2, result.imageId, 90, 3000, 'available')
statusLine "> Aws Machine Image Builder: image is ready, terminating the instance if needed"
if (terminateInstance) { awsNode.remove(amazonEC2) }
statusLine "> aws machine image builder: waiting for image '$imageName' - '$newImageId' is available"
waitForImageState(amazonEC2, newImageId, waitingCount, waitingDelay, 'available')
info "aws machine image is ready: $newImageId"
if (terminateInstance) {
statusLine "> aws machine image builder: terminating the temporary instance"
awsNode.remove(amazonEC2)
}
statusLine "> Aws Machine Image Builder: the image creation has finished"
return result.imageId
statusLine "> aws machine image builder: image creation proccess is complete"
return newImageId
}
}
}
@@ -0,0 +1,8 @@
package io.infrastructor.aws.inventory
class AwsMachineImageBuilderException extends RuntimeException {
public AwsMachineImageBuilderException(String message) {
super(message)
}
}
@@ -17,6 +17,7 @@ import com.amazonaws.services.ec2.model.TerminateInstancesRequest
import groovy.transform.ToString
import io.infrastructor.core.inventory.Node
import static io.infrastructor.aws.inventory.utils.AmazonEC2Utils.waitForInstanceState
import static io.infrastructor.core.logging.ConsoleLogger.*
@ToString(includePackage = false, includeNames = true, ignoreNulls = true, includeSuperProperties = true)
@@ -33,6 +34,9 @@ public class AwsNode extends Node {
def blockDeviceMappings = [] as Set
def state = ''
int waitingCount = 100
int waitingDelay = 3000
def blockDeviceMapping(Map params) {
blockDeviceMapping(params, {})
}
@@ -81,7 +85,7 @@ public class AwsNode extends Node {
id = amazonEC2.runInstances(request).getReservation().getInstances().get(0).getInstanceId()
updateTags(amazonEC2)
def instance = waitForInstanceIsRunning(amazonEC2, 50, 7000)
def instance = waitForInstanceState(amazonEC2, id, waitingCount, waitingDelay, 'running')
debug "updating host to publicIp: ${usePublicIp}"
host = usePublicIp ? instance.publicIpAddress : instance.privateIpAddress
@@ -139,21 +143,6 @@ public class AwsNode extends Node {
amazonEC2.modifyInstanceAttribute(request)
}
private def waitForInstanceIsRunning(def amazonEC2, int attempts, int interval) {
for (int i = attempts; i > 0; i--) {
DescribeInstancesRequest request = new DescribeInstancesRequest()
request.setInstanceIds([id])
DescribeInstancesResult result = amazonEC2.describeInstances(request)
Instance instance = result.getReservations().get(0).getInstances().get(0)
debug "waiting for instance '$id' state is running, current state: ${instance.getState().getCode()}"
if (instance.getState().getCode() == 16) { // instance is running
return instance
}
sleep(interval)
}
throw new RuntimeException("timeout waiting for instance $id state is running after $attempts attempts. node: $this")
}
private def buildBlockDeviceMappings() {
def mappings = []
blockDeviceMappings.each { mapping ->
@@ -5,11 +5,13 @@ import com.amazonaws.auth.AWSStaticCredentialsProvider
import com.amazonaws.retry.RetryUtils
import com.amazonaws.services.ec2.AmazonEC2
import com.amazonaws.services.ec2.AmazonEC2ClientBuilder
import com.amazonaws.services.ec2.model.CreateImageRequest
import com.amazonaws.services.ec2.model.DescribeImagesRequest
import com.amazonaws.services.ec2.model.DescribeImagesResult
import com.amazonaws.services.ec2.model.DescribeInstancesRequest
import com.amazonaws.services.ec2.model.DescribeInstancesResult
import com.amazonaws.services.ec2.model.DescribeSubnetsRequest
import com.amazonaws.services.ec2.model.DeregisterImageRequest
import com.amazonaws.services.ec2.model.Filter
import com.amazonaws.services.ec2.model.Instance
import io.infrastructor.aws.inventory.AwsNode
@@ -40,6 +42,7 @@ class AmazonEC2Utils {
Instance instance = result.getReservations().get(0).getInstances().get(0)
debug "waiting for instance $instanceId state is $state, current state: ${instance.getState().getName()}"
assert instance.getState().getName() == state
instance
}
}
@@ -54,6 +57,30 @@ class AmazonEC2Utils {
}
}
def static findImageId(def amazonEC2, def imageName) {
DescribeImagesRequest describeImagesRequest = new DescribeImagesRequest()
describeImagesRequest.withFilters(new Filter().withName("name").withValues(imageName))
DescribeImagesResult describeImagesResult = amazonEC2.describeImages(describeImagesRequest)
def images = describeImagesResult.getImages()
if (!images.isEmpty()) {
return images.get(0).imageId
}
return null
}
def static deregisterImage(def amazonEC2, def imageId) {
amazonEC2.deregisterImage(new DeregisterImageRequest(imageId))
}
def static createImage(def amazonEC2, def imageName, def instanceId) {
CreateImageRequest createImageRequest = new CreateImageRequest()
createImageRequest.withName(imageName)
createImageRequest.withInstanceId(instanceId)
amazonEC2.createImage(createImageRequest).imageId
}
public static void assertInstanceExists(def awsAccessKey, def awsSecretKey, def awsRegion, def definition) {
def amazonEC2 = amazonEC2(awsAccessKey, awsSecretKey, awsRegion)
@@ -76,21 +103,4 @@ class AmazonEC2Utils {
if (expected.securityGroupIds) assert (expected.securityGroupIds as Set) == (instance.securityGroups.collect { it.groupId } as Set)
if (expected.tags) assert expected.tags == instance.tags.collectEntries { [(it.key as String) : (it.value as String)] }
}
public static def findSubnetIdByName(def awsAccessKey, def awsSecretKey, def awsRegion, def name) {
def amazonEC2 = amazonEC2(awsAccessKey, awsSecretKey, awsRegion)
def result = amazonEC2.describeSubnets(
new DescribeSubnetsRequest().withFilters(new Filter("tag:Name", [name])))
if (result.getSubnets().size() == 0) {
throw new RuntimeException("Unable to find subnet with name '$name'")
}
if (result.getSubnets().size() > 1) {
throw new RuntimeException("Multiple subnets with the same name ($name) has been found")
}
return result.getSubnets()[0].subnetId
}
}
}
@@ -84,5 +84,37 @@ class AwsMachineImageBuilderTest {
}
}
}
@Test(expected = ValidationException)
void negative_waitingDelay() {
AwsMachineImageBuilder.awsMachineImage {
awsAccessKeyId = '...'
awsAccessSecretKey = '...'
awsRegion = 'eu-west-1'
imageName = "test-image"
usePublicIp = true
node {
name = 'dummy'
}
waitingDelay = -1
}
}
@Test(expected = ValidationException)
void negative_waitingCount() {
AwsMachineImageBuilder.awsMachineImage {
awsAccessKeyId = '...'
awsAccessSecretKey = '...'
awsRegion = 'eu-west-1'
imageName = "test-image"
usePublicIp = true
node {
name = 'dummy'
}
waitingCount = -1
}
}
}
@@ -52,7 +52,7 @@ public class Starter {
def message = ex.toString()?.replaceAll("\n", "\n ")
debug " ${bold('UNCAUGHT EXCEPTION:')}"
debug " ${ex.class.name}: $message"
debug " $message"
debug " ${bold('STACK TRACE:\n')} - ${deepSanitize(ex).replaceAll('\n', '\n - ')}"
def duration = TimeCategory.minus(new Date(), timeStart)
@@ -7,8 +7,7 @@ class RetryUtils {
def attempt = 1
while (attempt <= count) {
try {
actions()
return
return actions()
} catch (AssertionError | Exception ex) {
debug "retry - attempt ${attempt} of $count failed due to:\n" + "$ex"
if (attempt == count) throw new RuntimeException("retry failed after $attempt attempts. Last error: $ex")

0 comments on commit b27c762

Please sign in to comment.