diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/NovaAsyncClient.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/NovaAsyncClient.java index da9d6507b5..9f5d32b035 100644 --- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/NovaAsyncClient.java +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/NovaAsyncClient.java @@ -28,6 +28,7 @@ import org.jclouds.openstack.nova.v1_1.extensions.HostAdministrationAsyncClient; import org.jclouds.openstack.nova.v1_1.extensions.KeyPairAsyncClient; import org.jclouds.openstack.nova.v1_1.extensions.SecurityGroupAsyncClient; import org.jclouds.openstack.nova.v1_1.extensions.SimpleTenantUsageAsyncClient; +import org.jclouds.openstack.nova.v1_1.extensions.VolumeAsyncClient; import org.jclouds.openstack.nova.v1_1.features.ExtensionAsyncClient; import org.jclouds.openstack.nova.v1_1.features.FlavorAsyncClient; import org.jclouds.openstack.nova.v1_1.features.ImageAsyncClient; @@ -120,4 +121,10 @@ public interface NovaAsyncClient { Optional getSimpleTenantUsageExtensionForZone( @EndpointParam(parser = ZoneToEndpoint.class) @Nullable String zone); + /** + * Provides asynchronous access to Volume features. + */ + @Delegate + Optional getVolumeExtensionForZone( + @EndpointParam(parser = ZoneToEndpoint.class) @Nullable String zone); } diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/NovaClient.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/NovaClient.java index d33e0ab91b..37dc81e228 100644 --- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/NovaClient.java +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/NovaClient.java @@ -30,6 +30,7 @@ import org.jclouds.openstack.nova.v1_1.extensions.HostAdministrationClient; import org.jclouds.openstack.nova.v1_1.extensions.KeyPairClient; import org.jclouds.openstack.nova.v1_1.extensions.SecurityGroupClient; import org.jclouds.openstack.nova.v1_1.extensions.SimpleTenantUsageClient; +import org.jclouds.openstack.nova.v1_1.extensions.VolumeClient; import org.jclouds.openstack.nova.v1_1.features.ExtensionClient; import org.jclouds.openstack.nova.v1_1.features.FlavorClient; import org.jclouds.openstack.nova.v1_1.features.ImageClient; @@ -122,4 +123,12 @@ public interface NovaClient { Optional getSimpleTenantUsageExtensionForZone( @EndpointParam(parser = ZoneToEndpoint.class) @Nullable String zone); + + /** + * Provides synchronous access to Volume features. + */ + @Delegate + Optional getVolumeExtensionForZone( + @EndpointParam(parser = ZoneToEndpoint.class) @Nullable String zone); + } diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/config/NovaRestClientModule.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/config/NovaRestClientModule.java index fdb3b039a8..8a9a6e20bc 100644 --- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/config/NovaRestClientModule.java +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/config/NovaRestClientModule.java @@ -73,6 +73,7 @@ public class NovaRestClientModule extends RestClientModule builder() { + return new ConcreteBuilder(); + } + + public Builder toBuilder() { + return new ConcreteBuilder().fromVolume(this); + } + + public static abstract class Builder> { + protected abstract T self(); + + private String id; + private Status status; + private int size; + private String zone; + private Date created; + private Set attachments = Sets.newLinkedHashSet(); + private String volumeType; + private String snapshotId; + private String name; + private String description; + private Map metadata = Maps.newHashMap(); + + /** @see Volume#getId() */ + public T id(String id) { + this.id = id; + return self(); + } + + /** @see Volume#getStatus() */ + public T status(Status status) { + this.status = status; + return self(); + } + + /** @see Volume#getSize() */ + public T size(int size) { + this.size = size; + return self(); + } + + /** @see Volume#getZone() */ + public T zone(String zone) { + this.zone = zone; + return self(); + } + + /** @see Volume#getCreated() */ + public T created(Date created) { + this.created = created; + return self(); + } + + /** @see Volume#getAttachments() */ + public T attachments(Set attachments) { + this.attachments = attachments; + return self(); + } + + /** @see Volume#getVolumeType() */ + public T volumeType(String volumeType) { + this.volumeType = volumeType; + return self(); + } + + /** @see Volume#getSnapshotId() */ + public T snapshotId(String snapshotId) { + this.snapshotId = snapshotId; + return self(); + } + + /** @see Volume#getMetadata() */ + public T metadata(Map metadata) { + this.metadata = metadata; + return self(); + } + + /** @see Volume#getName() */ + public T name(String name) { + this.name = name; + return self(); + } + + /** @see Volume#getDescription() */ + public T description(String description) { + this.description = description; + return self(); + } + + public Volume build() { + return new Volume(this); + } + + public T fromVolume(Volume in) { + return this + .id(in.getId()) + .status(in.getStatus()) + .size(in.getSize()) + .zone(in.getZone()) + .created(in.getCreated()) + .attachments(in.getAttachments()) + .volumeType(in.getVolumeType()) + .snapshotId(in.getSnapshotId()) + .metadata(in.getMetadata()) + ; + } + + } + + private static class ConcreteBuilder extends Builder { + @Override + protected ConcreteBuilder self() { + return this; + } + } + + private final String id; + private final Status status; + private final int size; + @SerializedName(value="availabilityZone") + private final String zone; + @SerializedName(value="createdAt") + private final Date created; + private final Set attachments; + private final String volumeType; + private final String snapshotId; + @SerializedName(value="displayName") + private final String name; + @SerializedName(value="displayDescription") + private final String description; + private final Map metadata; + + protected Volume(Builder builder) { + this.id = builder.id; + this.status = builder.status; + this.size = builder.size; + this.zone = builder.zone; + this.created = builder.created; + this.attachments = ImmutableSet.copyOf(checkNotNull(builder.attachments, "attachments")); + this.volumeType = builder.volumeType; + this.snapshotId = builder.snapshotId; + this.name = builder.name; + this.description = builder.description; + this.metadata = ImmutableMap.copyOf(checkNotNull(builder.metadata, "metadata")); + } + + /** + * @return the id of this volume + */ + public String getId() { + return this.id; + } + + /** + * @return the status of this volume + */ + public Status getStatus() { + return this.status; + } + + /** + * @return the size in GB of this volume + */ + public int getSize() { + return this.size; + } + + /** + * @return the availabilityZone containing this volume + */ + public String getZone() { + return this.zone; + } + + /** + * @return the time this volume was created + */ + public Date getCreated() { + return this.created; + } + + /** + * @return the set of attachments (to Servers) + */ + @Nullable + public Set getAttachments() { + return Collections.unmodifiableSet(this.attachments); + } + + /** + * @return the type of this volume + */ + @Nullable + public String getVolumeType() { + return this.volumeType; + } + + @Nullable + public String getSnapshotId() { + return this.snapshotId; + } + + /** + * @return the name of this volume - as displayed in the openstack console + */ + @Nullable + public String getName() { + return this.name; + } + + /** + * @return the description of this volume - as displayed in the openstack console + */ + @Nullable + public String getDescription() { + return this.description; + } + + @Nullable + public Map getMetadata() { + return Collections.unmodifiableMap(this.metadata); + } + + @Override + public int hashCode() { + return Objects.hashCode(id, status, size, zone, created, attachments, volumeType, snapshotId, name, description, metadata); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + Volume that = Volume.class.cast(obj); + return Objects.equal(this.id, that.id) + && Objects.equal(this.status, that.status) + && Objects.equal(this.size, that.size) + && Objects.equal(this.zone, that.zone) + && Objects.equal(this.created, that.created) + && Objects.equal(this.attachments, that.attachments) + && Objects.equal(this.volumeType, that.volumeType) + && Objects.equal(this.snapshotId, that.snapshotId) + && Objects.equal(this.name, that.name) + && Objects.equal(this.description, that.description) + && Objects.equal(this.metadata, that.metadata) + ; + } + + protected ToStringHelper string() { + return Objects.toStringHelper("") + .add("id", id) + .add("status", status) + .add("size", size) + .add("zone", zone) + .add("created", created) + .add("attachments", attachments) + .add("volumeType", volumeType) + .add("snapshotId", snapshotId) + .add("name", name) + .add("description", description) + .add("metadata", metadata) + ; + } + + @Override + public String toString() { + return string().toString(); + } + +} \ No newline at end of file diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/VolumeAttachment.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/VolumeAttachment.java new file mode 100644 index 0000000000..bc33a0ba63 --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/VolumeAttachment.java @@ -0,0 +1,168 @@ +/** + * 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.domain; + +import static com.google.common.base.Preconditions.checkNotNull; + +import org.jclouds.javax.annotation.Nullable; + +import com.google.common.base.Objects; +import com.google.common.base.Objects.ToStringHelper; + +/** + * An Openstack Nova Volume Attachment (describes how Volumes are attached to Servers) + */ +public class VolumeAttachment { + + public static Builder builder() { + return new ConcreteBuilder(); + } + + public Builder toBuilder() { + return new ConcreteBuilder().fromAttachment(this); + } + + public static abstract class Builder> { + protected abstract T self(); + + private String id; + private String volumeId; + private String serverId; + private String device; + + /** @see VolumeAttachment#getId() */ + public T id(String id) { + this.id = id; + return self(); + } + + /** @see VolumeAttachment#getVolumeId() */ + public T volumeId(String volumeId) { + this.volumeId = volumeId; + return self(); + } + + /** @see VolumeAttachment#getServerId() */ + public T serverId(String serverId) { + this.serverId = serverId; + return self(); + } + + /** @see VolumeAttachment#getDevice() */ + public T device(String device) { + this.device = device; + return self(); + } + + public VolumeAttachment build() { + return new VolumeAttachment(this); + } + + public T fromAttachment(VolumeAttachment in) { + return this + .id(in.getId()) + .volumeId(in.getVolumeId()) + .serverId(in.getServerId()) + .device(in.getDevice()) + ; + } + + } + + private static class ConcreteBuilder extends Builder { + @Override + protected ConcreteBuilder self() { + return this; + } + } + + private final String id; + private final String volumeId; + private final String serverId; + private final String device; + + protected VolumeAttachment(Builder builder) { + this.id = checkNotNull(builder.id, "id"); + this.volumeId = checkNotNull(builder.volumeId, "volumeId"); + this.serverId = builder.serverId; + this.device = builder.device; + } + + /** + * @return the attachment id (typically the same as #getVolumeId()) + */ + public String getId() { + return this.id; + } + + /** + * @return the id of the volume attached + */ + public String getVolumeId() { + return this.volumeId; + } + + /** + * @return the id of the server the volume is attached to + */ + @Nullable + public String getServerId() { + return this.serverId; + } + + /** + * @return the device name (e.g. "/dev/vdc") + */ + @Nullable + public String getDevice() { + return this.device; + } + + @Override + public int hashCode() { + return Objects.hashCode(id, volumeId, serverId, device); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + VolumeAttachment that = VolumeAttachment.class.cast(obj); + return Objects.equal(this.id, that.id) + && Objects.equal(this.volumeId, that.volumeId) + && Objects.equal(this.serverId, that.serverId) + && Objects.equal(this.device, that.device) + ; + } + + protected ToStringHelper string() { + return Objects.toStringHelper("") + .add("id", id) + .add("volumeId", volumeId) + .add("serverId", serverId) + .add("device", device) + ; + } + + @Override + public String toString() { + return string().toString(); + } + +} \ No newline at end of file diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/VolumeSnapshot.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/VolumeSnapshot.java new file mode 100644 index 0000000000..d79d3d3231 --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/VolumeSnapshot.java @@ -0,0 +1,233 @@ +/** + * 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.domain; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Date; + +import org.jclouds.javax.annotation.Nullable; + +import com.google.common.base.Objects; +import com.google.common.base.Objects.ToStringHelper; +import com.google.gson.annotations.SerializedName; + +/** + * An Openstack Nova Volume Snapshot + */ +public class VolumeSnapshot { + + public static Builder builder() { + return new ConcreteBuilder(); + } + + public Builder toBuilder() { + return new ConcreteBuilder().fromSnapshot(this); + } + + public static abstract class Builder> { + protected abstract T self(); + + private String id; + private String volumeId; + private Volume.Status status; + private int size; + private Date created; + private String name; + private String description; + + /** @see VolumeSnapshot#getId() */ + public T id(String id) { + this.id = id; + return self(); + } + + /** @see VolumeSnapshot#getVolumeId() */ + public T volumeId(String volumeId) { + this.volumeId = volumeId; + return self(); + } + + /** @see VolumeSnapshot#getStatus() */ + public T status(Volume.Status status) { + this.status = status; + return self(); + } + + /** @see VolumeSnapshot#getSize() */ + public T size(int size) { + this.size = size; + return self(); + } + + /** @see VolumeSnapshot#getCreated() */ + public T created(Date created) { + this.created = created; + return self(); + } + + /** @see VolumeSnapshot#getName() */ + public T name(String name) { + this.name = name; + return self(); + } + + /** @see VolumeSnapshot#getDescription() */ + public T description(String description) { + this.description = description; + return self(); + } + + public VolumeSnapshot build() { + return new VolumeSnapshot(this); + } + + public T fromSnapshot(VolumeSnapshot in) { + return this + .id(in.getId()) + .volumeId(in.getVolumeId()) + .status(in.getStatus()) + .size(in.getSize()) + .created(in.getCreated()) + .name(in.getName()) + .description(in.getDescription()) + ; + } + + } + + private static class ConcreteBuilder extends Builder { + @Override + protected ConcreteBuilder self() { + return this; + } + } + + private final String id; + private final String volumeId; + private final Volume.Status status; + private final int size; + @SerializedName(value="createdAt") + private final Date created; + @SerializedName(value="displayName") + private final String name; + @SerializedName(value="displayDescription") + private final String description; + + protected VolumeSnapshot(Builder builder) { + this.id = checkNotNull(builder.id, "id"); + this.volumeId = checkNotNull(builder.volumeId, "volumeId"); + this.status = checkNotNull(builder.status, "status"); + this.size = builder.size; + this.created = builder.created; + this.name = builder.name; + this.description = builder.description; + } + + /** + * @return the id of this snapshot + */ + public String getId() { + return this.id; + } + + /** + * @return the id of the Volume this snapshot was taken from + */ + public String getVolumeId() { + return this.volumeId; + } + + /** + * @return the status of this snapshot + */ + public Volume.Status getStatus() { + return this.status; + } + + /** + * @return the size in GB of the volume this snapshot was taken from + */ + public int getSize() { + return this.size; + } + + /** + * @return the data the snapshot was taken + */ + @Nullable + public Date getCreated() { + return this.created; + } + + /** + * @return the name of this snapshot - as displayed in the openstack console + */ + @Nullable + public String getName() { + return this.name; + } + + + /** + * @return the description of this snapshot - as displayed in the openstack console + */ + @Nullable + public String getDescription() { + return this.description; + } + + @Override + public int hashCode() { + return Objects.hashCode(id, volumeId, status, size, created, name, description); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + VolumeSnapshot that = VolumeSnapshot.class.cast(obj); + return Objects.equal(this.id, that.id) + && Objects.equal(this.volumeId, that.volumeId) + && Objects.equal(this.status, that.status) + && Objects.equal(this.size, that.size) + && Objects.equal(this.created, that.created) + && Objects.equal(this.name, that.name) + && Objects.equal(this.description, that.description) + ; + } + + protected ToStringHelper string() { + return Objects.toStringHelper("") + .add("id", id) + .add("volumeId", volumeId) + .add("status", status) + .add("size", size) + .add("created", created) + .add("name", name) + .add("description", description) + ; + } + + @Override + public String toString() { + return string().toString(); + } + +} \ No newline at end of file diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeAsyncClient.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeAsyncClient.java new file mode 100644 index 0000000000..97584b1d07 --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeAsyncClient.java @@ -0,0 +1,234 @@ +/** + * 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.extensions; + +import java.util.Set; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.jclouds.openstack.filters.AuthenticateRequest; +import org.jclouds.openstack.nova.v1_1.domain.VolumeAttachment; +import org.jclouds.openstack.nova.v1_1.domain.VolumeSnapshot; +import org.jclouds.openstack.nova.v1_1.domain.Volume; +import org.jclouds.openstack.nova.v1_1.options.CreateVolumeSnapshotOptions; +import org.jclouds.openstack.nova.v1_1.options.CreateVolumeOptions; +import org.jclouds.openstack.services.Extension; +import org.jclouds.openstack.services.ServiceType; +import org.jclouds.rest.annotations.ExceptionParser; +import org.jclouds.rest.annotations.MapBinder; +import org.jclouds.rest.annotations.Payload; +import org.jclouds.rest.annotations.PayloadParam; +import org.jclouds.rest.annotations.RequestFilters; +import org.jclouds.rest.annotations.SelectJson; +import org.jclouds.rest.annotations.SkipEncoding; +import org.jclouds.rest.functions.ReturnEmptySetOnNotFoundOr404; +import org.jclouds.rest.functions.ReturnFalseOnNotFoundOr404; +import org.jclouds.rest.functions.ReturnNullOnNotFoundOr404; + +import com.google.common.util.concurrent.ListenableFuture; + +/** + * Provides synchronous access to Volumes. + *

+ * + * @see org.jclouds.openstack.nova.v1_1.extensions.VolumeAsyncClient + * @author Adam Lowe + */ +@Extension(of = ServiceType.COMPUTE, namespace = ExtensionNamespaces.VOLUMES) +@SkipEncoding({'/', '='}) +@RequestFilters(AuthenticateRequest.class) +public interface VolumeAsyncClient { + /** + * Returns a summary list of volumes. + * + * @return the list of volumes + */ + @GET + @Path("/os-volumes") + @SelectJson("volumes") + @Consumes(MediaType.APPLICATION_JSON) + @ExceptionParser(ReturnEmptySetOnNotFoundOr404.class) + ListenableFuture> listVolumes(); + + /** + * Returns a detailed list of volumes. + * + * @return the list of volumes. + */ + @GET + @Path("/os-volumes/detail") + @SelectJson("volumes") + @Consumes(MediaType.APPLICATION_JSON) + @ExceptionParser(ReturnEmptySetOnNotFoundOr404.class) + ListenableFuture> listVolumesInDetail(); + + /** + * Return data about the given volume. + * + * @return details of a specific volume. + */ + @GET + @Path("/os-volumes/{id}") + @SelectJson("volume") + @Consumes(MediaType.APPLICATION_JSON) + @ExceptionParser(ReturnNullOnNotFoundOr404.class) + ListenableFuture getVolume(@PathParam("id") String volumeId); + + /** + * Creates a new volume + * + * @return the new Snapshot + */ + @POST + @Path("/os-volumes") + @SelectJson("volume") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @MapBinder(CreateVolumeOptions.class) + ListenableFuture createVolume(@PayloadParam("size") int sizeGB, CreateVolumeOptions... options); + + /** + * Delete a volume. + * + * @return true if successful + */ + @DELETE + @Path("/os-volumes/{id}") + @Consumes(MediaType.APPLICATION_JSON) + @ExceptionParser(ReturnFalseOnNotFoundOr404.class) + ListenableFuture deleteVolume(@PathParam("id") String volumeId); + + /** + * List volume attachments for a given instance. + * + * @return all Floating IPs + */ + @GET + @Path("/servers/{server_id}/os-volume_attachments") + @SelectJson("volumeAttachments") + @Consumes(MediaType.APPLICATION_JSON) + @ExceptionParser(ReturnEmptySetOnNotFoundOr404.class) + ListenableFuture> listAttachmentsOnServer(@PathParam("server_id") String serverId); + + /** + * Get a specific attached volume. + * + * @return data about the given volume attachment. + */ + @GET + @Path("/servers/{server_id}/os-volume_attachments/{id}") + @SelectJson("volumeAttachment") + @Consumes(MediaType.APPLICATION_JSON) + @ExceptionParser(ReturnNullOnNotFoundOr404.class) + ListenableFuture getAttachmentForVolumeOnServer(@PathParam("id") String volumeId, + @PathParam("server_id") String serverId); + + /** + * Attach a volume to an instance + * + * @return the new Attachment + */ + @POST + @Path("/servers/{server_id}/os-volume_attachments") + @SelectJson("volumeAttachment") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Payload("%7B\"volumeAttachment\":%7B\"volumeId\":\"{id}\",\"device\":\"{device}\"%7D%7D") + ListenableFuture attachVolumeToServerAsDevice(@PayloadParam("id") String volumeId, + @PathParam("server_id") String serverId, @PayloadParam("device") String device); + + /** + * Detach a Volume from an instance. + * + * @return true if successful + */ + @DELETE + @Path("/servers/{server_id}/os-volume_attachments/{id}") + @Consumes(MediaType.APPLICATION_JSON) + @ExceptionParser(ReturnFalseOnNotFoundOr404.class) + ListenableFuture detachVolumeFromServer(@PathParam("id") String volumeId, @PathParam("server_id") String serverId); + + /** + * Returns a summary list of snapshots. + * + * @return the list of snapshots + */ + @GET + @Path("/os-snapshots") + @SelectJson("snapshots") + @Consumes(MediaType.APPLICATION_JSON) + @ExceptionParser(ReturnEmptySetOnNotFoundOr404.class) + ListenableFuture> listSnapshots(); + + /** + * Returns a summary list of snapshots. + * + * @return the list of snapshots + */ + @GET + @Path("/os-snapshots/detail") + @SelectJson("snapshots") + @Consumes(MediaType.APPLICATION_JSON) + @ExceptionParser(ReturnEmptySetOnNotFoundOr404.class) + ListenableFuture> listSnapshotsInDetail(); + + /** + * Return data about the given snapshot. + * + * @return details of a specific snapshot. + */ + @GET + @Path("/os-snapshots/{id}") + @SelectJson("snapshot") + @Consumes(MediaType.APPLICATION_JSON) + @ExceptionParser(ReturnNullOnNotFoundOr404.class) + ListenableFuture getSnapshot(@PathParam("id") String snapshotId); + + /** + * Creates a new Snapshot + * + * @return the new Snapshot + */ + @POST + @Path("/os-snapshots") + @SelectJson("snapshot") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @MapBinder(CreateVolumeSnapshotOptions.class) + ListenableFuture createSnapshot(@PayloadParam("volume_id") String volumeId, CreateVolumeSnapshotOptions... options); + + /** + * Delete a snapshot. + * + * @return true if successful + */ + @DELETE + @Path("/os-snapshots/{id}") + @Consumes(MediaType.APPLICATION_JSON) + @ExceptionParser(ReturnFalseOnNotFoundOr404.class) + ListenableFuture deleteSnapshot(@PathParam("id") String snapshotId); + +} diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClient.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClient.java new file mode 100644 index 0000000000..3f40523d79 --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClient.java @@ -0,0 +1,141 @@ +/** + * 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.extensions; + +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.jclouds.concurrent.Timeout; +import org.jclouds.openstack.nova.v1_1.domain.VolumeAttachment; +import org.jclouds.openstack.nova.v1_1.domain.VolumeSnapshot; +import org.jclouds.openstack.nova.v1_1.domain.Volume; +import org.jclouds.openstack.nova.v1_1.options.CreateVolumeSnapshotOptions; +import org.jclouds.openstack.nova.v1_1.options.CreateVolumeOptions; +import org.jclouds.openstack.services.Extension; +import org.jclouds.openstack.services.ServiceType; + +/** + * Provides synchronous access to Volumes. + *

+ * + * @see org.jclouds.openstack.nova.v1_1.extensions.VolumeAsyncClient + * @author Adam Lowe + */ +@Extension(of = ServiceType.COMPUTE, namespace = ExtensionNamespaces.VOLUMES) +@Timeout(duration = 180, timeUnit = TimeUnit.SECONDS) +public interface VolumeClient { + /** + * Returns a summary list of snapshots. + * + * @return the list of snapshots + */ + Set listVolumes(); + + /** + * Returns a detailed list of volumes. + * + * @return the list of volumes. + */ + Set listVolumesInDetail(); + + /** + * Return data about the given volume. + * + * @return details of a specific snapshot. + */ + Volume getVolume(String volumeId); + + /** + * Creates a new Snapshot + * + * @return the new Snapshot + */ + Volume createVolume(int sizeGB, CreateVolumeOptions... options); + + /** + * Delete a snapshot. + * + * @return true if successful + */ + Boolean deleteVolume(String volumeId); + + /** + * List volume attachments for a given instance. + * + * @return all Floating IPs + */ + Set listAttachmentsOnServer(String serverId); + + /** + * Get a specific attached volume. + * + * @return data about the given volume attachment. + */ + VolumeAttachment getAttachmentForVolumeOnServer(String volumeId, String serverId); + + /** + * Attach a volume to an instance + * + * @return data about the new volume attachment + */ + VolumeAttachment attachVolumeToServerAsDevice(String volumeId, String serverId, String device); + + /** + * Detach a Volume from an instance. + * + * @return true if successful + */ + Boolean detachVolumeFromServer(String server_id, String volumeId); + + /** + * Returns a summary list of snapshots. + * + * @return the list of snapshots + */ + Set listSnapshots(); + + /** + * Returns a summary list of snapshots. + * + * @return the list of snapshots + */ + Set listSnapshotsInDetail(); + + /** + * Return data about the given snapshot. + * + * @return details of a specific snapshot. + */ + VolumeSnapshot getSnapshot(String snapshotId); + + /** + * Creates a new Snapshot + * + * @return the new Snapshot + */ + VolumeSnapshot createSnapshot(String volumeId, CreateVolumeSnapshotOptions... options); + + /** + * Delete a snapshot. + * + * @return true if successful + */ + Boolean deleteSnapshot(String snapshotId); + +} diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/functions/PresentWhenExtensionAnnotationNamespaceEqualsAnyNamespaceInExtensionsSet.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/functions/PresentWhenExtensionAnnotationNamespaceEqualsAnyNamespaceInExtensionsSet.java index 5db7bb9d46..85406b5e4c 100644 --- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/functions/PresentWhenExtensionAnnotationNamespaceEqualsAnyNamespaceInExtensionsSet.java +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/functions/PresentWhenExtensionAnnotationNamespaceEqualsAnyNamespaceInExtensionsSet.java @@ -66,6 +66,8 @@ public class PresentWhenExtensionAnnotationNamespaceEqualsAnyNamespaceInExtensio URI.create("http://docs.openstack.org/compute/ext/os-simple-tenant-usage/api/v1.1")) .put(URI.create(ExtensionNamespaces.HOSTS), URI.create("http://docs.openstack.org/compute/ext/hosts/api/v1.1")) + .put(URI.create(ExtensionNamespaces.VOLUMES), + URI.create("http://docs.openstack.org/compute/ext/volumes/api/v1.1")) .build(); @Inject diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/options/CreateVolumeOptions.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/options/CreateVolumeOptions.java new file mode 100644 index 0000000000..e92f3566db --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/options/CreateVolumeOptions.java @@ -0,0 +1,224 @@ +/** + * 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.options; + +import static com.google.common.base.Objects.equal; +import static com.google.common.base.Objects.toStringHelper; +import static com.google.common.base.Preconditions.*; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import javax.inject.Inject; + +import org.jclouds.encryption.internal.Base64; +import org.jclouds.http.HttpRequest; +import org.jclouds.openstack.nova.v1_1.domain.SecurityGroup; +import org.jclouds.rest.MapBinder; +import org.jclouds.rest.binders.BindToJsonPayload; +import org.jclouds.util.Preconditions2; + +import com.google.common.base.Objects; +import com.google.common.base.Objects.ToStringHelper; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.gson.annotations.SerializedName; + +/** + * @author Adam Lowe + */ +public class CreateVolumeOptions implements MapBinder { + public static final CreateVolumeOptions NONE = new CreateVolumeOptions(); + + @Inject + private BindToJsonPayload jsonBinder; + + private String name; + private String description; + private String volumeType; + private String availabilityZone; + private String snapshotId; + private Map metadata = ImmutableMap.of(); + + @Override + public R bindToRequest(R request, Map postParams) { + Map image = Maps.newHashMap(); + image.putAll(postParams); + if (name != null) + image.put("display_name", name); + if (description != null) + image.put("display_description", description); + if (!metadata.isEmpty()) + image.put("metadata", metadata); + return jsonBinder.bindToRequest(request, ImmutableMap.of("volume", image)); + } + + @Override + public R bindToRequest(R request, Object toBind) { + throw new IllegalStateException("CreateVolume is a POST operation"); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof CreateVolumeOptions)) return false; + final CreateVolumeOptions other = CreateVolumeOptions.class.cast(object); + return equal(volumeType, other.volumeType) && equal(availabilityZone, other.availabilityZone) && equal(snapshotId, other.snapshotId) + && equal(name, other.name) && equal(description, other.description) && equal(metadata, other.metadata); + } + + @Override + public int hashCode() { + return Objects.hashCode(volumeType, availabilityZone, snapshotId, name, description, metadata); + } + + protected ToStringHelper string() { + return toStringHelper("").add("volumeType", volumeType).add("availabilityZone", availabilityZone) + .add("snapshotId", snapshotId).add("name", name).add("description", description).add("metadata", metadata); + } + + @Override + public String toString() { + return string().toString(); + } + + /** + * Custom cloud server metadata can also be supplied at launch time. This + * metadata is stored in the API system where it is retrievable by querying + * the API for server status. The maximum size of the metadata key and value + * is each 255 bytes and the maximum number of key-value pairs that can be + * supplied per volume is 5. + */ + public CreateVolumeOptions metadata(Map metadata) { + checkNotNull(metadata, "metadata"); + checkArgument(metadata.size() <= 5, + "you cannot have more then 5 metadata values. You specified: " + metadata.size()); + for (Entry entry : metadata.entrySet()) { + checkArgument( + entry.getKey().getBytes().length < 255, + String.format("maximum length of metadata key is 255 bytes. Key specified %s is %d bytes", + entry.getKey(), entry.getKey().getBytes().length)); + checkArgument(entry.getKey().getBytes().length < 255, String.format( + "maximum length of metadata value is 255 bytes. Value specified for %s (%s) is %d bytes", + entry.getKey(), entry.getValue(), entry.getValue().getBytes().length)); + } + this.metadata = ImmutableMap.copyOf(metadata); + return this; + } + + public CreateVolumeOptions name(String name) { + this.name = name; + return this; + } + + public CreateVolumeOptions description(String description) { + this.description = description; + return this; + } + + public CreateVolumeOptions volumeType(String volumeType) { + this.volumeType = volumeType; + return this; + } + + public CreateVolumeOptions availabilityZone(String availabilityZone) { + this.availabilityZone = availabilityZone; + return this; + } + + public CreateVolumeOptions snapshotId(String snapshotId) { + this.snapshotId = snapshotId; + return this; + } + + public String getVolumeType() { + return volumeType; + } + + public String getAvailabilityZone() { + return availabilityZone; + } + + public String getSnapshotId() { + return snapshotId; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public Map getMetadata() { + return metadata; + } + + public static class Builder { + /** + * @see CreateVolumeOptions#getName() + */ + public static CreateVolumeOptions name(String name) { + return new CreateVolumeOptions().name(name); + } + /** + * @see CreateVolumeOptions#getDescription() + */ + public static CreateVolumeOptions description(String description) { + return new CreateVolumeOptions().description(description); + } + + /** + * @see CreateVolumeOptions#getVolumeType() + */ + public static CreateVolumeOptions volumeType(String volumeType) { + return new CreateVolumeOptions().volumeType(volumeType); + } + + /** + * @see CreateVolumeOptions#getAvailabilityZone() + */ + public static CreateVolumeOptions availabilityZone(String availabilityZone) { + return new CreateVolumeOptions().availabilityZone(availabilityZone); + } + + /** + * @see CreateVolumeOptions#getSnapshotId() + */ + public static CreateVolumeOptions snapshotId(String snapshotId) { + return new CreateVolumeOptions().snapshotId(snapshotId); + } + + /** + * @see CreateVolumeOptions#getMetadata() + */ + public static CreateVolumeOptions metadata(Map metadata) { + return new CreateVolumeOptions().metadata(metadata); + } + } + +} diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/options/CreateVolumeSnapshotOptions.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/options/CreateVolumeSnapshotOptions.java new file mode 100644 index 0000000000..df9b628970 --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/options/CreateVolumeSnapshotOptions.java @@ -0,0 +1,140 @@ +/** + * 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.options; + +import static com.google.common.base.Objects.equal; +import static com.google.common.base.Objects.toStringHelper; + +import java.util.Map; + +import javax.inject.Inject; + +import org.jclouds.http.HttpRequest; +import org.jclouds.rest.MapBinder; +import org.jclouds.rest.binders.BindToJsonPayload; + +import com.google.common.base.Objects; +import com.google.common.base.Objects.ToStringHelper; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; + +/** + * @author Adam Lowe + */ +public class CreateVolumeSnapshotOptions implements MapBinder { + public static final CreateVolumeSnapshotOptions NONE = new CreateVolumeSnapshotOptions(); + + @Inject + private BindToJsonPayload jsonBinder; + + private String name; + private String description; + private boolean force = false; + + @Override + public R bindToRequest(R request, Map postParams) { + Map data = Maps.newHashMap(postParams); + if (name != null) + data.put("display_name", name); + if (description != null) + data.put("display_description", description); + if (force) + data.put("force", "true"); + return jsonBinder.bindToRequest(request, ImmutableMap.of("snapshot", data)); + } + + @Override + public R bindToRequest(R request, Object toBind) { + throw new IllegalStateException("CreateSnapshot is a POST operation"); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof CreateVolumeSnapshotOptions)) return false; + final CreateVolumeSnapshotOptions other = CreateVolumeSnapshotOptions.class.cast(object); + return equal(name, other.name) && equal(description, other.description); + } + + @Override + public int hashCode() { + return Objects.hashCode(name, description); + } + + protected ToStringHelper string() { + return toStringHelper("").add("name", name).add("description", description); + } + + @Override + public String toString() { + return string().toString(); + } + + public CreateVolumeSnapshotOptions name(String name) { + this.name = name; + return this; + } + + public CreateVolumeSnapshotOptions description(String description) { + this.description = description; + return this; + } + + public CreateVolumeSnapshotOptions force() { + this.force = true; + return this; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public boolean isForce() { + return force; + } + + public static class Builder { + /** + * @see CreateVolumeSnapshotOptions#getName() + */ + public static CreateVolumeSnapshotOptions name(String name) { + return new CreateVolumeSnapshotOptions().name(name); + } + /** + * @see CreateVolumeSnapshotOptions#getDescription() + */ + public static CreateVolumeSnapshotOptions description(String description) { + return new CreateVolumeSnapshotOptions().description(description); + } + + /** + * @see CreateVolumeSnapshotOptions#isForce() + */ + public static CreateVolumeSnapshotOptions force() { + return new CreateVolumeSnapshotOptions().force(); + } + } + +} diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClientExpectTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClientExpectTest.java new file mode 100644 index 0000000000..3349f0ac92 --- /dev/null +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClientExpectTest.java @@ -0,0 +1,458 @@ +/** + * 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.extensions; + +import static org.testng.Assert.*; + +import java.net.URI; +import java.util.Set; + +import javax.ws.rs.core.MediaType; + +import org.jclouds.date.DateService; +import org.jclouds.date.internal.SimpleDateFormatDateService; +import org.jclouds.openstack.nova.v1_1.domain.Volume; +import org.jclouds.openstack.nova.v1_1.domain.VolumeAttachment; +import org.jclouds.openstack.nova.v1_1.domain.VolumeSnapshot; +import org.jclouds.openstack.nova.v1_1.internal.BaseNovaClientExpectTest; +import org.jclouds.openstack.nova.v1_1.options.CreateVolumeOptions; +import org.jclouds.openstack.nova.v1_1.options.CreateVolumeSnapshotOptions; +import org.jclouds.rest.AuthorizationException; +import org.jclouds.rest.ResourceNotFoundException; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; + +/** + * Tests VolumeClient guice wiring and parsing + * + * @author Adam Lowe + */ +@Test(groups = "unit", testName = "VolumeClientExpectTest") +public class VolumeClientExpectTest extends BaseNovaClientExpectTest { + private DateService dateService = new SimpleDateFormatDateService(); + + public void testListVolumes() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(200).payload(payloadFromResource("/volume_list.json")).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + Set volumes = client.listVolumes(); + assertEquals(volumes, ImmutableSet.of(testVolume())); + } + + public void testListVolumesFail() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(404).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + Set volumes = client.listVolumes(); + assertTrue(volumes.isEmpty()); + } + + public void testListVolumesInDetail() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes/detail"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(200).payload(payloadFromResource("/volume_list_detail.json")).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + Set volumes = client.listVolumesInDetail(); + assertEquals(volumes, ImmutableSet.of(testVolume())); + } + + public void testListVolumesInDetailFail() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes/detail"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(404).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + Set volumes = client.listVolumesInDetail(); + assertTrue(volumes.isEmpty()); + } + + public void testCreateVolume() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint) + .method("POST") + .payload(payloadFromStringWithContentType("{\"volume\":{\"display_name\":\"jclouds-test-volume\",\"display_description\":\"description of test volume\",\"size\":\"1\"}}", MediaType.APPLICATION_JSON)) + .build(), + standardResponseBuilder(200).payload(payloadFromResource("/volume_details.json")).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + Volume volume = client.createVolume(1, CreateVolumeOptions.Builder.name("jclouds-test-volume").description("description of test volume")); + assertEquals(volume, testVolume()); + } + + @Test(expectedExceptions = ResourceNotFoundException.class) + public void testCreateVolumeFail() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint) + .endpoint(endpoint) + .method("POST") + .payload(payloadFromStringWithContentType("{\"volume\":{\"display_name\":\"jclouds-test-volume\",\"display_description\":\"description of test volume\",\"size\":\"1\"}}", MediaType.APPLICATION_JSON)) + .build(), + standardResponseBuilder(404).payload(payloadFromResource("/volume_details.json")).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + client.createVolume(1, CreateVolumeOptions.Builder.name("jclouds-test-volume").description("description of test volume")); + } + + public void testGetVolume() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes/1"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(200).payload(payloadFromResource("/volume_details.json")).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + Volume volume = client.getVolume("1"); + assertEquals(volume, testVolume()); + // double-check equals() + assertEquals(volume.getStatus(), Volume.Status.IN_USE); + assertEquals(volume.getDescription(), "This is a test volume"); + assertEquals(volume.getZone(), "nova"); + assertEquals(volume.getName(), "test"); + assertEquals(volume.getStatus(), Volume.Status.IN_USE); + assertEquals(Iterables.getOnlyElement(volume.getAttachments()), testAttachment()); + } + + public void testGetVolumeFail() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes/1"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(404).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + assertNull(client.getVolume("1")); + } + + public void testDeleteVolume() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes/1"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).method("DELETE").build(), + standardResponseBuilder(200).payload(payloadFromResource("/attachment_details.json")).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + assertTrue(client.deleteVolume("1")); + } + + public void testDeleteVolumeFail() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes/1"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).method("DELETE").build(), + standardResponseBuilder(404).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + assertFalse(client.deleteVolume("1")); + } + + public void testListAttachments() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/servers/instance-1/os-volume_attachments"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(200).payload(payloadFromResource("/attachment_list.json")).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + Set attachments = client.listAttachmentsOnServer("instance-1"); + assertEquals(attachments, ImmutableSet.of(testAttachment())); + // double-check individual fields + VolumeAttachment attachment = Iterables.getOnlyElement(attachments); + assertEquals(attachment.getDevice(), "/dev/vdc"); + assertEquals(attachment.getServerId(), "b4785058-cb80-491b-baa3-e4ee6546450e"); + assertEquals(attachment.getId(), "1"); + assertEquals(attachment.getVolumeId(), "1"); + } + + @Test(expectedExceptions = AuthorizationException.class) + public void testListAttachmentsFail() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/servers/instance-2/os-volume_attachments"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(401).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + client.listAttachmentsOnServer("instance-2"); + } + + public void testGetAttachment() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/servers/instance-1/os-volume_attachments/1"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(200).payload(payloadFromResource("/attachment_details.json")).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + VolumeAttachment attachment = client.getAttachmentForVolumeOnServer("1", "instance-1"); + assertEquals(attachment, testAttachment()); + } + + public void testGetAttachmentFail() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/servers/instance-1/os-volume_attachments/1"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(404).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + assertNull(client.getAttachmentForVolumeOnServer("1", "instance-1")); + } + + public void testAttachVolume() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/servers/instance-1/os-volume_attachments"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).method("POST") + .payload(payloadFromStringWithContentType("{\"volumeAttachment\":{\"volumeId\":\"1\",\"device\":\"/dev/vdc\"}}", MediaType.APPLICATION_JSON)).endpoint(endpoint).build(), + standardResponseBuilder(200).payload(payloadFromResource("/attachment_details.json")).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + VolumeAttachment result = client.attachVolumeToServerAsDevice("1", "instance-1", "/dev/vdc"); + assertEquals(result, testAttachment()); + } + + @Test(expectedExceptions = ResourceNotFoundException.class) + public void testAttachVolumeFail() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/servers/instance-1/os-volume_attachments"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).method("POST") + .payload(payloadFromStringWithContentType("{\"volumeAttachment\":{\"volumeId\":\"1\",\"device\":\"/dev/vdc\"}}", MediaType.APPLICATION_JSON)).endpoint(endpoint).build(), + standardResponseBuilder(404).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + client.attachVolumeToServerAsDevice("1", "instance-1","/dev/vdc"); + } + + public void testDetachVolume() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/servers/instance-1/os-volume_attachments/1"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).method("DELETE").build(), + standardResponseBuilder(200).payload(payloadFromResource("/attachment_details.json")).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + assertTrue(client.detachVolumeFromServer("1", "instance-1")); + } + + public void testDetachVolumeFail() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/servers/instance-1/os-volume_attachments/1"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).method("DELETE").build(), + standardResponseBuilder(404).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + assertFalse(client.detachVolumeFromServer("1", "instance-1")); + } + + public void testListSnapshots() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(200).payload(payloadFromResource("/snapshot_list.json")).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + Set snapshots = client.listSnapshots(); + assertEquals(snapshots, ImmutableSet.of(testSnapshot())); + } + + public void testListSnapshotsFail() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(404).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + Set snapshots = client.listSnapshots(); + assertTrue(snapshots.isEmpty()); + } + + public void testGetSnapshot() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots/1"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(200).payload(payloadFromResource("/snapshot_details.json")).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + VolumeSnapshot snapshot = client.getSnapshot("1"); + assertEquals(snapshot, testSnapshot()); + } + + public void testGetSnapshotFail() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots/1"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(404).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + assertNull(client.getSnapshot("1")); + } + + public void testListSnapshotsInDetail() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots/detail"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(200).payload(payloadFromResource("/snapshot_list_detail.json")).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + Set snapshots = client.listSnapshotsInDetail(); + assertEquals(snapshots, ImmutableSet.of(testSnapshot())); + + // double-check individual fields + VolumeSnapshot snappy = Iterables.getOnlyElement(snapshots); + assertEquals(snappy.getId(), "7"); + assertEquals(snappy.getVolumeId(), "9"); + assertEquals(snappy.getStatus(), Volume.Status.AVAILABLE); + assertEquals(snappy.getDescription(), "jclouds live test snapshot"); + assertEquals(snappy.getName(), "jclouds-live-test"); + assertEquals(snappy.getSize(), 1); + } + + public void testListSnapshotsInDetailFail() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots/detail"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(404).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + Set snapshots = client.listSnapshotsInDetail(); + assertTrue(snapshots.isEmpty()); + } + + public void testCreateSnapshot() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint) + .method("POST") + .payload(payloadFromStringWithContentType("{\"snapshot\":{\"display_name\":\"jclouds-live-test\",\"volume_id\":\"13\",\"display_description\":\"jclouds live test snapshot\",\"force\":\"true\"}}", MediaType.APPLICATION_JSON)) + .build(), + standardResponseBuilder(200).payload(payloadFromResource("/snapshot_details.json")).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + VolumeSnapshot snapshot = client.createSnapshot("13", CreateVolumeSnapshotOptions.Builder.name("jclouds-live-test").description("jclouds live test snapshot").force()); + assertEquals(snapshot, testSnapshot()); + } + + @Test(expectedExceptions = AuthorizationException.class) + public void testCreateSnapshotFail() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint) + .method("POST") + .payload(payloadFromStringWithContentType("{\"snapshot\":{\"display_name\":\"jclouds-live-test\",\"volume_id\":\"13\",\"display_description\":\"jclouds live test snapshot\",\"force\":\"true\"}}", MediaType.APPLICATION_JSON)) + .build(), + standardResponseBuilder(401).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + client.createSnapshot("13", CreateVolumeSnapshotOptions.Builder.name("jclouds-live-test").description("jclouds live test snapshot").force()); + } + + public void testDeleteSnapshot() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots/1"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).method("DELETE").build(), + standardResponseBuilder(200).payload(payloadFromResource("/snapshot_details.json")).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + assertTrue(client.deleteSnapshot("1")); + } + + @Test(expectedExceptions = AuthorizationException.class) + public void testDeleteSnapshotFail() { + URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots/1"); + VolumeClient client = requestsSendResponses( + keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).method("DELETE").build(), + standardResponseBuilder(401).build() + ).getVolumeExtensionForZone("az-1.region-a.geo-1").get(); + + client.deleteSnapshot("1"); + } + + protected Volume testVolume() { + return Volume.builder().status(Volume.Status.IN_USE).description("This is a test volume").zone("nova").name("test") + .attachments(ImmutableSet.of(testAttachment())).size(1).id("1").created(dateService.iso8601SecondsDateParse("2012-04-23 12:16:45")).build(); + } + + protected VolumeAttachment testAttachment() { + return VolumeAttachment.builder().device("/dev/vdc").serverId("b4785058-cb80-491b-baa3-e4ee6546450e").id("1").volumeId("1").build(); + } + + protected VolumeSnapshot testSnapshot() { + return VolumeSnapshot.builder().id("7").volumeId("9").description("jclouds live test snapshot").status(Volume.Status.AVAILABLE) + .name("jclouds-live-test").size(1).created(dateService.iso8601SecondsDateParse("2012-04-24 13:34:42")).build(); + } +} \ No newline at end of file diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClientLiveTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClientLiveTest.java new file mode 100644 index 0000000000..bca364837c --- /dev/null +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClientLiveTest.java @@ -0,0 +1,268 @@ +/** + * 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.extensions; + +import static org.testng.Assert.*; + +import java.util.Set; + +import org.jclouds.openstack.domain.Resource; +import org.jclouds.openstack.nova.v1_1.domain.VolumeAttachment; +import org.jclouds.openstack.nova.v1_1.domain.VolumeSnapshot; +import org.jclouds.openstack.nova.v1_1.domain.Volume; +import org.jclouds.openstack.nova.v1_1.internal.BaseNovaClientLiveTest; +import org.jclouds.openstack.nova.v1_1.options.CreateVolumeSnapshotOptions; +import org.jclouds.openstack.nova.v1_1.options.CreateVolumeOptions; +import org.jclouds.predicates.RetryablePredicate; +import org.testng.annotations.AfterGroups; +import org.testng.annotations.BeforeGroups; +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.Iterables; + +/** + * Tests behavior of VolumeClient + * + * @author Adam Lowe + */ +@Test(groups = "live", testName = "VolumeClientLiveTest", singleThreaded = true) +public class VolumeClientLiveTest extends BaseNovaClientLiveTest { + + private VolumeClient client; + private String zone; + + private Volume testVolume; + private VolumeSnapshot testSnapshot; + + @BeforeGroups(groups = { "integration", "live" }) + @Override + public void setupContext() { + super.setupContext(); + zone = Iterables.getFirst(novaContext.getApi().getConfiguredZones(), "nova"); + Optional optClient = novaContext.getApi().getVolumeExtensionForZone(zone); + if (optClient.isPresent()) { + client = optClient.get(); + } + } + + @AfterGroups(groups = "live", alwaysRun = true) + @Override + protected void tearDown() { + if (client != null) { + if (testSnapshot != null) { + final String snapshotId = testSnapshot.getId(); + assertTrue(client.deleteSnapshot(snapshotId)); + assertTrue(new RetryablePredicate(new Predicate() { + @Override + public boolean apply(VolumeClient volumeClient) { + return client.getSnapshot(snapshotId) == null; + } + }, 30 * 1000L).apply(client)); + } + if (testVolume != null) { + final String volumeId = testVolume.getId(); + assertTrue(client.deleteVolume(volumeId)); + assertTrue(new RetryablePredicate(new Predicate() { + @Override + public boolean apply(VolumeClient volumeClient) { + return client.getVolume(volumeId) == null; + } + }, 180 * 1000L).apply(client)); + } + } + super.tearDown(); + } + + public void testCreateVolume() throws Exception { + testVolume = client.createVolume(1, CreateVolumeOptions.Builder.name("jclouds-test-volume").description("description of test volume").availabilityZone(zone)); + for (int i=0; i<100 && testVolume.getStatus() == Volume.Status.CREATING; i++) { + Thread.sleep(100); + testVolume = client.getVolume(testVolume.getId()); + } + assertEquals(testVolume.getStatus(), Volume.Status.AVAILABLE); + } + + @Test(dependsOnMethods = "testCreateVolume") + public void testListVolumes() throws Exception { + if (client != null) { + Set volumes = client.listVolumes(); + assertNotNull(volumes); + boolean foundIt = false; + for (Volume vol : volumes) { + Volume details = client.getVolume(vol.getId()); + assertNotNull(details); + if (Objects.equal(details.getId(), testVolume.getId())) { + foundIt = true; + } + } + assertTrue(foundIt, "Failed to find the volume we created in listVolumes() response"); + } + } + + @Test(dependsOnMethods = "testCreateVolume") + public void testListVolumesInDetail() throws Exception { + if (client != null) { + Set volumes = client.listVolumesInDetail(); + assertNotNull(volumes); + assertTrue(volumes.contains(testVolume)); + boolean foundIt = false; + for (Volume vol : volumes) { + Volume details = client.getVolume(vol.getId()); + assertNotNull(details); + assertNotNull(details.getId()); + assertNotNull(details.getCreated()); + assertTrue(details.getSize() > -1); + + assertEquals(details.getId(), vol.getId()); + assertEquals(details.getSize(), vol.getSize()); + assertEquals(details.getName(), vol.getName()); + assertEquals(details.getDescription(), vol.getDescription()); + assertEquals(details.getCreated(), vol.getCreated()); + if (Objects.equal(details.getId(), testVolume.getId())) { + foundIt = true; + } + } + assertTrue(foundIt, "Failed to find the volume we previously created in listVolumesInDetail() response"); + } + } + + @Test(dependsOnMethods = "testCreateVolume") + public void testCreateSnapshot() throws Exception { + if (client != null) { + testSnapshot = client.createSnapshot(testVolume.getId(), CreateVolumeSnapshotOptions.Builder.name("jclouds-live-test").description("jclouds live test snapshot").force()); + assertNotNull(testSnapshot); + assertNotNull(testSnapshot.getId()); + final String snapshotId = testSnapshot.getId(); + assertNotNull(testSnapshot.getStatus()); + assertTrue(testSnapshot.getSize() > -1); + assertNotNull(testSnapshot.getCreated()); + + assertTrue(new RetryablePredicate(new Predicate() { + @Override + public boolean apply(VolumeClient volumeClient) { + return client.getSnapshot(snapshotId).getStatus() == Volume.Status.AVAILABLE; + } + }, 30 * 1000L).apply(client)); + } + } + + @Test(dependsOnMethods = "testCreateSnapshot") + public void testListSnapshots() throws Exception { + if (client != null) { + Set snapshots = client.listSnapshots(); + assertNotNull(snapshots); + boolean foundIt = false; + for (VolumeSnapshot snap : snapshots) { + VolumeSnapshot details = client.getSnapshot(snap.getId()); + if (Objects.equal(snap.getVolumeId(), testVolume.getId())) { + foundIt = true; + } + assertNotNull(details); + assertEquals(details.getId(), snap.getId()); + assertEquals(details.getVolumeId(), snap.getVolumeId()); + } + assertTrue(foundIt, "Failed to find the snapshot we previously created in listSnapshots() response"); + } + } + + @Test(dependsOnMethods = "testCreateSnapshot") + public void testListSnapshotsInDetail() throws Exception { + if (client != null) { + Set snapshots = client.listSnapshotsInDetail(); + assertNotNull(snapshots); + boolean foundIt = false; + for (VolumeSnapshot snap : snapshots) { + VolumeSnapshot details = client.getSnapshot(snap.getId()); + if (Objects.equal(snap.getVolumeId(), testVolume.getId())) { + foundIt = true; + assertSame(details, testSnapshot); + } + assertSame(details, snap); + } + + assertTrue(foundIt, "Failed to find the snapshot we created in listSnapshotsInDetail() response"); + } + } + + private void assertSame(VolumeSnapshot a, VolumeSnapshot b) { + assertNotNull(a); + assertNotNull(b); + assertEquals(a.getId(), b.getId()); + assertEquals(a.getDescription(), b.getDescription()); + assertEquals(a.getName(), b.getName()); + assertEquals(a.getVolumeId(), b.getVolumeId()); + } + + @Test(enabled=false, dependsOnMethods = "testCreateVolume") // disabled as it alters an existing server + public void testAttachments() throws Exception { + if (client != null) { + Set servers = novaContext.getApi().getServerClientForZone(zone).listServers(); + if (!servers.isEmpty()) { + final String serverId = Iterables.getFirst(servers, null).getId(); + Set attachments = client.listAttachmentsOnServer(serverId); + assertNotNull(attachments); + final int before = attachments.size(); + + VolumeAttachment testAttachment = client.attachVolumeToServerAsDevice(testVolume.getId(), serverId, "/dev/vdf"); + assertNotNull(testAttachment.getId()); + assertEquals(testAttachment.getVolumeId(), testVolume.getId()); + + assertTrue(new RetryablePredicate(new Predicate() { + @Override + public boolean apply(VolumeClient volumeClient) { + return client.listAttachmentsOnServer(serverId).size() == before+1; + } + }, 60 * 1000L).apply(client)); + + attachments = client.listAttachmentsOnServer(serverId); + assertNotNull(attachments); + assertEquals(attachments.size(), before+1); + + assertEquals(client.getVolume(testVolume.getId()).getStatus(), Volume.Status.IN_USE); + + boolean foundIt = false; + for (VolumeAttachment att : attachments) { + VolumeAttachment details = client.getAttachmentForVolumeOnServer(att.getVolumeId(), serverId); + assertNotNull(details); + assertNotNull(details.getId()); + assertNotNull(details.getServerId()); + assertNotNull(details.getVolumeId()); + if (Objects.equal(details.getVolumeId(), testVolume.getId())) { + foundIt = true; + assertEquals(details.getDevice(), "/dev/vdf"); + assertEquals(details.getServerId(), serverId); + } + } + + assertTrue(foundIt, "Failed to find the attachment we created in listAttachments() response"); + + client.detachVolumeFromServer(testVolume.getId(), serverId); + assertTrue(new RetryablePredicate(new Predicate() { + @Override + public boolean apply(VolumeClient volumeClient) { + return client.listAttachmentsOnServer(serverId).size() == before; + } + }, 60 * 1000L).apply(client)); + } + } + } +} diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/internal/BaseNovaExpectTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/internal/BaseNovaExpectTest.java index 336f507ba7..8815279eb4 100644 --- a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/internal/BaseNovaExpectTest.java +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/internal/BaseNovaExpectTest.java @@ -20,6 +20,8 @@ package org.jclouds.openstack.nova.v1_1.internal; import java.net.URI; +import javax.ws.rs.core.MediaType; + import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpResponse; import org.jclouds.openstack.keystone.v2_0.internal.KeystoneFixture; @@ -68,4 +70,14 @@ public class BaseNovaExpectTest extends BaseRestClientExpectTest { unmatchedExtensionsOfNovaResponse = HttpResponse.builder().statusCode(200) .payload(payloadFromResource("/extension_list.json")).build(); } + + protected HttpRequest.Builder standardRequestBuilder(URI endpoint) { + return HttpRequest.builder().method("GET") + .headers(ImmutableMultimap.of("Accept", MediaType.APPLICATION_JSON, "X-Auth-Token", authToken)) + .endpoint(endpoint); + } + + protected HttpResponse.Builder standardResponseBuilder(int status) { + return HttpResponse.builder().statusCode(status); + } } diff --git a/apis/openstack-nova/src/test/resources/attachment_details.json b/apis/openstack-nova/src/test/resources/attachment_details.json new file mode 100644 index 0000000000..c436406780 --- /dev/null +++ b/apis/openstack-nova/src/test/resources/attachment_details.json @@ -0,0 +1 @@ +{"volumeAttachment": {"device": "/dev/vdc", "serverId": "b4785058-cb80-491b-baa3-e4ee6546450e", "id": 1, "volumeId": 1}} \ No newline at end of file diff --git a/apis/openstack-nova/src/test/resources/attachment_list.json b/apis/openstack-nova/src/test/resources/attachment_list.json new file mode 100644 index 0000000000..b40de0bd46 --- /dev/null +++ b/apis/openstack-nova/src/test/resources/attachment_list.json @@ -0,0 +1 @@ +{"volumeAttachments": [{"device": "/dev/vdc", "serverId": "b4785058-cb80-491b-baa3-e4ee6546450e", "id": 1, "volumeId": 1}]} \ No newline at end of file diff --git a/apis/openstack-nova/src/test/resources/snapshot_details.json b/apis/openstack-nova/src/test/resources/snapshot_details.json new file mode 100644 index 0000000000..375127b705 --- /dev/null +++ b/apis/openstack-nova/src/test/resources/snapshot_details.json @@ -0,0 +1 @@ +{"snapshot": {"status": "available", "displayDescription": "jclouds live test snapshot", "displayName": "jclouds-live-test", "volumeId": 9, "id": 7, "createdAt": "2012-04-24 13:34:42", "size": 1}} \ No newline at end of file diff --git a/apis/openstack-nova/src/test/resources/snapshot_list.json b/apis/openstack-nova/src/test/resources/snapshot_list.json new file mode 100644 index 0000000000..03980b1697 --- /dev/null +++ b/apis/openstack-nova/src/test/resources/snapshot_list.json @@ -0,0 +1 @@ +{"snapshots": [{"status": "available", "displayDescription": "jclouds live test snapshot", "displayName": "jclouds-live-test", "volumeId": 9, "id": 7, "createdAt": "2012-04-24 13:34:42", "size": 1}]} \ No newline at end of file diff --git a/apis/openstack-nova/src/test/resources/snapshot_list_detail.json b/apis/openstack-nova/src/test/resources/snapshot_list_detail.json new file mode 100644 index 0000000000..03980b1697 --- /dev/null +++ b/apis/openstack-nova/src/test/resources/snapshot_list_detail.json @@ -0,0 +1 @@ +{"snapshots": [{"status": "available", "displayDescription": "jclouds live test snapshot", "displayName": "jclouds-live-test", "volumeId": 9, "id": 7, "createdAt": "2012-04-24 13:34:42", "size": 1}]} \ No newline at end of file diff --git a/apis/openstack-nova/src/test/resources/volume_details.json b/apis/openstack-nova/src/test/resources/volume_details.json new file mode 100644 index 0000000000..8dd11dc4e6 --- /dev/null +++ b/apis/openstack-nova/src/test/resources/volume_details.json @@ -0,0 +1 @@ +{"volume": {"status": "in-use", "displayDescription": "This is a test volume", "availabilityZone": "nova", "displayName": "test", "attachments": [{"device": "/dev/vdc", "serverId": "b4785058-cb80-491b-baa3-e4ee6546450e", "id": 1, "volumeId": 1}], "volumeType": null, "snapshotId": null, "size": 1, "id": 1, "createdAt": "2012-04-23 12:16:45", "metadata": {}}} \ No newline at end of file diff --git a/apis/openstack-nova/src/test/resources/volume_list.json b/apis/openstack-nova/src/test/resources/volume_list.json new file mode 100644 index 0000000000..d2179e799d --- /dev/null +++ b/apis/openstack-nova/src/test/resources/volume_list.json @@ -0,0 +1 @@ +{"volumes": [{"status": "in-use", "displayDescription": "This is a test volume", "availabilityZone": "nova", "displayName": "test", "attachments": [{"device": "/dev/vdc", "serverId": "b4785058-cb80-491b-baa3-e4ee6546450e", "id": 1, "volumeId": 1}], "volumeType": null, "snapshotId": null, "size": 1, "id": 1, "createdAt": "2012-04-23 12:16:45", "metadata": {}}]} \ No newline at end of file diff --git a/apis/openstack-nova/src/test/resources/volume_list_detail.json b/apis/openstack-nova/src/test/resources/volume_list_detail.json new file mode 100644 index 0000000000..d2179e799d --- /dev/null +++ b/apis/openstack-nova/src/test/resources/volume_list_detail.json @@ -0,0 +1 @@ +{"volumes": [{"status": "in-use", "displayDescription": "This is a test volume", "availabilityZone": "nova", "displayName": "test", "attachments": [{"device": "/dev/vdc", "serverId": "b4785058-cb80-491b-baa3-e4ee6546450e", "id": 1, "volumeId": 1}], "volumeType": null, "snapshotId": null, "size": 1, "id": 1, "createdAt": "2012-04-23 12:16:45", "metadata": {}}]} \ No newline at end of file