Repository: jclouds Updated Branches: refs/heads/master 0ac7dfd37 -> 3f2b9376a
JCLOUDS-514: Support attaching volumes at boot in Nova Project: http://git-wip-us.apache.org/repos/asf/jclouds/repo Commit: http://git-wip-us.apache.org/repos/asf/jclouds/commit/3f2b9376 Tree: http://git-wip-us.apache.org/repos/asf/jclouds/tree/3f2b9376 Diff: http://git-wip-us.apache.org/repos/asf/jclouds/diff/3f2b9376 Branch: refs/heads/master Commit: 3f2b9376a1f2ea54fa979d1338b9ed11ea176c4b Parents: 0ac7dfd Author: jasdeep-hundal <[email protected]> Authored: Tue Mar 25 14:12:37 2014 -0700 Committer: Jeremy Daggett <[email protected]> Committed: Thu Sep 18 16:32:24 2014 -0700 ---------------------------------------------------------------------- .../nova/v2_0/domain/BlockDeviceMapping.java | 279 +++++++++++++++++++ .../nova/v2_0/options/CreateServerOptions.java | 37 ++- .../extensions/VolumeAttachmentApiLiveTest.java | 38 +++ .../nova/v2_0/features/ServerApiExpectTest.java | 27 ++ 4 files changed, 380 insertions(+), 1 deletion(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/jclouds/blob/3f2b9376/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/domain/BlockDeviceMapping.java ---------------------------------------------------------------------- diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/domain/BlockDeviceMapping.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/domain/BlockDeviceMapping.java new file mode 100644 index 0000000..efb3ba4 --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/domain/BlockDeviceMapping.java @@ -0,0 +1,279 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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.v2_0.domain; + +import static com.google.common.base.Preconditions.checkNotNull; + +import javax.inject.Named; +import java.beans.ConstructorProperties; + +import org.jclouds.javax.annotation.Nullable; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; + +/** + * A representation of a block device that should be attached to the Nova instance to be launched + * + */ +public class BlockDeviceMapping { + + @Named("delete_on_termination") + String deleteOnTermination = "0"; + @Named("device_name") + String deviceName = null; + @Named("volume_id") + String volumeId = null; + @Named("volume_size") + String volumeSize = ""; + + @ConstructorProperties({"volume_id", "volume_size", "device_name", "delete_on_termination"}) + private BlockDeviceMapping(String volumeId, String volumeSize, String deviceName, String deleteOnTermination) { + checkNotNull(volumeId); + checkNotNull(deviceName); + this.volumeId = volumeId; + this.volumeSize = volumeSize; + this.deviceName = deviceName; + if (deleteOnTermination != null) { + this.deleteOnTermination = deleteOnTermination; + } + } + + /** + * Default constructor. + */ + private BlockDeviceMapping() {} + + /** + * Copy constructor + * @param blockDeviceMapping + */ + private BlockDeviceMapping(BlockDeviceMapping blockDeviceMapping) { + this(blockDeviceMapping.volumeId, + blockDeviceMapping.volumeSize, + blockDeviceMapping.deviceName, + blockDeviceMapping.deleteOnTermination); + } + + /** + * @return the volume id of the block device + */ + @Nullable + public String getVolumeId() { + return volumeId; + } + + /** + * @return the size of the block device + */ + @Nullable + public String getVolumeSize() { + return volumeSize; + } + + /** + * @return the device name to which the volume is attached + */ + @Nullable + public String getDeviceName() { + return deviceName; + } + + /** + * @return whether the volume should be deleted on terminating the instance + */ + public String getDeleteOnTermination() { + return deviceName; + } + + @Override + public int hashCode() { + return Objects.hashCode(volumeId, volumeSize, deviceName, deleteOnTermination); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null || getClass() != obj.getClass()) + return false; + BlockDeviceMapping that = BlockDeviceMapping.class.cast(obj); + return Objects.equal(this.volumeId, that.volumeId) + && Objects.equal(this.volumeSize, that.volumeSize) + && Objects.equal(this.deviceName, that.deviceName) + && Objects.equal(this.deleteOnTermination, that.deleteOnTermination); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("volumeId", volumeId) + .add("volumeSize", volumeSize) + .add("deviceName", deviceName) + .add("deleteOnTermination", deleteOnTermination) + .toString(); + } + + /* + * Methods to get the Create and Update builders follow + */ + + /** + * @return the Builder for creating a new block device mapping + */ + public static CreateBuilder createOptions(String volumeId, String deviceName) { + return new CreateBuilder(volumeId, deviceName); + } + + /** + * @return the Builder for updating a block device mapping + */ + public static UpdateBuilder updateOptions() { + return new UpdateBuilder(); + } + + private abstract static class Builder<ParameterizedBuilderType> { + protected BlockDeviceMapping blockDeviceMapping; + + /** + * No-parameters constructor used when updating. + * */ + private Builder() { + blockDeviceMapping = new BlockDeviceMapping(); + } + + protected abstract ParameterizedBuilderType self(); + + /** + * Provide the volume id to the BlockDeviceMapping's Builder. + * + * @return the Builder. + * @see BlockDeviceMapping#getVolumeId() + */ + public ParameterizedBuilderType volumeId(String volumeId) { + blockDeviceMapping.volumeId = volumeId; + return self(); + } + + /** + * Provide the volume size in GB to the BlockDeviceMapping's Builder. + * + * @return the Builder. + * @see BlockDeviceMapping#getVolumeSize() + */ + public ParameterizedBuilderType volumeSize(int volumeSize) { + blockDeviceMapping.volumeSize = Integer.toString(volumeSize); + return self(); + } + + /** + * Provide the deviceName to the BlockDeviceMapping's Builder. + * + * @return the Builder. + * @see BlockDeviceMapping#getDeviceName() + */ + public ParameterizedBuilderType deviceName(String deviceName) { + blockDeviceMapping.deviceName = deviceName; + return self(); + } + + /** + * Provide an option indicated to delete the volume on instance deletion to BlockDeviceMapping's Builder. + * + * @return the Builder. + * @see BlockDeviceMapping#getVolumeSize() + */ + public ParameterizedBuilderType deleteOnTermination(boolean deleteOnTermination) { + blockDeviceMapping.deleteOnTermination = deleteOnTermination ? "1" : "0"; + return self(); + } + } + + /** + * Create and Update builders (inheriting from Builder) + */ + public static class CreateBuilder extends Builder<CreateBuilder> { + /** + * Supply required properties for creating a Builder + */ + private CreateBuilder(String volumeId, String deviceName) { + blockDeviceMapping.volumeId = volumeId; + blockDeviceMapping.deviceName = deviceName; + } + + /** + * @return a CreateOptions constructed with this Builder. + */ + public CreateOptions build() { + return new CreateOptions(blockDeviceMapping); + } + + protected CreateBuilder self() { + return this; + } + } + + /** + * Create and Update builders (inheriting from Builder) + */ + public static class UpdateBuilder extends Builder<UpdateBuilder> { + /** + * Supply required properties for updating a Builder + */ + private UpdateBuilder() { + } + + /** + * @return a UpdateOptions constructed with this Builder. + */ + public UpdateOptions build() { + return new UpdateOptions(blockDeviceMapping); + } + + protected UpdateBuilder self() { + return this; + } + } + + /** + * Create and Update options - extend the domain class, passed to API update and create calls. + * Essentially the same as the domain class. Ensure validation and safe typing. + */ + public static class CreateOptions extends BlockDeviceMapping { + /** + * Copy constructor + */ + private CreateOptions(BlockDeviceMapping blockDeviceMapping) { + super(blockDeviceMapping); + checkNotNull(blockDeviceMapping.volumeId, "volume id should not be null"); + checkNotNull(blockDeviceMapping.deviceName, "device name should not be null"); + } + } + + /** + * Create and Update options - extend the domain class, passed to API update and create calls. + * Essentially the same as the domain class. Ensure validation and safe typing. + */ + public static class UpdateOptions extends BlockDeviceMapping { + /** + * Copy constructor + */ + private UpdateOptions(BlockDeviceMapping blockDeviceMapping) { + super(blockDeviceMapping); + } + } +} http://git-wip-us.apache.org/repos/asf/jclouds/blob/3f2b9376/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/options/CreateServerOptions.java ---------------------------------------------------------------------- diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/options/CreateServerOptions.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/options/CreateServerOptions.java index f3f7a6f..dc092bf 100644 --- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/options/CreateServerOptions.java +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/options/CreateServerOptions.java @@ -16,8 +16,8 @@ */ package org.jclouds.openstack.nova.v2_0.options; -import static com.google.common.base.Objects.equal; import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Objects.equal; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; @@ -33,6 +33,7 @@ import javax.inject.Inject; import javax.inject.Named; import org.jclouds.http.HttpRequest; +import org.jclouds.openstack.nova.v2_0.domain.BlockDeviceMapping; import org.jclouds.openstack.nova.v2_0.domain.Network; import org.jclouds.rest.MapBinder; import org.jclouds.rest.binders.BindToJsonPayload; @@ -109,6 +110,7 @@ public class CreateServerOptions implements MapBinder { private Set<Network> novaNetworks = ImmutableSet.of(); private String availabilityZone; private boolean configDrive; + private Set<BlockDeviceMapping> blockDeviceMapping = ImmutableSet.of(); @Override public boolean equals(Object object) { @@ -151,6 +153,8 @@ public class CreateServerOptions implements MapBinder { toString.add("networks", networks); toString.add("availability_zone", availabilityZone == null ? null : availabilityZone); toString.add("configDrive", configDrive); + if (!blockDeviceMapping.isEmpty()) + toString.add("blockDeviceMapping", blockDeviceMapping); return toString; } @@ -177,6 +181,8 @@ public class CreateServerOptions implements MapBinder { Set<Map<String, String>> networks; @Named("config_drive") String configDrive; + @Named("block_device_mapping") + Set<BlockDeviceMapping> blockDeviceMapping; private ServerRequest(String name, String imageRef, String flavorRef) { this.name = name; @@ -238,6 +244,10 @@ public class CreateServerOptions implements MapBinder { } } + if (!blockDeviceMapping.isEmpty()) { + server.blockDeviceMapping = blockDeviceMapping; + } + return bindToRequest(request, ImmutableMap.of("server", server)); } @@ -459,6 +469,23 @@ public class CreateServerOptions implements MapBinder { return networks(ImmutableSet.copyOf(networks)); } + /** + * @see #getBlockDeviceMapping + */ + public CreateServerOptions blockDeviceMapping(Set<BlockDeviceMapping> blockDeviceMapping) { + this.blockDeviceMapping = ImmutableSet.copyOf(blockDeviceMapping); + return this; + } + + /** + * Block volumes that should be attached to the instance at boot time. + * + * @see <a href="http://docs.openstack.org/trunk/openstack-ops/content/attach_block_storage.html">Attach Block Storage<a/> + */ + public Set<BlockDeviceMapping> getBlockDeviceMapping() { + return blockDeviceMapping; + } + public static class Builder { /** @@ -545,6 +572,14 @@ public class CreateServerOptions implements MapBinder { CreateServerOptions options = new CreateServerOptions(); return options.availabilityZone(availabilityZone); } + + /** + * @see org.jclouds.openstack.nova.v2_0.options.CreateServerOptions#getBlockDeviceMapping() + */ + public static CreateServerOptions blockDeviceMapping (Set<BlockDeviceMapping> blockDeviceMapping) { + CreateServerOptions options = new CreateServerOptions(); + return options.blockDeviceMapping(blockDeviceMapping); + } } @Override http://git-wip-us.apache.org/repos/asf/jclouds/blob/3f2b9376/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/extensions/VolumeAttachmentApiLiveTest.java ---------------------------------------------------------------------- diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/extensions/VolumeAttachmentApiLiveTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/extensions/VolumeAttachmentApiLiveTest.java index 3c34563..dd7f39e 100644 --- a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/extensions/VolumeAttachmentApiLiveTest.java +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/extensions/VolumeAttachmentApiLiveTest.java @@ -23,9 +23,11 @@ import static org.testng.Assert.assertTrue; import java.util.Set; +import org.jclouds.openstack.nova.v2_0.domain.BlockDeviceMapping; import org.jclouds.openstack.nova.v2_0.domain.Volume; import org.jclouds.openstack.nova.v2_0.domain.VolumeAttachment; import org.jclouds.openstack.nova.v2_0.internal.BaseNovaApiLiveTest; +import org.jclouds.openstack.nova.v2_0.options.CreateServerOptions; import org.jclouds.openstack.nova.v2_0.options.CreateVolumeOptions; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; @@ -34,6 +36,7 @@ import org.testng.annotations.Test; import com.google.common.base.Objects; import com.google.common.base.Optional; import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; /** @@ -148,7 +151,42 @@ public class VolumeAttachmentApiLiveTest extends BaseNovaApiLiveTest { if (server_id != null) api.getServerApi(region).delete(server_id); } + } + } + + @Test(dependsOnMethods = "testCreateVolume") + public void testAttachmentAtBoot() { + if (volumeApi.isPresent()) { + String server_id = null; + BlockDeviceMapping blockDeviceMapping = BlockDeviceMapping.createOptions(testVolume.getId(), "/dev/vdf").build(); + try { + CreateServerOptions createServerOptions = + CreateServerOptions.Builder.blockDeviceMapping(ImmutableSet.of(blockDeviceMapping)); + final String serverId = server_id = createServerInRegion(region, createServerOptions).getId(); + + Set<? extends VolumeAttachment> attachments = volumeAttachmentApi.get() + .listAttachmentsOnServer(serverId).toSet(); + VolumeAttachment attachment = Iterables.getOnlyElement(attachments); + + VolumeAttachment details = volumeAttachmentApi.get() + .getAttachmentForVolumeOnServer(attachment.getVolumeId(), serverId); + assertNotNull(details.getId()); // Probably same as volumeId? Not necessarily true though + assertEquals(details.getVolumeId(), testVolume.getId()); + assertEquals(details.getDevice(), "/dev/vdf"); + assertEquals(details.getServerId(), serverId); + + assertEquals(volumeApi.get().get(testVolume.getId()).getStatus(), Volume.Status.IN_USE); + + assertTrue(volumeAttachmentApi.get().detachVolumeFromServer(testVolume.getId(), serverId), + "Could not detach volume " + testVolume.getId() + " from server " + serverId); + assertEquals(volumeAttachmentApi.get().listAttachmentsOnServer(serverId).size(), 0, + "Number of volumes on server " + serverId + " was not zero."); + } finally { + if (server_id != null) { + api.getServerApi(region).delete(server_id); + } + } } } } http://git-wip-us.apache.org/repos/asf/jclouds/blob/3f2b9376/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/features/ServerApiExpectTest.java ---------------------------------------------------------------------- diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/features/ServerApiExpectTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/features/ServerApiExpectTest.java index e5fecfc..3092bb6 100644 --- a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/features/ServerApiExpectTest.java +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/features/ServerApiExpectTest.java @@ -23,6 +23,7 @@ import static org.testng.Assert.fail; import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpResponse; import org.jclouds.openstack.nova.v2_0.NovaApi; +import org.jclouds.openstack.nova.v2_0.domain.BlockDeviceMapping; import org.jclouds.openstack.nova.v2_0.domain.Server; import org.jclouds.openstack.nova.v2_0.internal.BaseNovaApiExpectTest; import org.jclouds.openstack.nova.v2_0.options.CreateServerOptions; @@ -199,6 +200,32 @@ public class ServerApiExpectTest extends BaseNovaApiExpectTest { new ParseCreatedServerTest().expected().toString()); } + public void testCreateServerWithAttachedDiskWhenResponseIs202() throws Exception { + + HttpRequest createServer = HttpRequest + .builder() + .method("POST") + .endpoint("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v2/3456/servers") + .addHeader("Accept", "application/json") + .addHeader("X-Auth-Token", authToken) + .payload(payloadFromStringWithContentType( + "{\"server\":{\"name\":\"test-e92\",\"imageRef\":\"1241\",\"flavorRef\":\"100\",\"block_device_mapping\":[{\"volume_size\":\"\",\"volume_id\":\"f0c907a5-a26b-48ba-b803-83f6b7450ba5\",\"delete_on_termination\":\"1\",\"device_name\":\"vdb\"}]}}", "application/json")) + .build(); + + + HttpResponse createServerResponse = HttpResponse.builder().statusCode(202).message("HTTP/1.1 202 Accepted") + .payload(payloadFromResourceWithContentType("/new_server.json", "application/json; charset=UTF-8")).build(); + + + NovaApi apiWithNewServer = requestsSendResponses(keystoneAuthWithUsernameAndPasswordAndTenantName, + responseWithKeystoneAccess, createServer, createServerResponse); + + BlockDeviceMapping blockDeviceMapping = BlockDeviceMapping.createOptions("f0c907a5-a26b-48ba-b803-83f6b7450ba5", "vdb").deleteOnTermination(true).build(); + assertEquals(apiWithNewServer.getServerApi("az-1.region-a.geo-1").create("test-e92", "1241", + "100", new CreateServerOptions().blockDeviceMapping(ImmutableSet.of(blockDeviceMapping))).toString(), + new ParseCreatedServerTest().expected().toString()); + } + public void testCreateServerWithDiskConfigAuto() throws Exception { HttpRequest createServer = HttpRequest.builder() .method("POST")
