From bf4626016b426d1fbc0e3e4d4e7499ea3aca9ac2 Mon Sep 17 00:00:00 2001 From: Andrew Bayer Date: Tue, 4 Feb 2014 11:08:02 -0800 Subject: [PATCH] JCLOUDS-450. Adding support for EC2 MaxCount option. Note - no live test for this, because it's very hard to guarantee a situation where it would be relevant. --- .../compute/options/EC2TemplateOptions.java | 75 ++++++++-- ...rityGroupsAsNeededAndReturnRunOptions.java | 24 ++-- .../EC2CreateNodesInGroupThenAddToSet.java | 48 ++++--- .../ec2/options/RunInstancesOptions.java | 17 +++ .../compute/EC2ComputeServiceExpectTest.java | 133 +++++++++++++++++- .../compute/EC2ComputeServiceLiveTest.java | 7 + .../BaseEC2ComputeServiceExpectTest.java | 68 +++++++-- .../options/EC2TemplateOptionsTest.java | 43 +++++- ...GroupsAsNeededAndReturnRunOptionsTest.java | 17 +-- ...EC2CreateNodesInGroupThenAddToSetTest.java | 16 +-- .../ec2/options/RunInstancesOptionsTest.java | 17 ++- .../describe_instances_three_ids.xml | 74 ++++++++++ .../test/resources/run_instances_three.xml | 74 ++++++++++ 13 files changed, 536 insertions(+), 77 deletions(-) create mode 100644 apis/ec2/src/test/resources/describe_instances_three_ids.xml create mode 100644 apis/ec2/src/test/resources/run_instances_three.xml diff --git a/apis/ec2/src/main/java/org/jclouds/ec2/compute/options/EC2TemplateOptions.java b/apis/ec2/src/main/java/org/jclouds/ec2/compute/options/EC2TemplateOptions.java index 1d837fe41f..32c81244f5 100644 --- a/apis/ec2/src/main/java/org/jclouds/ec2/compute/options/EC2TemplateOptions.java +++ b/apis/ec2/src/main/java/org/jclouds/ec2/compute/options/EC2TemplateOptions.java @@ -16,6 +16,16 @@ */ package org.jclouds.ec2.compute.options; +import static com.google.common.base.Objects.equal; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Strings.emptyToNull; + +import java.util.List; +import java.util.Map; +import java.util.Set; + import com.google.common.base.Objects; import com.google.common.base.Objects.ToStringHelper; import com.google.common.collect.ImmutableSet; @@ -32,16 +42,6 @@ import org.jclouds.ec2.domain.BlockDeviceMapping.UnmapDeviceNamed; import org.jclouds.javax.annotation.Nullable; import org.jclouds.scriptbuilder.domain.Statement; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import static com.google.common.base.Objects.equal; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.base.Strings.emptyToNull; - /** * Contains options supported in the {@code ComputeService#runNode} operation on * the "ec2" provider.

