From 082158ac3f822eac1d2dc982e084ff4fcdb18dc7 Mon Sep 17 00:00:00 2001 From: David Ribeiro Alves Date: Thu, 3 May 2012 03:55:40 +0100 Subject: [PATCH] image extension working on hpcloud, virtualbox and aws-ec2, cloudservers implemented but has issues --- .../compute/CloudServersImageExtension.java | 165 +++++++++++++++++ ...oudServersComputeServiceContextModule.java | 13 +- .../CloudServersImageExtensionLiveTest.java | 47 +++++ .../ec2/compute/EC2ImageExtension.java | 169 ++++++++++++++++++ .../EC2ComputeServiceContextModule.java | 8 + .../EC2ComputeServiceDependenciesModule.java | 4 + .../EC2CreateNodesInGroupThenAddToSet.java | 16 +- .../compute/EC2ImageExtensionLiveTest.java | 47 +++++ .../nova/v1_1/compute/NovaImageExtension.java | 79 +++++++- ...sWithGroupEncodedIntoNameThenAddToSet.java | 61 ++++--- .../compute/NovaImageExtensionLiveTest.java | 47 +++++ .../org/jclouds/compute/ImageExtension.java | 4 +- .../internal/BaseImageExtensionLiveTest.java | 91 ++++++++-- .../compute/VirtualBoxImageExtension.java | 6 +- .../AWSEC2ComputeServiceContextModule.java | 7 + ...WSEC2ComputeServiceDependenciesModule.java | 4 + .../compute/AWSEC2ImageExtensionLiveTest.java | 67 +++++++ .../HPCloudComputeImageExtensionLivetest.java | 5 + 18 files changed, 781 insertions(+), 59 deletions(-) create mode 100644 apis/cloudservers/src/main/java/org/jclouds/cloudservers/compute/CloudServersImageExtension.java create mode 100644 apis/cloudservers/src/test/java/org/jclouds/cloudservers/compute/CloudServersImageExtensionLiveTest.java create mode 100644 apis/ec2/src/main/java/org/jclouds/ec2/compute/EC2ImageExtension.java create mode 100644 apis/ec2/src/test/java/org/jclouds/ec2/compute/EC2ImageExtensionLiveTest.java create mode 100644 apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/compute/NovaImageExtensionLiveTest.java create mode 100644 providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/AWSEC2ImageExtensionLiveTest.java diff --git a/apis/cloudservers/src/main/java/org/jclouds/cloudservers/compute/CloudServersImageExtension.java b/apis/cloudservers/src/main/java/org/jclouds/cloudservers/compute/CloudServersImageExtension.java new file mode 100644 index 0000000000..2914789c53 --- /dev/null +++ b/apis/cloudservers/src/main/java/org/jclouds/cloudservers/compute/CloudServersImageExtension.java @@ -0,0 +1,165 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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. + */ + +package org.jclouds.cloudservers.compute; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import java.util.NoSuchElementException; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Resource; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.jclouds.Constants; +import org.jclouds.cloudservers.CloudServersClient; +import org.jclouds.cloudservers.domain.Server; +import org.jclouds.cloudservers.options.ListOptions; +import org.jclouds.compute.ImageExtension; +import org.jclouds.compute.domain.CloneImageTemplate; +import org.jclouds.compute.domain.Image; +import org.jclouds.compute.domain.ImageTemplate; +import org.jclouds.compute.domain.ImageTemplateBuilder; +import org.jclouds.compute.reference.ComputeServiceConstants; +import org.jclouds.concurrent.Futures; +import org.jclouds.logging.Logger; +import org.jclouds.predicates.PredicateWithResult; +import org.jclouds.predicates.Retryables; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.util.concurrent.ListenableFuture; + +/** + * CloudServers implementation of {@link ImageExtension} + * + * @author David Alves + * + */ +@Singleton +public class CloudServersImageExtension implements ImageExtension { + + @Resource + @Named(ComputeServiceConstants.COMPUTE_LOGGER) + protected Logger logger = Logger.NULL; + + private final CloudServersClient syncClient; + private final ExecutorService executor; + private final Function cloudserversImageToImage; + @com.google.inject.Inject(optional = true) + @Named("IMAGE_MAX_WAIT") + long maxWait = 3600; + @com.google.inject.Inject(optional = true) + @Named("IMAGE_WAIT_PERIOD") + long waitPeriod = 1; + + @Inject + public CloudServersImageExtension(CloudServersClient novaClient, + @Named(Constants.PROPERTY_USER_THREADS) ExecutorService userThreads, + Function cloudserversImageToImage) { + this.syncClient = checkNotNull(novaClient); + this.executor = userThreads; + this.cloudserversImageToImage = cloudserversImageToImage; + } + + @Override + public ImageTemplate buildImageTemplateFromNode(String name, final String id) { + Server server = syncClient.getServer(Integer.parseInt(id)); + if (server == null) + throw new NoSuchElementException("Cannot find server with id: " + id); + CloneImageTemplate template = new ImageTemplateBuilder.CloneImageTemplateBuilder().nodeId(id).name(name).build(); + return template; + } + + @Override + public ListenableFuture createImage(ImageTemplate template) { + checkState(template instanceof CloneImageTemplate, + " openstack-nova only supports creating images through cloning."); + CloneImageTemplate cloneTemplate = (CloneImageTemplate) template; + final org.jclouds.cloudservers.domain.Image image = syncClient.createImageFromServer(cloneTemplate.getName(), + Integer.parseInt(cloneTemplate.getSourceNodeId())); + return Futures.makeListenable(executor.submit(new Callable() { + @Override + public Image call() throws Exception { + return Retryables.retryGettingResultOrFailing(new PredicateWithResult() { + + org.jclouds.cloudservers.domain.Image result; + RuntimeException lastFailure; + + @Override + public boolean apply(Integer input) { + result = checkNotNull(findImage(input)); + switch (result.getStatus()) { + case ACTIVE: + logger.info("<< Image %s is available for use.", input); + return true; + case UNKNOWN: + case SAVING: + logger.debug("<< Image %s is not available yet.", input); + return false; + default: + lastFailure = new IllegalStateException("Image was not created: " + input); + throw lastFailure; + } + } + + @Override + public Image getResult() { + return cloudserversImageToImage.apply(image); + } + + @Override + public Throwable getLastFailure() { + return lastFailure; + } + }, image.getId(), maxWait, waitPeriod, TimeUnit.SECONDS, + "Image was not created within the time limit, Giving up! [Limit: " + maxWait + " secs.]"); + } + }), executor); + + } + + @Override + public boolean deleteImage(String id) { + try { + this.syncClient.deleteImage(Integer.parseInt(id)); + } catch (Exception e) { + return false; + } + return true; + } + + private org.jclouds.cloudservers.domain.Image findImage(final int id) { + return Iterables.tryFind(syncClient.listImages(ListOptions.NONE), + new Predicate() { + @Override + public boolean apply(org.jclouds.cloudservers.domain.Image input) { + return input.getId() == id; + } + }).orNull(); + + } + +} diff --git a/apis/cloudservers/src/main/java/org/jclouds/cloudservers/compute/config/CloudServersComputeServiceContextModule.java b/apis/cloudservers/src/main/java/org/jclouds/cloudservers/compute/config/CloudServersComputeServiceContextModule.java index 9747eca77f..6375c15277 100644 --- a/apis/cloudservers/src/main/java/org/jclouds/cloudservers/compute/config/CloudServersComputeServiceContextModule.java +++ b/apis/cloudservers/src/main/java/org/jclouds/cloudservers/compute/config/CloudServersComputeServiceContextModule.java @@ -22,6 +22,7 @@ import java.util.Map; import javax.inject.Singleton; +import org.jclouds.cloudservers.compute.CloudServersImageExtension; import org.jclouds.cloudservers.compute.functions.CloudServersImageToImage; import org.jclouds.cloudservers.compute.functions.CloudServersImageToOperatingSystem; import org.jclouds.cloudservers.compute.functions.FlavorToHardware; @@ -31,6 +32,7 @@ import org.jclouds.cloudservers.domain.Flavor; import org.jclouds.cloudservers.domain.Server; import org.jclouds.cloudservers.domain.ServerStatus; import org.jclouds.compute.ComputeServiceAdapter; +import org.jclouds.compute.ImageExtension; import org.jclouds.compute.config.ComputeServiceAdapterContextModule; import org.jclouds.compute.domain.Hardware; import org.jclouds.compute.domain.Image; @@ -43,7 +45,9 @@ import org.jclouds.functions.IdentityFunction; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; +import com.google.common.base.Optional; import com.google.common.collect.ImmutableMap; +import com.google.inject.Injector; import com.google.inject.Provides; import com.google.inject.TypeLiteral; @@ -77,6 +81,9 @@ public class CloudServersComputeServiceContextModule extends // we aren't converting location from a provider-specific type bind(new TypeLiteral>() { }).to((Class) IdentityFunction.class); + + bind(new TypeLiteral() { + }).to(CloudServersImageExtension.class); } @@ -112,6 +119,10 @@ public class CloudServersComputeServiceContextModule extends Map provideServerToNodeState() { return serverToNodeState; } - + + @Override + protected Optional provideImageExtension(Injector i) { + return Optional.of(i.getInstance(ImageExtension.class)); + } } diff --git a/apis/cloudservers/src/test/java/org/jclouds/cloudservers/compute/CloudServersImageExtensionLiveTest.java b/apis/cloudservers/src/test/java/org/jclouds/cloudservers/compute/CloudServersImageExtensionLiveTest.java new file mode 100644 index 0000000000..14e7a889b0 --- /dev/null +++ b/apis/cloudservers/src/test/java/org/jclouds/cloudservers/compute/CloudServersImageExtensionLiveTest.java @@ -0,0 +1,47 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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. + */ + +package org.jclouds.cloudservers.compute; + +import org.jclouds.compute.ImageExtension; +import org.jclouds.compute.internal.BaseImageExtensionLiveTest; +import org.jclouds.sshj.config.SshjSshClientModule; +import org.testng.annotations.Test; + +import com.google.inject.Module; + +/** + * Live test for cloudservers {@link ImageExtension} implementation + * + * @author David Alves + * + */ +@Test(groups = "live", singleThreaded = true, testName = "CloudServersImageExtensionLiveTest") +public class CloudServersImageExtensionLiveTest extends BaseImageExtensionLiveTest { + + public CloudServersImageExtensionLiveTest() { + provider = "cloudservers"; + } + + @Override + protected Module getSshModule() { + return new SshjSshClientModule(); + } + +} diff --git a/apis/ec2/src/main/java/org/jclouds/ec2/compute/EC2ImageExtension.java b/apis/ec2/src/main/java/org/jclouds/ec2/compute/EC2ImageExtension.java new file mode 100644 index 0000000000..2dc958b934 --- /dev/null +++ b/apis/ec2/src/main/java/org/jclouds/ec2/compute/EC2ImageExtension.java @@ -0,0 +1,169 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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. + */ + +package org.jclouds.ec2.compute; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import java.util.NoSuchElementException; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Resource; +import javax.inject.Inject; +import javax.inject.Named; + +import org.jclouds.Constants; +import org.jclouds.aws.util.AWSUtils; +import org.jclouds.compute.ImageExtension; +import org.jclouds.compute.domain.CloneImageTemplate; +import org.jclouds.compute.domain.Image; +import org.jclouds.compute.domain.ImageTemplate; +import org.jclouds.compute.domain.ImageTemplateBuilder; +import org.jclouds.compute.reference.ComputeServiceConstants; +import org.jclouds.concurrent.Futures; +import org.jclouds.ec2.EC2Client; +import org.jclouds.ec2.compute.functions.EC2ImageParser; +import org.jclouds.ec2.domain.Reservation; +import org.jclouds.ec2.domain.RunningInstance; +import org.jclouds.ec2.options.CreateImageOptions; +import org.jclouds.ec2.options.DescribeImagesOptions; +import org.jclouds.logging.Logger; +import org.jclouds.predicates.PredicateWithResult; +import org.jclouds.predicates.Retryables; + +import com.google.common.base.Function; +import com.google.common.collect.Iterables; +import com.google.common.util.concurrent.ListenableFuture; + +/** + * EC2 implementation of {@link ImageExtension} please note that {@link #createImage(ImageTemplate)} + * only works by cloning EBS backed instances for the moment. + * + * @author David Alves + * + */ +public class EC2ImageExtension implements ImageExtension { + + @Resource + @Named(ComputeServiceConstants.COMPUTE_LOGGER) + protected Logger logger = Logger.NULL; + + @com.google.inject.Inject(optional = true) + @Named("IMAGE_MAX_WAIT") + long maxWait = 3600; + @com.google.inject.Inject(optional = true) + @Named("IMAGE_WAIT_PERIOD") + long waitPeriod = 1; + private final EC2Client ec2Client; + private final ExecutorService executor; + private final Function ecImageToImage; + + @Inject + public EC2ImageExtension(EC2Client ec2Client, @Named(Constants.PROPERTY_USER_THREADS) ExecutorService userThreads, + EC2ImageParser ec2ImageToImage) { + this.ec2Client = checkNotNull(ec2Client); + this.executor = checkNotNull(userThreads); + this.ecImageToImage = checkNotNull(ec2ImageToImage); + } + + @Override + public ImageTemplate buildImageTemplateFromNode(String name, String id) { + String[] parts = AWSUtils.parseHandle(id); + String region = parts[0]; + String instanceId = parts[1]; + Reservation instance = Iterables.getOnlyElement(ec2Client.getInstanceServices() + .describeInstancesInRegion(region, instanceId)); + if (instance == null) + throw new NoSuchElementException("Cannot find server with id: " + id); + CloneImageTemplate template = new ImageTemplateBuilder.CloneImageTemplateBuilder().nodeId(id).name(name).build(); + return template; + } + + @Override + public ListenableFuture createImage(ImageTemplate template) { + checkState(template instanceof CloneImageTemplate, " ec2 only supports creating images through cloning."); + CloneImageTemplate cloneTemplate = (CloneImageTemplate) template; + String[] parts = AWSUtils.parseHandle(cloneTemplate.getSourceNodeId()); + final String region = parts[0]; + String instanceId = parts[1]; + + final String imageId = ec2Client.getAMIServices().createImageInRegion(region, cloneTemplate.getName(), + instanceId, CreateImageOptions.NONE); + + return Futures.makeListenable(executor.submit(new Callable() { + @Override + public Image call() throws Exception { + return Retryables.retryGettingResultOrFailing(new PredicateWithResult() { + + org.jclouds.ec2.domain.Image result; + RuntimeException lastFailure; + + @Override + public boolean apply(String input) { + result = checkNotNull(findImage(region, input)); + switch (result.getImageState()) { + case AVAILABLE: + logger.info("<< Image %s is available for use.", input); + return true; + case UNRECOGNIZED: + logger.debug("<< Image %s is not available yet.", input); + return false; + default: + lastFailure = new IllegalStateException("Image was not created: " + input); + throw lastFailure; + } + } + + @Override + public Image getResult() { + return ecImageToImage.apply(result); + } + + @Override + public Throwable getLastFailure() { + return lastFailure; + } + }, imageId, maxWait, waitPeriod, TimeUnit.SECONDS, + "Image was not created within the time limit, Giving up! [Limit: " + maxWait + " secs.]"); + } + }), executor); + } + + @Override + public boolean deleteImage(String id) { + String[] parts = AWSUtils.parseHandle(id); + String region = parts[0]; + String instanceId = parts[1]; + try { + ec2Client.getAMIServices().deregisterImageInRegion(region, instanceId); + return true; + } catch (Exception e) { + return false; + } + } + + private org.jclouds.ec2.domain.Image findImage(String region, String id) { + return Iterables.getOnlyElement(ec2Client.getAMIServices().describeImagesInRegion(region, + new DescribeImagesOptions().imageIds(id))); + + } +} diff --git a/apis/ec2/src/main/java/org/jclouds/ec2/compute/config/EC2ComputeServiceContextModule.java b/apis/ec2/src/main/java/org/jclouds/ec2/compute/config/EC2ComputeServiceContextModule.java index dd714a4d0f..4ffdde1766 100644 --- a/apis/ec2/src/main/java/org/jclouds/ec2/compute/config/EC2ComputeServiceContextModule.java +++ b/apis/ec2/src/main/java/org/jclouds/ec2/compute/config/EC2ComputeServiceContextModule.java @@ -30,16 +30,19 @@ import javax.inject.Named; import javax.inject.Singleton; import org.jclouds.compute.ComputeServiceContext; +import org.jclouds.compute.ImageExtension; import org.jclouds.compute.config.BaseComputeServiceContextModule; import org.jclouds.compute.domain.Image; import org.jclouds.concurrent.RetryOnTimeOutExceptionSupplier; import org.jclouds.ec2.compute.EC2ComputeService; +import org.jclouds.ec2.compute.EC2ImageExtension; import org.jclouds.ec2.compute.domain.RegionAndName; import org.jclouds.ec2.compute.loaders.RegionAndIdToImage; import org.jclouds.ec2.compute.suppliers.RegionAndNameToImageSupplier; import org.jclouds.rest.AuthorizationException; import org.jclouds.rest.suppliers.SetAndThrowAuthorizationExceptionSupplier; +import com.google.common.base.Optional; import com.google.common.base.Splitter; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; @@ -136,6 +139,11 @@ public class EC2ComputeServiceContextModule extends BaseComputeServiceContextMod return new String[] {}; return toArray(Splitter.on(',').split(amiOwners), String.class); } + + @Override + protected Optional provideImageExtension(Injector i) { + return Optional.of(i.getInstance(ImageExtension.class)); + } } diff --git a/apis/ec2/src/main/java/org/jclouds/ec2/compute/config/EC2ComputeServiceDependenciesModule.java b/apis/ec2/src/main/java/org/jclouds/ec2/compute/config/EC2ComputeServiceDependenciesModule.java index f82fa97ec4..9b2beea9e4 100644 --- a/apis/ec2/src/main/java/org/jclouds/ec2/compute/config/EC2ComputeServiceDependenciesModule.java +++ b/apis/ec2/src/main/java/org/jclouds/ec2/compute/config/EC2ComputeServiceDependenciesModule.java @@ -29,6 +29,7 @@ import javax.inject.Named; import javax.inject.Singleton; import org.jclouds.compute.ComputeService; +import org.jclouds.compute.ImageExtension; import org.jclouds.compute.domain.Image; import org.jclouds.compute.domain.NodeMetadata; import org.jclouds.compute.domain.NodeState; @@ -36,6 +37,7 @@ import org.jclouds.compute.domain.TemplateBuilder; import org.jclouds.compute.options.TemplateOptions; import org.jclouds.domain.Credentials; import org.jclouds.ec2.compute.EC2ComputeService; +import org.jclouds.ec2.compute.EC2ImageExtension; import org.jclouds.ec2.compute.domain.RegionAndName; import org.jclouds.ec2.compute.functions.AddElasticIpsToNodemetadata; import org.jclouds.ec2.compute.functions.CreateUniqueKeyPair; @@ -104,6 +106,8 @@ public class EC2ComputeServiceDependenciesModule extends AbstractModule { bind(new TypeLiteral>() { }).annotatedWith(Names.named("ELASTICIP")).to(LoadPublicIpForInstanceOrNull.class); bind(WindowsLoginCredentialsFromEncryptedData.class); + bind(new TypeLiteral() { + }).to(EC2ImageExtension.class); } /** 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 6ac3cd9b80..af2f4d648f 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 @@ -132,13 +132,23 @@ public class EC2CreateNodesInGroupThenAddToSet implements CreateNodesInGroupThen @Override public Map> execute(String group, int count, Template template, Set goodNodes, Map badNodes, Multimap customizationResponses) { + // ensure we don't mutate the input template - template = templateBuilderProvider.get().fromTemplate(template).build(); + Template mutableTemplate; + // ensure we don't mutate the input template, fromTemplate ignores imageId so + // build directly from imageId if we have it + if (template.getImage() != null && template.getImage().getId() != null) { + mutableTemplate = templateBuilderProvider.get().imageId(template.getImage().getId()).fromTemplate(template) + .build(); + // otherwise build from generic parameters + } else { + mutableTemplate = templateBuilderProvider.get().fromTemplate(template).build(); + } Iterable ips = allocateElasticIpsInRegion(count, template); Iterable started = createKeyPairAndSecurityGroupsAsNeededThenRunInstances(group, - count, template); + count, mutableTemplate); Iterable ids = transform(started, instanceToRegionAndName); @@ -152,7 +162,7 @@ public class EC2CreateNodesInGroupThenAddToSet implements CreateNodesInGroupThen assignElasticIpsToInstances(ips, started); - return utils.customizeNodesAndAddToGoodMapOrPutExceptionIntoBadMap(template.getOptions(), transform(started, + return utils.customizeNodesAndAddToGoodMapOrPutExceptionIntoBadMap(mutableTemplate.getOptions(), transform(started, runningInstanceToNodeMetadata), goodNodes, badNodes, customizationResponses); } diff --git a/apis/ec2/src/test/java/org/jclouds/ec2/compute/EC2ImageExtensionLiveTest.java b/apis/ec2/src/test/java/org/jclouds/ec2/compute/EC2ImageExtensionLiveTest.java new file mode 100644 index 0000000000..895958b1d8 --- /dev/null +++ b/apis/ec2/src/test/java/org/jclouds/ec2/compute/EC2ImageExtensionLiveTest.java @@ -0,0 +1,47 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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. + */ + +package org.jclouds.ec2.compute; + +import org.jclouds.compute.ImageExtension; +import org.jclouds.compute.internal.BaseImageExtensionLiveTest; +import org.jclouds.sshj.config.SshjSshClientModule; +import org.testng.annotations.Test; + +import com.google.inject.Module; + +/** + * Live test for ec2 {@link ImageExtension} implementation + * + * @author David Alves + * + */ +@Test(groups = "live", singleThreaded = true, testName = "EC2ImageExtensionLiveTest") +public class EC2ImageExtensionLiveTest extends BaseImageExtensionLiveTest { + + public EC2ImageExtensionLiveTest() { + provider = "ec2"; + } + + @Override + protected Module getSshModule() { + return new SshjSshClientModule(); + } + +} diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/compute/NovaImageExtension.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/compute/NovaImageExtension.java index 0b92881c5b..727a82bbb5 100644 --- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/compute/NovaImageExtension.java +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/compute/NovaImageExtension.java @@ -23,34 +23,59 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import java.util.NoSuchElementException; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import javax.annotation.Resource; import javax.inject.Inject; +import javax.inject.Named; import javax.inject.Singleton; +import org.jclouds.Constants; import org.jclouds.compute.ImageExtension; import org.jclouds.compute.domain.CloneImageTemplate; import org.jclouds.compute.domain.Image; import org.jclouds.compute.domain.ImageTemplate; import org.jclouds.compute.domain.ImageTemplateBuilder; +import org.jclouds.compute.reference.ComputeServiceConstants; +import org.jclouds.concurrent.Futures; +import org.jclouds.logging.Logger; import org.jclouds.openstack.nova.v1_1.NovaClient; import org.jclouds.openstack.nova.v1_1.domain.Server; import org.jclouds.openstack.nova.v1_1.domain.zonescoped.ImageInZone; import org.jclouds.openstack.nova.v1_1.domain.zonescoped.ZoneAndId; +import org.jclouds.predicates.PredicateWithResult; +import org.jclouds.predicates.Retryables; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; +import com.google.common.util.concurrent.ListenableFuture; @Singleton public class NovaImageExtension implements ImageExtension { - + + @Resource + @Named(ComputeServiceConstants.COMPUTE_LOGGER) + protected Logger logger = Logger.NULL; + private final NovaClient novaClient; private final Function imageInZoneToImage; + private final ExecutorService executor; + @com.google.inject.Inject(optional = true) + @Named("IMAGE_MAX_WAIT") + long maxWait = 3600; + @com.google.inject.Inject(optional = true) + @Named("IMAGE_WAIT_PERIOD") + long waitPeriod = 1; @Inject - public NovaImageExtension(NovaClient novaClient, Function imageInZoneToImage) { + public NovaImageExtension(NovaClient novaClient, Function imageInZoneToImage, + @Named(Constants.PROPERTY_USER_THREADS) ExecutorService userThreads) { this.novaClient = checkNotNull(novaClient); this.imageInZoneToImage = imageInZoneToImage; + this.executor = userThreads; } @Override @@ -64,16 +89,54 @@ public class NovaImageExtension implements ImageExtension { } @Override - public Image createImage(ImageTemplate template) { + public ListenableFuture createImage(ImageTemplate template) { checkState(template instanceof CloneImageTemplate, " openstack-nova only supports creating images through cloning."); CloneImageTemplate cloneTemplate = (CloneImageTemplate) template; - ZoneAndId zoneAndId = ZoneAndId.fromSlashEncoded(cloneTemplate.getSourceNodeId()); - String newImageId = novaClient.getServerClientForZone(zoneAndId.getZone()).createImageFromServer( + final ZoneAndId zoneAndId = ZoneAndId.fromSlashEncoded(cloneTemplate.getSourceNodeId()); + + final String newImageId = novaClient.getServerClientForZone(zoneAndId.getZone()).createImageFromServer( cloneTemplate.getName(), zoneAndId.getId()); - org.jclouds.openstack.nova.v1_1.domain.Image newImage = checkNotNull(findImage(ZoneAndId.fromZoneAndId( - zoneAndId.getZone(), newImageId))); - return imageInZoneToImage.apply(new ImageInZone(newImage, zoneAndId.getZone())); + logger.info(">> Registered new Image %s, waiting for it to become available.", newImageId); + + return Futures.makeListenable(executor.submit(new Callable() { + @Override + public Image call() throws Exception { + return Retryables.retryGettingResultOrFailing(new PredicateWithResult() { + + org.jclouds.openstack.nova.v1_1.domain.Image result; + RuntimeException lastFailure; + + @Override + public boolean apply(String input) { + result = checkNotNull(findImage(ZoneAndId.fromZoneAndId(zoneAndId.getZone(), newImageId))); + switch (result.getStatus()) { + case ACTIVE: + logger.info("<< Image %s is available for use.", newImageId); + return true; + case UNKNOWN: + case SAVING: + logger.debug("<< Image %s is not available yet.", newImageId); + return false; + default: + lastFailure = new IllegalStateException("Image was not created: " + newImageId); + throw lastFailure; + } + } + + @Override + public Image getResult() { + return imageInZoneToImage.apply(new ImageInZone(result, zoneAndId.getZone())); + } + + @Override + public Throwable getLastFailure() { + return lastFailure; + } + }, newImageId, maxWait, waitPeriod, TimeUnit.SECONDS, + "Image was not created within the time limit, Giving up! [Limit: " + maxWait + " secs.]"); + } + }), executor); } @Override diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/compute/strategy/ApplyNovaTemplateOptionsCreateNodesWithGroupEncodedIntoNameThenAddToSet.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/compute/strategy/ApplyNovaTemplateOptionsCreateNodesWithGroupEncodedIntoNameThenAddToSet.java index 6bd62df9c4..0ceca917cd 100644 --- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/compute/strategy/ApplyNovaTemplateOptionsCreateNodesWithGroupEncodedIntoNameThenAddToSet.java +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/compute/strategy/ApplyNovaTemplateOptionsCreateNodesWithGroupEncodedIntoNameThenAddToSet.java @@ -65,7 +65,7 @@ import com.google.common.primitives.Ints; */ @Singleton public class ApplyNovaTemplateOptionsCreateNodesWithGroupEncodedIntoNameThenAddToSet extends - CreateNodesWithGroupEncodedIntoNameThenAddToSet { + CreateNodesWithGroupEncodedIntoNameThenAddToSet { private final AllocateAndAddFloatingIpToNode allocateAndAddFloatingIpToNode; private final LoadingCache securityGroupCache; @@ -75,56 +75,67 @@ public class ApplyNovaTemplateOptionsCreateNodesWithGroupEncodedIntoNameThenAddT @Inject protected ApplyNovaTemplateOptionsCreateNodesWithGroupEncodedIntoNameThenAddToSet( - CreateNodeWithGroupEncodedIntoName addNodeWithTagStrategy, - ListNodesStrategy listNodesStrategy, - GroupNamingConvention.Factory namingConvention, - CustomizeNodeAndAddToGoodMapOrPutExceptionIntoBadMap.Factory customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory, - @Named(Constants.PROPERTY_USER_THREADS) ExecutorService executor, - Provider templateBuilderProvider, - AllocateAndAddFloatingIpToNode allocateAndAddFloatingIpToNode, - LoadingCache securityGroupCache, - LoadingCache keyPairCache, NovaClient novaClient) { + CreateNodeWithGroupEncodedIntoName addNodeWithTagStrategy, + ListNodesStrategy listNodesStrategy, + GroupNamingConvention.Factory namingConvention, + CustomizeNodeAndAddToGoodMapOrPutExceptionIntoBadMap.Factory customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory, + @Named(Constants.PROPERTY_USER_THREADS) ExecutorService executor, + Provider templateBuilderProvider, + AllocateAndAddFloatingIpToNode allocateAndAddFloatingIpToNode, + LoadingCache securityGroupCache, + LoadingCache keyPairCache, NovaClient novaClient) { super(addNodeWithTagStrategy, listNodesStrategy, namingConvention, executor, - customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory); + customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory); this.templateBuilderProvider = checkNotNull(templateBuilderProvider, "templateBuilderProvider"); this.securityGroupCache = checkNotNull(securityGroupCache, "securityGroupCache"); this.keyPairCache = checkNotNull(keyPairCache, "keyPairCache"); this.allocateAndAddFloatingIpToNode = checkNotNull(allocateAndAddFloatingIpToNode, - "allocateAndAddFloatingIpToNode"); + "allocateAndAddFloatingIpToNode"); this.novaClient = checkNotNull(novaClient, "novaClient"); } @Override public Map> execute(String group, int count, Template template, Set goodNodes, - Map badNodes, Multimap customizationResponses) { - // ensure we don't mutate the input template - Template mutableTemplate = templateBuilderProvider.get().fromTemplate(template).build(); + Map badNodes, Multimap customizationResponses) { + + Template mutableTemplate; + // ensure we don't mutate the input template, fromTemplate ignores imageId so + // build directly from imageId if we have it + if (template.getImage() != null && template.getImage().getId() != null) { + mutableTemplate = templateBuilderProvider.get().imageId(template.getImage().getId()).fromTemplate(template) + .build(); + // otherwise build from generic parameters + } else { + mutableTemplate = templateBuilderProvider.get().fromTemplate(template).build(); + } + NovaTemplateOptions templateOptions = NovaTemplateOptions.class.cast(mutableTemplate.getOptions()); - + assert template.getOptions().equals(templateOptions) : "options didn't clone properly"; - + String zone = mutableTemplate.getLocation().getId(); if (templateOptions.shouldAutoAssignFloatingIp()) { checkArgument(novaClient.getFloatingIPExtensionForZone(zone).isPresent(), - "Floating IPs are required by options, but the extension is not available! options: %s", templateOptions); + "Floating IPs are required by options, but the extension is not available! options: %s", + templateOptions); } boolean keyPairExensionPresent = novaClient.getKeyPairExtensionForZone(zone).isPresent(); if (templateOptions.shouldGenerateKeyPair()) { checkArgument(keyPairExensionPresent, - "Key Pairs are required by options, but the extension is not available! options: %s", templateOptions); + "Key Pairs are required by options, but the extension is not available! options: %s", templateOptions); KeyPair keyPair = keyPairCache.getUnchecked(ZoneAndName.fromZoneAndName(zone, namingConvention.create() - .sharedNameForGroup(group))); + .sharedNameForGroup(group))); keyPairCache.asMap().put(ZoneAndName.fromZoneAndName(zone, keyPair.getName()), keyPair); templateOptions.keyPairName(keyPair.getName()); } else if (templateOptions.getKeyPairName() != null) { checkArgument(keyPairExensionPresent, - "Key Pairs are required by options, but the extension is not available! options: %s", templateOptions); + "Key Pairs are required by options, but the extension is not available! options: %s", templateOptions); if (templateOptions.getLoginPrivateKey() != null) { String pem = templateOptions.getLoginPrivateKey(); KeyPair keyPair = KeyPair.builder().name(templateOptions.getKeyPairName()) - .fingerprint(fingerprintPrivateKey(pem)).privateKey(pem).build(); + .fingerprint(fingerprintPrivateKey(pem)).privateKey(pem).build(); keyPairCache.asMap().put(ZoneAndName.fromZoneAndName(zone, keyPair.getName()), keyPair); } } @@ -133,8 +144,8 @@ public class ApplyNovaTemplateOptionsCreateNodesWithGroupEncodedIntoNameThenAddT List inboundPorts = Ints.asList(templateOptions.getInboundPorts()); if (templateOptions.getSecurityGroupNames().size() > 0) { checkArgument(novaClient.getSecurityGroupExtensionForZone(zone).isPresent(), - "Security groups are required by options, but the extension is not available! options: %s", - templateOptions); + "Security groups are required by options, but the extension is not available! options: %s", + templateOptions); } else if (securityGroupExensionPresent && inboundPorts.size() > 0) { String securityGroupName = namingConvention.create().sharedNameForGroup(group); try { @@ -150,7 +161,7 @@ public class ApplyNovaTemplateOptionsCreateNodesWithGroupEncodedIntoNameThenAddT @Override protected Future> createNodeInGroupWithNameAndTemplate(String group, - final String name, Template template) { + final String name, Template template) { Future> future = super.createNodeInGroupWithNameAndTemplate(group, name, template); NovaTemplateOptions templateOptions = NovaTemplateOptions.class.cast(template.getOptions()); diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/compute/NovaImageExtensionLiveTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/compute/NovaImageExtensionLiveTest.java new file mode 100644 index 0000000000..1f8086610a --- /dev/null +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/compute/NovaImageExtensionLiveTest.java @@ -0,0 +1,47 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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. + */ + +package org.jclouds.openstack.nova.v1_1.compute; + +import org.jclouds.compute.ImageExtension; +import org.jclouds.compute.internal.BaseImageExtensionLiveTest; +import org.jclouds.sshj.config.SshjSshClientModule; +import org.testng.annotations.Test; + +import com.google.inject.Module; + +/** + * Live test for openstack-nova {@link ImageExtension} implementation. + * + * @author David Alves + * + */ +@Test(groups = "live", singleThreaded = true, testName = "NovaImageExtensionLiveTest") +public class NovaImageExtensionLiveTest extends BaseImageExtensionLiveTest { + + public NovaImageExtensionLiveTest() { + provider = "openstack-nova"; + } + + @Override + protected Module getSshModule() { + return new SshjSshClientModule(); + } + +} diff --git a/compute/src/main/java/org/jclouds/compute/ImageExtension.java b/compute/src/main/java/org/jclouds/compute/ImageExtension.java index 56da218825..ea0d92837c 100644 --- a/compute/src/main/java/org/jclouds/compute/ImageExtension.java +++ b/compute/src/main/java/org/jclouds/compute/ImageExtension.java @@ -22,6 +22,8 @@ package org.jclouds.compute; import org.jclouds.compute.domain.Image; import org.jclouds.compute.domain.ImageTemplate; +import com.google.common.util.concurrent.ListenableFuture; + /** * An extension to compute service to allow for the manipulation of {@link Image}s. Implementation * is optional by providers. @@ -49,7 +51,7 @@ public interface ImageExtension { * template to base the new image on * @return the image that was just built *after* it is registered on the provider */ - Image createImage(ImageTemplate template); + ListenableFuture createImage(ImageTemplate template); /** * Delete an {@link Image} on the provider. diff --git a/compute/src/test/java/org/jclouds/compute/internal/BaseImageExtensionLiveTest.java b/compute/src/test/java/org/jclouds/compute/internal/BaseImageExtensionLiveTest.java index 3bb31df3bf..92eb7a8cdb 100644 --- a/compute/src/test/java/org/jclouds/compute/internal/BaseImageExtensionLiveTest.java +++ b/compute/src/test/java/org/jclouds/compute/internal/BaseImageExtensionLiveTest.java @@ -22,16 +22,22 @@ package org.jclouds.compute.internal; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; -import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Resource; +import javax.inject.Named; import org.jclouds.compute.ComputeService; import org.jclouds.compute.ImageExtension; import org.jclouds.compute.RunNodesException; -import org.jclouds.compute.domain.ExecResponse; import org.jclouds.compute.domain.Image; import org.jclouds.compute.domain.ImageTemplate; import org.jclouds.compute.domain.NodeMetadata; import org.jclouds.compute.domain.Template; +import org.jclouds.compute.reference.ComputeServiceConstants; +import org.jclouds.logging.Logger; +import org.jclouds.predicates.RetryablePredicate; import org.jclouds.ssh.SshClient; import org.testng.annotations.Test; @@ -47,40 +53,74 @@ import com.google.common.collect.Iterables; */ public abstract class BaseImageExtensionLiveTest extends BaseComputeServiceContextLiveTest { + @Resource + @Named(ComputeServiceConstants.COMPUTE_LOGGER) + protected Logger logger = Logger.NULL; + + protected String imageId; + /** * Returns the template for the base node, override to test different templates. * * @return */ public Template getNodeTemplate() { - return view.getComputeService().templateBuilder().any().build(); + return view.getComputeService().templateBuilder().build(); + } + + /** + * Returns the maximum amount of time (in seconds) to wait for a node spawned from the new image + * to become available, override to increase this time. + * + * @return + */ + public long getSpawnNodeMaxWait() { + return 600L; + } + + /** + * Lists the images found in the {@link ComputeService}, subclasses may override to constrain + * search. + * + * @return + */ + protected Iterable listImages() { + return view.getComputeService().listImages(); } @Test(groups = { "integration", "live" }, singleThreaded = true) - public void testCreateImage() throws RunNodesException, InterruptedException { + public void testCreateImage() throws RunNodesException, InterruptedException, ExecutionException { ComputeService computeService = view.getComputeService(); Optional imageExtension = computeService.getImageExtension(); + assertTrue("image extension was not present", imageExtension.isPresent()); - Set imagesBefore = computeService.listImages(); + Template template = getNodeTemplate(); - NodeMetadata node = Iterables.getOnlyElement(computeService.createNodesInGroup("test-create-image", 1, - getNodeTemplate())); + NodeMetadata node = Iterables.getOnlyElement(computeService.createNodesInGroup("test-create-image", 1, template)); + + checkReachable(node); + + logger.info("Creating image from node %s, started with template: %s", node, template); ImageTemplate newImageTemplate = imageExtension.get().buildImageTemplateFromNode("test-create-image", node.getId()); - Image image = imageExtension.get().createImage(newImageTemplate); + Image image = imageExtension.get().createImage(newImageTemplate).get(); + + logger.info("Image created: %s", image); assertEquals("test-create-image", image.getName()); + imageId = image.getId(); + computeService.destroyNode(node.getId()); - Set imagesAfter = computeService.listImages(); + Optional optImage = getImage(); - assertTrue(imagesBefore.size() == imagesAfter.size() - 1); + assertTrue(optImage.isPresent()); } @@ -89,16 +129,16 @@ public abstract class BaseImageExtensionLiveTest extends BaseComputeServiceConte ComputeService computeService = view.getComputeService(); - Template template = computeService.templateBuilder().fromImage(getImage().get()).build(); + Optional optImage = getImage(); - NodeMetadata node = Iterables.getOnlyElement(computeService.createNodesInGroup("test-create-image", 1, template)); + assertTrue(optImage.isPresent()); - SshClient client = view.utils().sshForNode().apply(node); - client.connect(); + NodeMetadata node = Iterables.getOnlyElement(computeService.createNodesInGroup("test-create-image", 1, view + .getComputeService() + // fromImage does not use the arg image's id (but we do need to set location) + .templateBuilder().imageId(optImage.get().getId()).fromImage(optImage.get()).build())); - ExecResponse hello = client.exec("echo hello"); - - assertEquals(hello.getOutput().trim(), "hello"); + checkReachable(node); view.getComputeService().destroyNode(node.getId()); @@ -123,12 +163,25 @@ public abstract class BaseImageExtensionLiveTest extends BaseComputeServiceConte } private Optional getImage() { - return Iterables.tryFind(view.getComputeService().listImages(), new Predicate() { + return Iterables.tryFind(listImages(), new Predicate() { @Override public boolean apply(Image input) { - return input.getId().contains("test-create-image"); + return input.getId().equals(imageId); } }); } + private void checkReachable(NodeMetadata node) { + SshClient client = view.utils().sshForNode().apply(node); + assertTrue(new RetryablePredicate(new Predicate() { + @Override + public boolean apply(SshClient input) { + input.connect(); + if (input.exec("id").getExitStatus() == 0) { + return true; + } + return false; + } + }, getSpawnNodeMaxWait(), 1l, TimeUnit.SECONDS).apply(client)); + } } diff --git a/labs/virtualbox/src/main/java/org/jclouds/virtualbox/compute/VirtualBoxImageExtension.java b/labs/virtualbox/src/main/java/org/jclouds/virtualbox/compute/VirtualBoxImageExtension.java index 3b859549bd..6da9fbfbfb 100644 --- a/labs/virtualbox/src/main/java/org/jclouds/virtualbox/compute/VirtualBoxImageExtension.java +++ b/labs/virtualbox/src/main/java/org/jclouds/virtualbox/compute/VirtualBoxImageExtension.java @@ -60,6 +60,8 @@ import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.base.Supplier; import com.google.common.collect.Iterables; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; @Singleton public class VirtualBoxImageExtension implements ImageExtension { @@ -104,7 +106,7 @@ public class VirtualBoxImageExtension implements ImageExtension { } @Override - public Image createImage(ImageTemplate template) { + public ListenableFuture createImage(ImageTemplate template) { checkState(template instanceof CloneImageTemplate, " vbox image extension only supports cloning for the moment."); CloneImageTemplate cloneTemplate = CloneImageTemplate.class.cast(template); @@ -133,7 +135,7 @@ public class VirtualBoxImageExtension implements ImageExtension { // registering manager.get().getVBox().registerMachine(clonedMachine); - return imachineToImage.apply(clonedMachine); + return Futures.immediateFuture(imachineToImage.apply(clonedMachine)); } @Override diff --git a/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/config/AWSEC2ComputeServiceContextModule.java b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/config/AWSEC2ComputeServiceContextModule.java index 3368d5c120..1edd61873d 100644 --- a/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/config/AWSEC2ComputeServiceContextModule.java +++ b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/config/AWSEC2ComputeServiceContextModule.java @@ -39,6 +39,7 @@ import org.jclouds.aws.ec2.compute.strategy.AWSEC2ListNodesStrategy; import org.jclouds.aws.ec2.compute.strategy.AWSEC2ReviseParsedImage; import org.jclouds.aws.ec2.compute.strategy.CreateKeyPairPlacementAndSecurityGroupsAsNeededAndReturnRunOptions; import org.jclouds.aws.ec2.compute.suppliers.AWSEC2HardwareSupplier; +import org.jclouds.compute.ImageExtension; import org.jclouds.compute.config.BaseComputeServiceContextModule; import org.jclouds.compute.domain.Image; import org.jclouds.compute.domain.TemplateBuilder; @@ -62,6 +63,7 @@ import org.jclouds.ec2.compute.suppliers.RegionAndNameToImageSupplier; import org.jclouds.rest.AuthorizationException; import org.jclouds.rest.suppliers.SetAndThrowAuthorizationExceptionSupplier; +import com.google.common.base.Optional; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.base.Throwables; @@ -180,4 +182,9 @@ public class AWSEC2ComputeServiceContextModule extends BaseComputeServiceContext protected TemplateOptions provideTemplateOptions(Injector injector, TemplateOptions options) { return options.as(EC2TemplateOptions.class).userData("#cloud-config\nrepo_upgrade: none\n".getBytes()); } + + @Override + protected Optional provideImageExtension(Injector i) { + return Optional.of(i.getInstance(ImageExtension.class)); + } } diff --git a/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/config/AWSEC2ComputeServiceDependenciesModule.java b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/config/AWSEC2ComputeServiceDependenciesModule.java index e9c159ffe0..37b3593b53 100644 --- a/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/config/AWSEC2ComputeServiceDependenciesModule.java +++ b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/config/AWSEC2ComputeServiceDependenciesModule.java @@ -40,11 +40,13 @@ import org.jclouds.aws.ec2.functions.ImportOrReturnExistingKeypair; import org.jclouds.aws.ec2.predicates.PlacementGroupAvailable; import org.jclouds.aws.ec2.predicates.PlacementGroupDeleted; import org.jclouds.compute.ComputeService; +import org.jclouds.compute.ImageExtension; import org.jclouds.compute.domain.Image; import org.jclouds.compute.domain.TemplateBuilder; import org.jclouds.compute.options.TemplateOptions; import org.jclouds.config.ValueOfConfigurationKeyOrNull; import org.jclouds.domain.Credentials; +import org.jclouds.ec2.compute.EC2ImageExtension; import org.jclouds.ec2.compute.config.EC2ComputeServiceDependenciesModule; import org.jclouds.ec2.compute.domain.RegionAndName; import org.jclouds.ec2.compute.functions.CreateUniqueKeyPair; @@ -94,6 +96,8 @@ public class AWSEC2ComputeServiceDependenciesModule extends EC2ComputeServiceDep bind(new TypeLiteral>() { }).to(RegionAndIdToImage.class); install(new FactoryModuleBuilder().build(CallForImages.Factory.class)); + bind(new TypeLiteral() { + }).to(EC2ImageExtension.class); } @Provides diff --git a/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/AWSEC2ImageExtensionLiveTest.java b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/AWSEC2ImageExtensionLiveTest.java new file mode 100644 index 0000000000..37d255ba6d --- /dev/null +++ b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/AWSEC2ImageExtensionLiveTest.java @@ -0,0 +1,67 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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. + */ + +package org.jclouds.aws.ec2.compute; + +import org.jclouds.aws.ec2.AWSEC2AsyncClient; +import org.jclouds.aws.ec2.AWSEC2Client; +import org.jclouds.aws.util.AWSUtils; +import org.jclouds.compute.ImageExtension; +import org.jclouds.compute.domain.Image; +import org.jclouds.compute.internal.BaseImageExtensionLiveTest; +import org.jclouds.ec2.compute.functions.EC2ImageParser; +import org.jclouds.ec2.options.DescribeImagesOptions; +import org.jclouds.rest.RestContext; +import org.jclouds.sshj.config.SshjSshClientModule; +import org.testng.annotations.Test; + +import com.google.common.collect.Iterables; +import com.google.inject.Module; + +/** + * Live test for aws-ec2 {@link ImageExtension} implementation + * + * @author David Alves + * + */ +@Test(groups = "live", singleThreaded = true, testName = "AWSEC2ImageExtensionLiveTest") +public class AWSEC2ImageExtensionLiveTest extends BaseImageExtensionLiveTest { + + public AWSEC2ImageExtensionLiveTest() { + provider = "aws-ec2"; + } + + @Override + protected Iterable listImages() { + RestContext unwrapped = view.unwrap(); + String[] parts = AWSUtils.parseHandle(imageId); + String region = parts[0]; + String imageId = parts[1]; + EC2ImageParser parser = view.utils().getInjector().getInstance(EC2ImageParser.class); + return Iterables.transform( + unwrapped.getApi().getAMIServices() + .describeImagesInRegion(region, new DescribeImagesOptions().imageIds(imageId)), parser); + } + + @Override + protected Module getSshModule() { + return new SshjSshClientModule(); + } + +} diff --git a/providers/hpcloud-compute/src/test/java/org/jclouds/hpcloud/compute/compute/HPCloudComputeImageExtensionLivetest.java b/providers/hpcloud-compute/src/test/java/org/jclouds/hpcloud/compute/compute/HPCloudComputeImageExtensionLivetest.java index f632f51cc0..852ce623f5 100644 --- a/providers/hpcloud-compute/src/test/java/org/jclouds/hpcloud/compute/compute/HPCloudComputeImageExtensionLivetest.java +++ b/providers/hpcloud-compute/src/test/java/org/jclouds/hpcloud/compute/compute/HPCloudComputeImageExtensionLivetest.java @@ -41,4 +41,9 @@ public class HPCloudComputeImageExtensionLivetest extends BaseImageExtensionLive protected Module getSshModule() { return new SshjSshClientModule(); } + + @Override + public long getSpawnNodeMaxWait() { + return 2400L; + } }