Repository: jclouds Updated Branches: refs/heads/1.8.x 593fbaca0 -> f510424c4
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/f510424c Tree: http://git-wip-us.apache.org/repos/asf/jclouds/tree/f510424c Diff: http://git-wip-us.apache.org/repos/asf/jclouds/diff/f510424c Branch: refs/heads/1.8.x Commit: f510424c43a943667784dea69fffdd171dc3e626 Parents: 593fbac Author: jasdeep-hundal <[email protected]> Authored: Tue Mar 25 14:12:37 2014 -0700 Committer: Jeremy Daggett <[email protected]> Committed: Thu Sep 18 19:27:46 2014 -0700 ---------------------------------------------------------------------- .../nova/v2_0/domain/BlockDeviceMapping.java | 278 +++++++++++++++++++ .../nova/v2_0/options/CreateServerOptions.java | 39 ++- .../extensions/VolumeAttachmentApiLiveTest.java | 42 ++- .../nova/v2_0/features/ServerApiExpectTest.java | 27 ++ 4 files changed, 382 insertions(+), 4 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/jclouds/blob/f510424c/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..1a01531 --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/domain/BlockDeviceMapping.java @@ -0,0 +1,278 @@ +/* + * 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.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 Objects.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/f510424c/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 8a8bd2c..04e0aee 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 @@ -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; @@ -108,6 +109,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) { @@ -150,6 +152,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; } @@ -176,6 +180,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; @@ -237,6 +243,10 @@ public class CreateServerOptions implements MapBinder { } } + if (!blockDeviceMapping.isEmpty()) { + server.blockDeviceMapping = blockDeviceMapping; + } + return bindToRequest(request, ImmutableMap.of("server", server)); } @@ -375,7 +385,7 @@ public class CreateServerOptions implements MapBinder { public Set<String> getNetworks() { return networks; } - + /** * Get custom networks specified for the server. * @@ -428,7 +438,7 @@ public class CreateServerOptions implements MapBinder { /** * Determines if a configuration drive will be attached to the server or not. - * This can be used for cloud-init or other configuration purposes. + * This can be used for cloud-init or other configuration purposes. */ public boolean getConfigDrive() { return configDrive; @@ -458,6 +468,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 { /** @@ -544,6 +571,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/f510424c/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 473e5c7..14d14c8 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; /** @@ -44,7 +47,7 @@ public class VolumeAttachmentApiLiveTest extends BaseNovaApiLiveTest { private Optional<? extends VolumeApi> volumeApi; private Optional<? extends VolumeAttachmentApi> volumeAttachmentApi; - + private String zone; private Volume testVolume; @@ -98,7 +101,7 @@ public class VolumeAttachmentApiLiveTest extends BaseNovaApiLiveTest { try { final String serverId = server_id = createServerInZone(zone).getId(); - Set<? extends VolumeAttachment> attachments = + Set<? extends VolumeAttachment> attachments = volumeAttachmentApi.get().listAttachmentsOnServer(serverId).toSet(); assertNotNull(attachments); final int before = attachments.size(); @@ -148,7 +151,42 @@ public class VolumeAttachmentApiLiveTest extends BaseNovaApiLiveTest { if (server_id != null) api.getServerApiForZone(zone).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 = createServerInZone(zone, 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.getServerApiForZone(zone).delete(server_id); + } + } } } } http://git-wip-us.apache.org/repos/asf/jclouds/blob/f510424c/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 cfee8a6..78cd0a3 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.getServerApiForZone("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")
