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

Reply via email to