image extension working on hpcloud, virtualbox and aws-ec2, cloudservers implemented but has issues

This commit is contained in:
David Ribeiro Alves 2012-05-03 03:55:40 +01:00
parent 19390ea87d
commit 082158ac3f
18 changed files with 781 additions and 59 deletions

View File

@ -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<org.jclouds.cloudservers.domain.Image, Image> 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<org.jclouds.cloudservers.domain.Image, Image> 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<Image> 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<Image>() {
@Override
public Image call() throws Exception {
return Retryables.retryGettingResultOrFailing(new PredicateWithResult<Integer, Image>() {
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<org.jclouds.cloudservers.domain.Image>() {
@Override
public boolean apply(org.jclouds.cloudservers.domain.Image input) {
return input.getId() == id;
}
}).orNull();
}
}

View File

@ -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<Function<Location, Location>>() {
}).to((Class) IdentityFunction.class);
bind(new TypeLiteral<ImageExtension>() {
}).to(CloudServersImageExtension.class);
}
@ -112,6 +119,10 @@ public class CloudServersComputeServiceContextModule extends
Map<ServerStatus, NodeState> provideServerToNodeState() {
return serverToNodeState;
}
@Override
protected Optional<ImageExtension> provideImageExtension(Injector i) {
return Optional.of(i.getInstance(ImageExtension.class));
}
}

View File

@ -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();
}
}

View File

@ -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<org.jclouds.ec2.domain.Image, Image> 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<? extends RunningInstance> 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<Image> 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<Image>() {
@Override
public Image call() throws Exception {
return Retryables.retryGettingResultOrFailing(new PredicateWithResult<String, Image>() {
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)));
}
}

View File

@ -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<ImageExtension> provideImageExtension(Injector i) {
return Optional.of(i.getInstance(ImageExtension.class));
}
}

View File

@ -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<CacheLoader<RegionAndName, String>>() {
}).annotatedWith(Names.named("ELASTICIP")).to(LoadPublicIpForInstanceOrNull.class);
bind(WindowsLoginCredentialsFromEncryptedData.class);
bind(new TypeLiteral<ImageExtension>() {
}).to(EC2ImageExtension.class);
}
/**

View File

@ -132,13 +132,23 @@ public class EC2CreateNodesInGroupThenAddToSet implements CreateNodesInGroupThen
@Override
public Map<?, Future<Void>> execute(String group, int count, Template template, Set<NodeMetadata> goodNodes,
Map<NodeMetadata, Exception> badNodes, Multimap<NodeMetadata, CustomizationResponse> 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<String> ips = allocateElasticIpsInRegion(count, template);
Iterable<? extends RunningInstance> started = createKeyPairAndSecurityGroupsAsNeededThenRunInstances(group,
count, template);
count, mutableTemplate);
Iterable<RegionAndName> 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);
}

View File

@ -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();
}
}

View File

@ -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<ImageInZone, Image> 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<ImageInZone, Image> imageInZoneToImage) {
public NovaImageExtension(NovaClient novaClient, Function<ImageInZone, Image> 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<Image> 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<Image>() {
@Override
public Image call() throws Exception {
return Retryables.retryGettingResultOrFailing(new PredicateWithResult<String, Image>() {
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

View File

@ -65,7 +65,7 @@ import com.google.common.primitives.Ints;
*/
@Singleton
public class ApplyNovaTemplateOptionsCreateNodesWithGroupEncodedIntoNameThenAddToSet extends
CreateNodesWithGroupEncodedIntoNameThenAddToSet {
CreateNodesWithGroupEncodedIntoNameThenAddToSet {
private final AllocateAndAddFloatingIpToNode allocateAndAddFloatingIpToNode;
private final LoadingCache<ZoneAndName, SecurityGroupInZone> 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<TemplateBuilder> templateBuilderProvider,
AllocateAndAddFloatingIpToNode allocateAndAddFloatingIpToNode,
LoadingCache<ZoneAndName, SecurityGroupInZone> securityGroupCache,
LoadingCache<ZoneAndName, KeyPair> keyPairCache, NovaClient novaClient) {
CreateNodeWithGroupEncodedIntoName addNodeWithTagStrategy,
ListNodesStrategy listNodesStrategy,
GroupNamingConvention.Factory namingConvention,
CustomizeNodeAndAddToGoodMapOrPutExceptionIntoBadMap.Factory customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory,
@Named(Constants.PROPERTY_USER_THREADS) ExecutorService executor,
Provider<TemplateBuilder> templateBuilderProvider,
AllocateAndAddFloatingIpToNode allocateAndAddFloatingIpToNode,
LoadingCache<ZoneAndName, SecurityGroupInZone> securityGroupCache,
LoadingCache<ZoneAndName, KeyPair> 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<?, Future<Void>> execute(String group, int count, Template template, Set<NodeMetadata> goodNodes,
Map<NodeMetadata, Exception> badNodes, Multimap<NodeMetadata, CustomizationResponse> customizationResponses) {
// ensure we don't mutate the input template
Template mutableTemplate = templateBuilderProvider.get().fromTemplate(template).build();
Map<NodeMetadata, Exception> badNodes, Multimap<NodeMetadata, CustomizationResponse> 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<Integer> 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<AtomicReference<NodeMetadata>> createNodeInGroupWithNameAndTemplate(String group,
final String name, Template template) {
final String name, Template template) {
Future<AtomicReference<NodeMetadata>> future = super.createNodeInGroupWithNameAndTemplate(group, name, template);
NovaTemplateOptions templateOptions = NovaTemplateOptions.class.cast(template.getOptions());

View File

@ -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();
}
}

View File

@ -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<Image> createImage(ImageTemplate template);
/**
* Delete an {@link Image} on the provider.

View File

@ -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<? extends Image> 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> imageExtension = computeService.getImageExtension();
assertTrue("image extension was not present", imageExtension.isPresent());
Set<? extends Image> 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<? extends Image> imagesAfter = computeService.listImages();
Optional<? extends Image> 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<? extends Image> 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<? extends Image> getImage() {
return Iterables.tryFind(view.getComputeService().listImages(), new Predicate<Image>() {
return Iterables.tryFind(listImages(), new Predicate<Image>() {
@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<SshClient>(new Predicate<SshClient>() {
@Override
public boolean apply(SshClient input) {
input.connect();
if (input.exec("id").getExitStatus() == 0) {
return true;
}
return false;
}
}, getSpawnNodeMaxWait(), 1l, TimeUnit.SECONDS).apply(client));
}
}

View File

@ -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<Image> 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

View File

@ -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<ImageExtension> provideImageExtension(Injector i) {
return Optional.of(i.getInstance(ImageExtension.class));
}
}

View File

@ -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<CacheLoader<RegionAndName, Image>>() {
}).to(RegionAndIdToImage.class);
install(new FactoryModuleBuilder().build(CallForImages.Factory.class));
bind(new TypeLiteral<ImageExtension>() {
}).to(EC2ImageExtension.class);
}
@Provides

View File

@ -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<? extends Image> listImages() {
RestContext<AWSEC2Client, AWSEC2AsyncClient> 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();
}
}

View File

@ -41,4 +41,9 @@ public class HPCloudComputeImageExtensionLivetest extends BaseImageExtensionLive
protected Module getSshModule() {
return new SshjSshClientModule();
}
@Override
public long getSpawnNodeMaxWait() {
return 2400L;
}
}