@@ -82,6 +82,10 @@ public class EC2TemplateOptions extends TemplateOptions implements Cloneable { eTo.noKeyPair(); if (getUserData() != null) eTo.userData(getUserData()); + if (getMaxCount() > 0) + eTo.maxCount(getMaxCount()); + if (getClientToken() != null) + eTo.clientToken(getClientToken()); } } @@ -90,6 +94,8 @@ public class EC2TemplateOptions extends TemplateOptions implements Cloneable { private boolean noKeyPair; private List userData; private ImmutableSet.Builder blockDeviceMappings = ImmutableSet.builder(); + private Integer maxCount; + private String clientToken = null; @Override public boolean equals(Object o) { @@ -99,14 +105,17 @@ public class EC2TemplateOptions extends TemplateOptions implements Cloneable { return false; EC2TemplateOptions that = EC2TemplateOptions.class.cast(o); return super.equals(that) && equal(this.groupNames, that.groupNames) && equal(this.keyPair, that.keyPair) - && equal(this.noKeyPair, that.noKeyPair) && equal(this.userData, that.userData) - && equal(this.blockDeviceMappings, that.blockDeviceMappings); + && equal(this.noKeyPair, that.noKeyPair) && equal(this.userData, that.userData) + && equal(this.blockDeviceMappings, that.blockDeviceMappings) + && equal(this.maxCount, that.maxCount) + && equal(this.clientToken, that.clientToken); } @Override public int hashCode() { return Objects - .hashCode(super.hashCode(), groupNames, keyPair, noKeyPair, userData, userData, blockDeviceMappings); + .hashCode(super.hashCode(), groupNames, keyPair, noKeyPair, userData, userData, blockDeviceMappings, + maxCount, clientToken); } @Override @@ -122,6 +131,10 @@ public class EC2TemplateOptions extends TemplateOptions implements Cloneable { ImmutableSet mappings = blockDeviceMappings.build(); if (mappings.size() != 0) toString.add("blockDeviceMappings", mappings); + if (maxCount != null && maxCount.intValue() > 0) + toString.add("maxCount", maxCount); + if (clientToken != null) + toString.add("clientToken", clientToken); return toString; } @@ -200,6 +213,16 @@ public class EC2TemplateOptions extends TemplateOptions implements Cloneable { return this; } + public EC2TemplateOptions maxCount(Integer maxCount) { + this.maxCount = maxCount; + return this; + } + + public EC2TemplateOptions clientToken(String clientToken) { + this.clientToken = checkNotNull(clientToken, "clientToken"); + return this; + } + public static class Builder extends TemplateOptions.Builder { /** * @see EC2TemplateOptions#blockDeviceMappings @@ -404,6 +427,16 @@ public class EC2TemplateOptions extends TemplateOptions implements Cloneable { EC2TemplateOptions options = new EC2TemplateOptions(); return options.blockOnComplete(value); } + + public static EC2TemplateOptions maxCount(Integer maxCount) { + EC2TemplateOptions options = new EC2TemplateOptions(); + return options.maxCount(maxCount); + } + + public static EC2TemplateOptions clientToken(String clientToken) { + EC2TemplateOptions options = new EC2TemplateOptions(); + return options.clientToken(clientToken); + } } // methods that only facilitate returning the correct object type @@ -621,4 +654,20 @@ public class EC2TemplateOptions extends TemplateOptions implements Cloneable { return blockDeviceMappings.build(); } + /** + * @return the maximum number of instances to create + */ + public int getMaxCount() { + return maxCount != null ? maxCount.intValue() : 0; + } + + /** + * See here for more information. + * + * @return the optional client token string, used for idempotency + */ + public String getClientToken() { + return clientToken; + } + } diff --git a/apis/ec2/src/main/java/org/jclouds/ec2/compute/strategy/CreateKeyPairAndSecurityGroupsAsNeededAndReturnRunOptions.java b/apis/ec2/src/main/java/org/jclouds/ec2/compute/strategy/CreateKeyPairAndSecurityGroupsAsNeededAndReturnRunOptions.java index c54d9830d0..f111f8a684 100644 --- a/apis/ec2/src/main/java/org/jclouds/ec2/compute/strategy/CreateKeyPairAndSecurityGroupsAsNeededAndReturnRunOptions.java +++ b/apis/ec2/src/main/java/org/jclouds/ec2/compute/strategy/CreateKeyPairAndSecurityGroupsAsNeededAndReturnRunOptions.java @@ -22,13 +22,18 @@ import static com.google.common.base.Preconditions.checkState; import static org.jclouds.ssh.SshKeys.fingerprintPrivateKey; import static org.jclouds.ssh.SshKeys.sha1PrivateKey; -import java.util.Set; -import java.util.concurrent.ConcurrentMap; - import javax.inject.Named; import javax.inject.Provider; import javax.inject.Singleton; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSet.Builder; +import com.google.inject.Inject; import org.jclouds.compute.domain.Template; import org.jclouds.compute.functions.GroupNamingConvention; import org.jclouds.compute.functions.GroupNamingConvention.Factory; @@ -41,13 +46,6 @@ import org.jclouds.ec2.domain.KeyPair; import org.jclouds.ec2.options.RunInstancesOptions; import org.jclouds.javax.annotation.Nullable; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Function; -import com.google.common.cache.LoadingCache; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ImmutableSet.Builder; -import com.google.inject.Inject; - /** * * @author Adrian Cole @@ -101,6 +99,12 @@ public class CreateKeyPairAndSecurityGroupsAsNeededAndReturnRunOptions { "BlockDeviceMapping only available on ebs boot"); instanceOptions.withBlockDeviceMappings(blockDeviceMappings); } + + String clientToken = EC2TemplateOptions.class.cast(template.getOptions()).getClientToken(); + + if (clientToken != null) { + instanceOptions.withClientToken(clientToken); + } } return instanceOptions; } diff --git a/apis/ec2/src/main/java/org/jclouds/ec2/compute/strategy/EC2CreateNodesInGroupThenAddToSet.java b/apis/ec2/src/main/java/org/jclouds/ec2/compute/strategy/EC2CreateNodesInGroupThenAddToSet.java index e40abe9d1a..483a29bcd3 100644 --- a/apis/ec2/src/main/java/org/jclouds/ec2/compute/strategy/EC2CreateNodesInGroupThenAddToSet.java +++ b/apis/ec2/src/main/java/org/jclouds/ec2/compute/strategy/EC2CreateNodesInGroupThenAddToSet.java @@ -26,15 +26,24 @@ import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_R import static org.jclouds.compute.functions.DefaultCredentialsFromImageOrOverridingCredentials.overrideDefaultCredentialsWithOptionsIfPresent; import static org.jclouds.ec2.compute.util.EC2ComputeUtils.getZoneFromLocationOrNull; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; - import javax.annotation.Resource; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.util.concurrent.ListenableFuture; import org.jclouds.aws.util.AWSUtils; import org.jclouds.compute.config.CustomizationResponse; import org.jclouds.compute.domain.NodeMetadata; @@ -48,22 +57,12 @@ import org.jclouds.domain.LoginCredentials; import org.jclouds.ec2.EC2Api; import org.jclouds.ec2.compute.domain.RegionAndName; import org.jclouds.ec2.compute.functions.PresentInstances; +import org.jclouds.ec2.compute.options.EC2TemplateOptions; import org.jclouds.ec2.domain.RunningInstance; import org.jclouds.ec2.options.RunInstancesOptions; import org.jclouds.ec2.reference.EC2Constants; import org.jclouds.logging.Logger; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Function; -import com.google.common.base.Optional; -import com.google.common.base.Predicate; -import com.google.common.cache.LoadingCache; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Maps; -import com.google.common.collect.Multimap; -import com.google.common.util.concurrent.ListenableFuture; - /** * creates futures that correlate to * @@ -218,12 +217,23 @@ public class EC2CreateNodesInGroupThenAddToSet implements CreateNodesInGroupThen return createNodesInRegionAndZone(region, zone, group, count, template, instanceOptions); } - protected Set createNodesInRegionAndZone(String region, String zone, String group, int count, - Template template, RunInstancesOptions instanceOptions) { + protected Set createNodesInRegionAndZone(String region, String zone, String group, + int count, Template template, + RunInstancesOptions instanceOptions) { int countStarted = 0; int tries = 0; Set started = ImmutableSet. of(); + int maxCount = EC2TemplateOptions.class.cast(template.getOptions()).getMaxCount(); + int countToProvision; + + if (maxCount == 0) { + maxCount = count; + countToProvision = 1; + } else { + countToProvision = count; + } + while (countStarted < count && tries++ < count) { if (logger.isDebugEnabled()) logger.debug(">> running %d instance region(%s) zone(%s) ami(%s) params(%s)", count - countStarted, region, @@ -231,8 +241,8 @@ public class EC2CreateNodesInGroupThenAddToSet implements CreateNodesInGroupThen started = ImmutableSet.copyOf(concat( started, - client.getInstanceApi().get().runInstancesInRegion(region, zone, template.getImage().getProviderId(), 1, - count - countStarted, instanceOptions))); + client.getInstanceApi().get().runInstancesInRegion(region, zone, template.getImage().getProviderId(), + countToProvision, maxCount - countStarted, instanceOptions))); countStarted = size(started); if (countStarted < count) diff --git a/apis/ec2/src/main/java/org/jclouds/ec2/options/RunInstancesOptions.java b/apis/ec2/src/main/java/org/jclouds/ec2/options/RunInstancesOptions.java index 7ccb209a7e..427b222b4f 100644 --- a/apis/ec2/src/main/java/org/jclouds/ec2/options/RunInstancesOptions.java +++ b/apis/ec2/src/main/java/org/jclouds/ec2/options/RunInstancesOptions.java @@ -145,6 +145,15 @@ public class RunInstancesOptions extends BaseEC2RequestOptions { return this; } + /** + * Specifies the optional ClientToken field, which triggers idempotent RunInstances calls. + * See here for more details. + */ + public RunInstancesOptions withClientToken(String clientToken) { + formParameters.put("ClientToken", checkNotNull(clientToken, "clientToken")); + return this; + } + public static class Builder { /** * @see RunInstancesOptions#withKeyName(String) @@ -202,5 +211,13 @@ public class RunInstancesOptions extends BaseEC2RequestOptions { return options.withBlockDeviceMappings(mappings); } + /** + * @see RunInstancesOptions#withClientToken(String) + */ + public static RunInstancesOptions withClientToken(String clientToken) { + RunInstancesOptions options = new RunInstancesOptions(); + return options.withClientToken(clientToken); + } + } } diff --git a/apis/ec2/src/test/java/org/jclouds/ec2/compute/EC2ComputeServiceExpectTest.java b/apis/ec2/src/test/java/org/jclouds/ec2/compute/EC2ComputeServiceExpectTest.java index 24e03cf6d3..cdc832d457 100644 --- a/apis/ec2/src/test/java/org/jclouds/ec2/compute/EC2ComputeServiceExpectTest.java +++ b/apis/ec2/src/test/java/org/jclouds/ec2/compute/EC2ComputeServiceExpectTest.java @@ -17,23 +17,24 @@ package org.jclouds.ec2.compute; import static org.jclouds.ec2.compute.options.EC2TemplateOptions.Builder.blockUntilRunning; +import static org.jclouds.ec2.compute.options.EC2TemplateOptions.Builder.maxCount; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import javax.ws.rs.core.MediaType; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import org.jclouds.compute.ComputeService; import org.jclouds.compute.domain.NodeMetadata; import org.jclouds.ec2.compute.internal.BaseEC2ComputeServiceExpectTest; import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpResponse; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableMap.Builder; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; - /** * Tests the compute service abstraction of the EC2 api. * @@ -41,6 +42,74 @@ import com.google.common.collect.Iterables; */ @Test(groups = "unit", testName = "EC2ComputeServiceExpectTest") public class EC2ComputeServiceExpectTest extends BaseEC2ComputeServiceExpectTest { + protected HttpRequest createFirstTagRequest; + protected HttpRequest createSecondTagRequest; + protected HttpRequest createThirdTagRequest; + + @BeforeClass + @Override + protected void setupDefaultRequests() { + super.setupDefaultRequests(); + createFirstTagRequest = + formSigner.filter(HttpRequest.builder() + .method("POST") + .endpoint("https://ec2.us-east-1.amazonaws.com/") + .addHeader("Host", "ec2.us-east-1.amazonaws.com") + .payload( + payloadFromStringWithContentType( + "Action=CreateTags" + + "&ResourceId.1=i-2ba64342" + + "&Signature=Trp5e5%2BMqeBeBZbLYa9s9gxahQ9nkx6ETfsGl82IV8Y%3D" + + "&SignatureMethod=HmacSHA256" + + "&SignatureVersion=2" + + "&Tag.1.Key=Name" + + "&Tag.1.Value=test-2ba64342" + + "&Timestamp=2012-04-16T15%3A54%3A08.897Z" + + "&Version=2010-08-31" + + "&AWSAccessKeyId=identity", + "application/x-www-form-urlencoded")) + .build()); + + createSecondTagRequest = + formSigner.filter(HttpRequest.builder() + .method("POST") + .endpoint("https://ec2.us-east-1.amazonaws.com/") + .addHeader("Host", "ec2.us-east-1.amazonaws.com") + .payload( + payloadFromStringWithContentType( + "Action=CreateTags" + + "&ResourceId.1=i-2bc64242" + + "&Signature=Trp5e5%2BMqeBeBZbLYa9s9gxahQ9nkx6ETfsGl82IV8Y%3D" + + "&SignatureMethod=HmacSHA256" + + "&SignatureVersion=2" + + "&Tag.1.Key=Name" + + "&Tag.1.Value=test-2bc64242" + + "&Timestamp=2012-04-16T15%3A54%3A08.897Z" + + "&Version=2010-08-31" + + "&AWSAccessKeyId=identity", + "application/x-www-form-urlencoded")) + .build()); + + createThirdTagRequest = + formSigner.filter(HttpRequest.builder() + .method("POST") + .endpoint("https://ec2.us-east-1.amazonaws.com/") + .addHeader("Host", "ec2.us-east-1.amazonaws.com") + .payload( + payloadFromStringWithContentType( + "Action=CreateTags" + + "&ResourceId.1=i-2be64332" + + "&Signature=Trp5e5%2BMqeBeBZbLYa9s9gxahQ9nkx6ETfsGl82IV8Y%3D" + + "&SignatureMethod=HmacSHA256" + + "&SignatureVersion=2" + + "&Tag.1.Key=Name" + + "&Tag.1.Value=test-2be64332" + + "&Timestamp=2012-04-16T15%3A54%3A08.897Z" + + "&Version=2010-08-31" + + "&AWSAccessKeyId=identity", + "application/x-www-form-urlencoded")) + .build()); + } public void testCreateNodeWithGeneratedKeyPairAndOverriddenLoginUser() throws Exception { Builder requestResponseMap = ImmutableMap. builder(); @@ -67,6 +136,60 @@ public class EC2ComputeServiceExpectTest extends BaseEC2ComputeServiceExpectTest assertNotNull(node.getCredentials().getPrivateKey()); } + public void testCreateThreeNodesWithMaxCountThree() throws Exception { + + Builder requestResponseMap = ImmutableMap. builder(); + requestResponseMap.put(describeRegionsRequest, describeRegionsResponse); + requestResponseMap.put(describeAvailabilityZonesRequest, describeAvailabilityZonesResponse); + requestResponseMap.put(describeImagesRequest, describeImagesResponse); + requestResponseMap.put(createKeyPairRequest, createKeyPairResponse); + requestResponseMap.put(createSecurityGroupRequest, createSecurityGroupResponse); + requestResponseMap.put(describeSecurityGroupRequest, describeSecurityGroupResponse); + requestResponseMap.put(authorizeSecurityGroupIngressRequest22, authorizeSecurityGroupIngressResponse); + requestResponseMap.put(authorizeSecurityGroupIngressRequestGroup, authorizeSecurityGroupIngressResponse); + requestResponseMap.put(runThreeInstancesRequest, runThreeInstancesResponse); + requestResponseMap.put(describeInstanceRequest, describeInstanceResponse); + requestResponseMap.put(describeInstanceThreeIdsRequest, describeInstanceThreeIdsResponse); + requestResponseMap.put(describeImageRequest, describeImagesResponse); + requestResponseMap.put(createFirstTagRequest, createTagsResponse); + requestResponseMap.put(createSecondTagRequest, createTagsResponse); + requestResponseMap.put(createThirdTagRequest, createTagsResponse); + + ComputeService apiThatCreatesNode = requestsSendResponses(requestResponseMap.build()); + + NodeMetadata node = Iterables.getFirst(apiThatCreatesNode.createNodesInGroup("test", 3, + maxCount(3).blockUntilRunning(false).overrideLoginUser("ec2-user")), null); + assertNotNull(node, "Node should exist"); + assertEquals(node.getCredentials().getUser(), "ec2-user", "User should be ec2-user"); + } + + public void testCreateThreeNodesWithMaxCountFourGetThreeNodes() throws Exception { + + Builder requestResponseMap = ImmutableMap. builder(); + requestResponseMap.put(describeRegionsRequest, describeRegionsResponse); + requestResponseMap.put(describeAvailabilityZonesRequest, describeAvailabilityZonesResponse); + requestResponseMap.put(describeImagesRequest, describeImagesResponse); + requestResponseMap.put(createKeyPairRequest, createKeyPairResponse); + requestResponseMap.put(createSecurityGroupRequest, createSecurityGroupResponse); + requestResponseMap.put(describeSecurityGroupRequest, describeSecurityGroupResponse); + requestResponseMap.put(authorizeSecurityGroupIngressRequest22, authorizeSecurityGroupIngressResponse); + requestResponseMap.put(authorizeSecurityGroupIngressRequestGroup, authorizeSecurityGroupIngressResponse); + requestResponseMap.put(runThreeToFourInstancesRequest, runThreeInstancesResponse); + requestResponseMap.put(describeInstanceRequest, describeInstanceResponse); + requestResponseMap.put(describeInstanceThreeIdsRequest, describeInstanceThreeIdsResponse); + requestResponseMap.put(describeImageRequest, describeImagesResponse); + requestResponseMap.put(createFirstTagRequest, createTagsResponse); + requestResponseMap.put(createSecondTagRequest, createTagsResponse); + requestResponseMap.put(createThirdTagRequest, createTagsResponse); + + ComputeService apiThatCreatesNode = requestsSendResponses(requestResponseMap.build()); + + NodeMetadata node = Iterables.getFirst(apiThatCreatesNode.createNodesInGroup("test", 3, + maxCount(4).blockUntilRunning(false).overrideLoginUser("ec2-user")), null); + assertNotNull(node, "Node should exist"); + assertEquals(node.getCredentials().getUser(), "ec2-user", "User should be ec2-user"); + } + public void testCreateNodeWithSpecifiedName() throws Exception { HttpRequest createNamedTagsRequest = formSigner.filter(HttpRequest.builder() diff --git a/apis/ec2/src/test/java/org/jclouds/ec2/compute/EC2ComputeServiceLiveTest.java b/apis/ec2/src/test/java/org/jclouds/ec2/compute/EC2ComputeServiceLiveTest.java index e37e881da7..70f5ef9656 100644 --- a/apis/ec2/src/test/java/org/jclouds/ec2/compute/EC2ComputeServiceLiveTest.java +++ b/apis/ec2/src/test/java/org/jclouds/ec2/compute/EC2ComputeServiceLiveTest.java @@ -20,6 +20,7 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; +import java.security.SecureRandom; import java.util.Map; import java.util.Properties; import java.util.Set; @@ -111,6 +112,8 @@ public class EC2ComputeServiceLiveTest extends BaseComputeServiceLiveTest { @Test(enabled = true, dependsOnMethods = "testCompareSizes") public void testExtendedOptionsAndLogin() throws Exception { + final SecureRandom random = new SecureRandom(); + SecurityGroupApi securityGroupClient = view.unwrapApi(EC2Api.class) .getSecurityGroupApi().get(); @@ -125,6 +128,7 @@ public class EC2ComputeServiceLiveTest extends BaseComputeServiceLiveTest { TemplateOptions options = client.templateOptions(); options.as(EC2TemplateOptions.class).securityGroups(group); + options.as(EC2TemplateOptions.class).clientToken(Integer.toHexString(random.nextInt(65536 * 1024))); String startedId = null; try { @@ -151,6 +155,9 @@ public class EC2ComputeServiceLiveTest extends BaseComputeServiceLiveTest { assert first.getCredentials() != null : first; assert first.getCredentials().identity != null : first; + // Verify that the output of createNodesInGroup is the same. + assertEquals(client.createNodesInGroup(group, 1, options), nodes, "Idempotency failing - got different instances"); + startedId = Iterables.getOnlyElement(nodes).getProviderId(); RunningInstance instance = getInstance(instanceClient, startedId); diff --git a/apis/ec2/src/test/java/org/jclouds/ec2/compute/internal/BaseEC2ComputeServiceExpectTest.java b/apis/ec2/src/test/java/org/jclouds/ec2/compute/internal/BaseEC2ComputeServiceExpectTest.java index 610cfb7af5..4178e74e4e 100644 --- a/apis/ec2/src/test/java/org/jclouds/ec2/compute/internal/BaseEC2ComputeServiceExpectTest.java +++ b/apis/ec2/src/test/java/org/jclouds/ec2/compute/internal/BaseEC2ComputeServiceExpectTest.java @@ -18,9 +18,8 @@ package org.jclouds.ec2.compute.internal; import static org.jclouds.location.reference.LocationConstants.PROPERTY_REGIONS; -import java.util.Properties; - import javax.ws.rs.core.MediaType; +import java.util.Properties; import org.jclouds.compute.ComputeService; import org.jclouds.compute.ComputeServiceContext; @@ -54,10 +53,15 @@ public abstract class BaseEC2ComputeServiceExpectTest extends BaseEC2ComputeServ protected HttpRequest authorizeSecurityGroupIngressRequestGroup; protected HttpRequest runInstancesRequest; protected HttpResponse runInstancesResponse; + protected HttpRequest runThreeInstancesRequest; + protected HttpRequest runThreeToFourInstancesRequest; + protected HttpResponse runThreeInstancesResponse; protected HttpRequest describeInstanceRequest; protected HttpResponse describeInstanceResponse; protected HttpRequest describeInstanceMultiIdsRequest; protected HttpResponse describeInstanceMultiIdsResponse; + protected HttpRequest describeInstanceThreeIdsRequest; + protected HttpResponse describeInstanceThreeIdsResponse; protected HttpRequest describeImageRequest; protected HttpRequest createTagsRequest; protected HttpResponse createTagsResponse; @@ -177,14 +181,45 @@ public abstract class BaseEC2ComputeServiceExpectTest extends BaseEC2ComputeServ HttpResponse.builder().statusCode(200) .payload(payloadFromResourceWithContentType( "/new_instance.xml", MediaType.APPLICATION_XML)).build(); - - describeInstanceRequest = + + runThreeInstancesRequest = + formSigner.filter(HttpRequest.builder() + .method("POST") + .endpoint("https://ec2." + region + ".amazonaws.com/") + .addHeader("Host", "ec2." + region + ".amazonaws.com") + .addFormParam("Action", "RunInstances") + .addFormParam("ImageId", "ami-be3adfd7") + .addFormParam("InstanceType", "m1.small") + .addFormParam("KeyName", "jclouds#test#0") + .addFormParam("MaxCount", "3") + .addFormParam("MinCount", "3") + .addFormParam("SecurityGroup.1", "jclouds#test").build()); + + runThreeToFourInstancesRequest = + formSigner.filter(HttpRequest.builder() + .method("POST") + .endpoint("https://ec2." + region + ".amazonaws.com/") + .addHeader("Host", "ec2." + region + ".amazonaws.com") + .addFormParam("Action", "RunInstances") + .addFormParam("ImageId", "ami-be3adfd7") + .addFormParam("InstanceType", "m1.small") + .addFormParam("KeyName", "jclouds#test#0") + .addFormParam("MaxCount", "4") + .addFormParam("MinCount", "3") + .addFormParam("SecurityGroup.1", "jclouds#test").build()); + + runThreeInstancesResponse = + HttpResponse.builder().statusCode(200) + .payload(payloadFromResourceWithContentType( + "/run_instances_three.xml", MediaType.APPLICATION_XML)).build(); + + describeInstanceRequest = formSigner.filter(HttpRequest.builder() - .method("POST") - .endpoint("https://ec2." + region + ".amazonaws.com/") - .addHeader("Host", "ec2." + region + ".amazonaws.com") - .addFormParam("Action", "DescribeInstances") - .addFormParam("InstanceId.1", "i-2baa5550").build()); + .method("POST") + .endpoint("https://ec2." + region + ".amazonaws.com/") + .addHeader("Host", "ec2." + region + ".amazonaws.com") + .addFormParam("Action", "DescribeInstances") + .addFormParam("InstanceId.1", "i-2baa5550").build()); describeInstanceResponse = HttpResponse.builder().statusCode(200) @@ -205,6 +240,21 @@ public abstract class BaseEC2ComputeServiceExpectTest extends BaseEC2ComputeServ .payload(payloadFromResourceWithContentType( "/describe_instances_multiple.xml", MediaType.APPLICATION_XML)).build(); + describeInstanceThreeIdsRequest = + formSigner.filter(HttpRequest.builder() + .method("POST") + .endpoint("https://ec2." + region + ".amazonaws.com/") + .addHeader("Host", "ec2." + region + ".amazonaws.com") + .addFormParam("Action", "DescribeInstances") + .addFormParam("InstanceId.1", "i-2ba64342") + .addFormParam("InstanceId.2", "i-2bc64242") + .addFormParam("InstanceId.3", "i-2be64332").build()); + + describeInstanceThreeIdsResponse = + HttpResponse.builder().statusCode(200) + .payload(payloadFromResourceWithContentType( + "/describe_instances_multiple.xml", MediaType.APPLICATION_XML)).build(); + //TODO: duplicate.. shouldn't need this describeImageRequest = formSigner.filter(HttpRequest.builder() diff --git a/apis/ec2/src/test/java/org/jclouds/ec2/compute/options/EC2TemplateOptionsTest.java b/apis/ec2/src/test/java/org/jclouds/ec2/compute/options/EC2TemplateOptionsTest.java index 90b92c3be1..3025ca0712 100644 --- a/apis/ec2/src/test/java/org/jclouds/ec2/compute/options/EC2TemplateOptionsTest.java +++ b/apis/ec2/src/test/java/org/jclouds/ec2/compute/options/EC2TemplateOptionsTest.java @@ -24,14 +24,14 @@ import static org.jclouds.ec2.compute.options.EC2TemplateOptions.Builder.keyPair import static org.jclouds.ec2.compute.options.EC2TemplateOptions.Builder.noKeyPair; import static org.jclouds.ec2.compute.options.EC2TemplateOptions.Builder.securityGroups; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; import java.io.IOException; +import com.google.common.collect.ImmutableSet; import org.jclouds.compute.options.TemplateOptions; import org.testng.annotations.Test; -import com.google.common.collect.ImmutableSet; - /** * Tests possible uses of EC2TemplateOptions and EC2TemplateOptions.Builder.* * @@ -285,4 +285,43 @@ public class EC2TemplateOptionsTest { assertEquals(options.getInboundPorts()[0], 22); assertEquals(options.getInboundPorts()[1], 30); } + + @Test + public void testMaxCountDefault() { + EC2TemplateOptions options = new EC2TemplateOptions(); + assertEquals(options.getMaxCount(), 0); + } + + @Test + public void testMaxCount() { + EC2TemplateOptions options = new EC2TemplateOptions(); + options.maxCount(2); + assertEquals(options.getMaxCount(), 2); + } + + @Test + public void testMaxCountNull() { + EC2TemplateOptions options = new EC2TemplateOptions(); + options.maxCount(null); + assertEquals(options.getMaxCount(), 0); + } + + @Test + public void testClientTokenDefault() { + EC2TemplateOptions options = new EC2TemplateOptions(); + assertNull(options.getClientToken()); + } + + @Test + public void testClientToken() { + EC2TemplateOptions options = new EC2TemplateOptions(); + options.clientToken("some-token"); + assertEquals(options.getClientToken(), "some-token"); + } + + @Test(expectedExceptions = NullPointerException.class) + public void testClientTokenNPE() { + EC2TemplateOptions options = new EC2TemplateOptions(); + options.clientToken(null); + } } diff --git a/apis/ec2/src/test/java/org/jclouds/ec2/compute/strategy/CreateKeyPairAndSecurityGroupsAsNeededAndReturnRunOptionsTest.java b/apis/ec2/src/test/java/org/jclouds/ec2/compute/strategy/CreateKeyPairAndSecurityGroupsAsNeededAndReturnRunOptionsTest.java index 2babb8d338..0c57409249 100644 --- a/apis/ec2/src/test/java/org/jclouds/ec2/compute/strategy/CreateKeyPairAndSecurityGroupsAsNeededAndReturnRunOptionsTest.java +++ b/apis/ec2/src/test/java/org/jclouds/ec2/compute/strategy/CreateKeyPairAndSecurityGroupsAsNeededAndReturnRunOptionsTest.java @@ -23,13 +23,16 @@ import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; import static org.testng.Assert.assertEquals; +import javax.inject.Provider; import java.lang.reflect.Method; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; -import javax.inject.Provider; - +import com.google.common.base.Function; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; import org.jclouds.aws.domain.Region; import org.jclouds.compute.domain.Hardware; import org.jclouds.compute.domain.Template; @@ -46,11 +49,6 @@ import org.jclouds.ec2.options.RunInstancesOptions; import org.jclouds.scriptbuilder.domain.Statements; import org.testng.annotations.Test; -import com.google.common.base.Function; -import com.google.common.cache.LoadingCache; -import com.google.common.collect.ImmutableMultimap; -import com.google.common.collect.ImmutableSet; - /** * @author Adrian Cole */ @@ -130,6 +128,7 @@ public class CreateKeyPairAndSecurityGroupsAsNeededAndReturnRunOptionsTest { systemGeneratedKeyPairName); expect(strategy.getSecurityGroupsForTagAndOptions(region, group, options)).andReturn(generatedGroups); expect(options.getUserData()).andReturn(null); + expect(options.getClientToken()).andReturn(null); // replay mocks replay(options); @@ -181,6 +180,7 @@ public class CreateKeyPairAndSecurityGroupsAsNeededAndReturnRunOptionsTest { expect(template.getHardware()).andReturn(size).atLeastOnce(); expect(template.getOptions()).andReturn(options).atLeastOnce(); expect(options.getBlockDeviceMappings()).andReturn(ImmutableSet. of()).atLeastOnce(); + expect(options.getClientToken()).andReturn("some-token"); expect(strategy.createNewKeyPairUnlessUserSpecifiedOtherwise(region, group, options)).andReturn( systemGeneratedKeyPairName); expect(strategy.getSecurityGroupsForTagAndOptions(region, group, options)).andReturn(generatedGroups); @@ -197,7 +197,8 @@ public class CreateKeyPairAndSecurityGroupsAsNeededAndReturnRunOptionsTest { assertEquals( customize.buildFormParameters().entries(), ImmutableMultimap. of("InstanceType", size.getProviderId(), "SecurityGroup.1", "group", - "KeyName", systemGeneratedKeyPairName, "UserData", base64().encode("hello".getBytes())).entries()); + "KeyName", systemGeneratedKeyPairName, "UserData", base64().encode("hello".getBytes()), + "ClientToken", "some-token").entries()); assertEquals(customize.buildRequestHeaders(), ImmutableMultimap. of()); assertEquals(customize.buildStringPayload(), null); diff --git a/apis/ec2/src/test/java/org/jclouds/ec2/compute/strategy/EC2CreateNodesInGroupThenAddToSetTest.java b/apis/ec2/src/test/java/org/jclouds/ec2/compute/strategy/EC2CreateNodesInGroupThenAddToSetTest.java index f0201ad33d..1be1c8c151 100644 --- a/apis/ec2/src/test/java/org/jclouds/ec2/compute/strategy/EC2CreateNodesInGroupThenAddToSetTest.java +++ b/apis/ec2/src/test/java/org/jclouds/ec2/compute/strategy/EC2CreateNodesInGroupThenAddToSetTest.java @@ -26,6 +26,11 @@ import static org.easymock.EasyMock.verify; import java.util.Map; import java.util.Set; +import com.google.common.base.Optional; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Multimap; import org.easymock.IArgumentMatcher; import org.jclouds.compute.config.CustomizationResponse; import org.jclouds.compute.domain.Hardware; @@ -49,18 +54,12 @@ import org.jclouds.ec2.compute.functions.RunningInstanceToNodeMetadata; import org.jclouds.ec2.compute.options.EC2TemplateOptions; import org.jclouds.ec2.domain.Reservation; import org.jclouds.ec2.domain.RunningInstance; -import org.jclouds.ec2.options.RunInstancesOptions; import org.jclouds.ec2.features.ElasticIPAddressApi; import org.jclouds.ec2.features.InstanceApi; +import org.jclouds.ec2.options.RunInstancesOptions; import org.testng.Assert; import org.testng.annotations.Test; -import com.google.common.base.Optional; -import com.google.common.cache.LoadingCache; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; -import com.google.common.collect.Multimap; - /** * @author Adrian Cole */ @@ -124,6 +123,7 @@ public class EC2CreateNodesInGroupThenAddToSetTest { expect(input.options.getLoginPassword()).andReturn(null); expect(input.options.getLoginPrivateKey()).andReturn(null); expect(input.options.shouldAuthenticateSudo()).andReturn(null); + expect(input.options.getMaxCount()).andReturn(0); expect( strategy.utils.customizeNodesAndAddToGoodMapOrPutExceptionIntoBadMap(eq(input.options), @@ -222,7 +222,7 @@ public class EC2CreateNodesInGroupThenAddToSetTest { expect(input.options.getLoginPassword()).andReturn(null); expect(input.options.getLoginPrivateKey()).andReturn(null); expect(input.options.shouldAuthenticateSudo()).andReturn(null); - + expect(input.options.getMaxCount()).andReturn(0); expect(strategy.runningInstanceToNodeMetadata.apply(instance)).andReturn(nodeMetadata); expect( diff --git a/apis/ec2/src/test/java/org/jclouds/ec2/options/RunInstancesOptionsTest.java b/apis/ec2/src/test/java/org/jclouds/ec2/options/RunInstancesOptionsTest.java index f9c292a38a..d46535d4be 100644 --- a/apis/ec2/src/test/java/org/jclouds/ec2/options/RunInstancesOptionsTest.java +++ b/apis/ec2/src/test/java/org/jclouds/ec2/options/RunInstancesOptionsTest.java @@ -18,6 +18,7 @@ package org.jclouds.ec2.options; import static org.jclouds.ec2.options.RunInstancesOptions.Builder.asType; import static org.jclouds.ec2.options.RunInstancesOptions.Builder.withBlockDeviceMappings; +import static org.jclouds.ec2.options.RunInstancesOptions.Builder.withClientToken; import static org.jclouds.ec2.options.RunInstancesOptions.Builder.withKernelId; import static org.jclouds.ec2.options.RunInstancesOptions.Builder.withKeyName; import static org.jclouds.ec2.options.RunInstancesOptions.Builder.withRamdisk; @@ -25,14 +26,13 @@ import static org.jclouds.ec2.options.RunInstancesOptions.Builder.withSecurityGr import static org.jclouds.ec2.options.RunInstancesOptions.Builder.withUserData; import static org.testng.Assert.assertEquals; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import org.jclouds.ec2.domain.BlockDeviceMapping; import org.jclouds.ec2.domain.InstanceType; import org.jclouds.http.options.HttpRequestOptions; import org.testng.annotations.Test; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; - /** * Tests possible uses of RunInstancesOptions and RunInstancesOptions.Builder.* * @@ -244,4 +244,15 @@ public class RunInstancesOptionsTest { withBlockDeviceMappings(null); } + @Test + public void testWithClientToken() { + RunInstancesOptions options = withClientToken("some-token"); + assertEquals(options.buildFormParameters().get("ClientToken"), ImmutableList.of("some-token")); + } + + @Test(expectedExceptions = NullPointerException.class) + public void testWithClientTokenNPE() { + withClientToken(null); + } + } diff --git a/apis/ec2/src/test/resources/describe_instances_three_ids.xml b/apis/ec2/src/test/resources/describe_instances_three_ids.xml new file mode 100644 index 0000000000..dbf3b71747 --- /dev/null +++ b/apis/ec2/src/test/resources/describe_instances_three_ids.xml @@ -0,0 +1,74 @@ + + r-47a5402e + AIDADH4IGTRXXKCD + + + default + + + + + i-2ba64342 + ami-aecd60c7 + + 0 + pending + + + + example-key-name + 0 + m1.small + 2007-08-07T11:51:50.000Z + + us-east-1b + + + enabled + + xen + + + i-2bc64242 + ami-aecd60c7 + + 0 + pending + + + + example-key-name + 1 + m1.small + 2007-08-07T11:51:50.000Z + + us-east-1b + + + enabled + + xen + + + i-2be64332 + ami-aecd60c7 + + 0 + pending + + + + example-key-name + 2 + m1.small + 2007-08-07T11:51:50.000Z + + us-east-1b + + + enabled + + xen + + + \ No newline at end of file diff --git a/apis/ec2/src/test/resources/run_instances_three.xml b/apis/ec2/src/test/resources/run_instances_three.xml new file mode 100644 index 0000000000..6530a7fc18 --- /dev/null +++ b/apis/ec2/src/test/resources/run_instances_three.xml @@ -0,0 +1,74 @@ + + r-47a5402e + AIDADH4IGTRXXKCD + + + default + + + + + i-2ba64342 + ami-aecd60c7 + + 0 + pending + + + + example-key-name + 0 + m1.small + 2007-08-07T11:51:50.000Z + + us-east-1b + + + enabled + + xen + + + i-2bc64242 + ami-aecd60c7 + + 0 + pending + + + + example-key-name + 1 + m1.small + 2007-08-07T11:51:50.000Z + + us-east-1b + + + enabled + + xen + + + i-2be64332 + ami-aecd60c7 + + 0 + pending + + + + example-key-name + 2 + m1.small + 2007-08-07T11:51:50.000Z + + us-east-1b + + + enabled + + xen + + + \ No newline at end of file