JCLOUDS-514: Support attaching volumes at boot in Nova

This commit is contained in:
jasdeep-hundal 2014-03-25 14:12:37 -07:00 committed by Jeremy Daggett
parent 0ac7dfd377
commit 3f2b9376a1
4 changed files with 380 additions and 1 deletions

View File

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

View File

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

View File

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

View File

@ -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")