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 7a16d7c194..a3ea5134de 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 @@ -23,15 +23,7 @@ import java.util.Set; import org.jclouds.javax.annotation.Nullable; import org.jclouds.location.Zone; import org.jclouds.location.functions.ZoneToEndpoint; -import org.jclouds.openstack.nova.v1_1.extensions.AdminActionsAsyncClient; -import org.jclouds.openstack.nova.v1_1.extensions.FloatingIPAsyncClient; -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.ServerWithSecurityGroupsAsyncClient; -import org.jclouds.openstack.nova.v1_1.extensions.SimpleTenantUsageAsyncClient; -import org.jclouds.openstack.nova.v1_1.extensions.VirtualInterfaceAsyncClient; -import org.jclouds.openstack.nova.v1_1.extensions.VolumeAsyncClient; +import org.jclouds.openstack.nova.v1_1.extensions.*; 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; @@ -153,4 +145,11 @@ public interface NovaAsyncClient { Optional getAdminActionsExtensionForZone( @EndpointParam(parser = ZoneToEndpoint.class) @Nullable String zone); + /** + * Provides asynchronous access to HostAggregate features. + */ + @Delegate + Optional getHostAggregateExtensionForZone( + @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 75d3173bbd..546a3b4809 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 @@ -25,15 +25,7 @@ import org.jclouds.concurrent.Timeout; import org.jclouds.javax.annotation.Nullable; import org.jclouds.location.Zone; import org.jclouds.location.functions.ZoneToEndpoint; -import org.jclouds.openstack.nova.v1_1.extensions.AdminActionsClient; -import org.jclouds.openstack.nova.v1_1.extensions.FloatingIPClient; -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.ServerWithSecurityGroupsClient; -import org.jclouds.openstack.nova.v1_1.extensions.SimpleTenantUsageClient; -import org.jclouds.openstack.nova.v1_1.extensions.VirtualInterfaceClient; -import org.jclouds.openstack.nova.v1_1.extensions.VolumeClient; +import org.jclouds.openstack.nova.v1_1.extensions.*; 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; @@ -153,5 +145,12 @@ public interface NovaClient { @Delegate Optional getAdminActionsExtensionForZone( @EndpointParam(parser = ZoneToEndpoint.class) @Nullable String zone); + + /** + * Provides synchronous access to Aggregate features. + */ + @Delegate + Optional getHostAggregateExtensionForZone( + @EndpointParam(parser = ZoneToEndpoint.class) @Nullable String zone); } diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/binders/BindAggregateMetadataToJsonPayload.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/binders/BindAggregateMetadataToJsonPayload.java new file mode 100644 index 0000000000..f3c867f7c5 --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/binders/BindAggregateMetadataToJsonPayload.java @@ -0,0 +1,39 @@ +/** + * 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.binders; + +import java.util.Map; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.jclouds.json.Json; + +import com.google.inject.TypeLiteral; + +/** + * @author Adam Lowe + */ +@Singleton +public class BindAggregateMetadataToJsonPayload extends BindObjectToJsonPayload> { + @Inject + public BindAggregateMetadataToJsonPayload(Json jsonBinder) { + super(jsonBinder, "metadata", new TypeLiteral>(){}, "set_metadata"); + } +} \ No newline at end of file diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/binders/BindObjectToJsonPayload.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/binders/BindObjectToJsonPayload.java new file mode 100644 index 0000000000..4f4541b111 --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/binders/BindObjectToJsonPayload.java @@ -0,0 +1,86 @@ +/** + * 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.binders; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Map; + +import org.jclouds.http.HttpRequest; +import org.jclouds.json.Json; +import org.jclouds.rest.MapBinder; +import org.jclouds.rest.binders.BindToJsonPayload; +import org.jclouds.rest.internal.GeneratedHttpRequest; + +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; +import com.google.common.collect.Iterables; +import com.google.inject.TypeLiteral; + +/** + * @author Adam Lowe + */ +public abstract class BindObjectToJsonPayload extends BindToJsonPayload implements MapBinder { + private final String fieldName; + private final String wrapperName; + private final TypeLiteral type; + + /** Bind a specific argument to the json payload + * + * @param fieldName the name of the output json field + * @param fieldType the type of the object to select from the method arguments + * @param wrapperName the name of the json field wrapper (if any) + */ + public BindObjectToJsonPayload(Json jsonBinder, String fieldName, TypeLiteral fieldType, String wrapperName) { + super(jsonBinder); + this.fieldName = fieldName; + this.wrapperName = wrapperName; + this.type = fieldType; + } + + public BindObjectToJsonPayload(Json jsonBinder, String fieldName, TypeLiteral fieldType) { + this(jsonBinder, fieldName, fieldType, null); + } + + @Override + public R bindToRequest(R request, Object toBind) { + throw new IllegalStateException("BindMapToJsonPayload needs parameters"); + } + + @SuppressWarnings("unchecked") + @Override + public R bindToRequest(R request, Map postParams) { + Builder payload = ImmutableMap.builder(); + payload.putAll(postParams); + checkArgument(checkNotNull(request, "request") instanceof GeneratedHttpRequest, + "this binder is only valid for GeneratedHttpRequests!"); + GeneratedHttpRequest gRequest = (GeneratedHttpRequest) request; + + T specs = (T) Iterables.find(gRequest.getArgs(), Predicates.instanceOf(type.getRawType())); + payload.put(fieldName, specs); + + if (wrapperName != null) { + return super.bindToRequest(request, ImmutableMap.of(wrapperName, payload.build())); + } + + return super.bindToRequest(request, payload.build()); + } +} \ No newline at end of file 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 b03338efcd..9e3d851a05 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 @@ -35,24 +35,7 @@ import org.jclouds.openstack.keystone.v2_0.config.KeystoneAuthenticationModule; import org.jclouds.openstack.nova.v1_1.NovaAsyncClient; import org.jclouds.openstack.nova.v1_1.NovaClient; import org.jclouds.openstack.nova.v1_1.domain.Extension; -import org.jclouds.openstack.nova.v1_1.extensions.AdminActionsAsyncClient; -import org.jclouds.openstack.nova.v1_1.extensions.AdminActionsClient; -import org.jclouds.openstack.nova.v1_1.extensions.FloatingIPAsyncClient; -import org.jclouds.openstack.nova.v1_1.extensions.FloatingIPClient; -import org.jclouds.openstack.nova.v1_1.extensions.HostAdministrationAsyncClient; -import org.jclouds.openstack.nova.v1_1.extensions.HostAdministrationClient; -import org.jclouds.openstack.nova.v1_1.extensions.KeyPairAsyncClient; -import org.jclouds.openstack.nova.v1_1.extensions.KeyPairClient; -import org.jclouds.openstack.nova.v1_1.extensions.SecurityGroupAsyncClient; -import org.jclouds.openstack.nova.v1_1.extensions.SecurityGroupClient; -import org.jclouds.openstack.nova.v1_1.extensions.ServerWithSecurityGroupsAsyncClient; -import org.jclouds.openstack.nova.v1_1.extensions.ServerWithSecurityGroupsClient; -import org.jclouds.openstack.nova.v1_1.extensions.SimpleTenantUsageAsyncClient; -import org.jclouds.openstack.nova.v1_1.extensions.SimpleTenantUsageClient; -import org.jclouds.openstack.nova.v1_1.extensions.VirtualInterfaceAsyncClient; -import org.jclouds.openstack.nova.v1_1.extensions.VirtualInterfaceClient; -import org.jclouds.openstack.nova.v1_1.extensions.VolumeAsyncClient; -import org.jclouds.openstack.nova.v1_1.extensions.VolumeClient; +import org.jclouds.openstack.nova.v1_1.extensions.*; import org.jclouds.openstack.nova.v1_1.features.ExtensionAsyncClient; import org.jclouds.openstack.nova.v1_1.features.ExtensionClient; import org.jclouds.openstack.nova.v1_1.features.FlavorAsyncClient; @@ -94,6 +77,7 @@ public class NovaRestClientModule extends RestClientModule builder() { + return new ConcreteBuilder(); + } + + public Builder toBuilder() { + return new ConcreteBuilder().fromAggregate(this); + } + + public static abstract class Builder> { + protected abstract T self(); + + private String id; + private String name; + private String availabilityZone; + private Set hosts = ImmutableSet.of(); + private String state; + private Date created = new Date(); + private Date updated; + private Map metadata = ImmutableMap.of(); + + /** + * @see HostAggregate#getId() + */ + public T id(String id) { + this.id = id; + return self(); + } + + /** + * @see HostAggregate#getName() + */ + public T name(String name) { + this.name = name; + return self(); + } + + /** + * @see HostAggregate#getAvailabilityZone() + */ + public T availabilityZone(String availabilityZone) { + this.availabilityZone = availabilityZone; + return self(); + } + + /** + * @see HostAggregate#getHosts() + */ + public T hosts(String... hosts) { + return hosts(ImmutableSet.copyOf(hosts)); + } + + /** + * @see HostAggregate#getHosts() + */ + public T hosts(Set hosts) { + this.hosts = hosts; + return self(); + } + + /** + * @see HostAggregate#getState() + */ + public T state(String state) { + this.state = state; + return self(); + } + + /** + * @see HostAggregate#getCreated() + */ + public T created(Date created) { + this.created = created; + return self(); + } + + /** + * @see HostAggregate#getUpdated() + */ + public T updated(Date updated) { + this.updated = updated; + return self(); + } + + /** + * @see HostAggregate#getMetadata() + */ + public T metadata(Map metadata) { + this.metadata = metadata; + return self(); + } + + public HostAggregate build() { + return new HostAggregate(this); + } + + public T fromAggregate(HostAggregate in) { + return this + .id(in.getId()) + .name(in.getName()) + .availabilityZone(in.getAvailabilityZone()) + .hosts(in.getHosts()) + .state(in.getState()) + .created(in.getCreated()) + .updated(in.getUpdated().orNull()) + .metadata(in.getMetadata()); + } + + } + + private static class ConcreteBuilder extends Builder { + @Override + protected ConcreteBuilder self() { + return this; + } + } + + private final String id; + private final String name; + @SerializedName(value = "availability_zone") + private final String availabilityZone; + private final Set hosts; + @SerializedName(value = "operational_state") + private final String state; + @SerializedName(value = "created_at") + private final Date created; + @SerializedName(value = "updated_at") + private final Optional updated; + private final Map metadata; + + protected HostAggregate(Builder builder) { + this.id = checkNotNull(builder.id, "id"); + this.name = checkNotNull(builder.name, "name"); + this.availabilityZone = checkNotNull(builder.availabilityZone, "availabilityZone"); + this.hosts = ImmutableSet.copyOf(checkNotNull(builder.hosts, "hosts")); + this.state = checkNotNull(builder.state, "state"); + this.created = checkNotNull(builder.created, "created"); + this.updated = Optional.fromNullable(builder.updated); + this.metadata = ImmutableMap.copyOf(checkNotNull(builder.metadata, "metadata")); + } + + // Ensure GSON parsed objects don't have null collections or optionals + protected HostAggregate() { + this.id = null; + this.name = null; + this.availabilityZone = null; + this.hosts = ImmutableSet.of(); + this.state = null; + this.created = null; + this.updated = Optional.absent(); + this.metadata = ImmutableMap.of(); + } + + public String getId() { + return this.id; + } + + public String getName() { + return this.name; + } + + /** + * note: an "Availability Zone" is different from a Nova "Zone" + * + * @return the availability zone this aggregate is in + */ + public String getAvailabilityZone() { + return this.availabilityZone; + } + + public Set getHosts() { + return Collections.unmodifiableSet(this.hosts); + } + + public String getState() { + return this.state; + } + + public Date getCreated() { + return this.created; + } + + public Optional getUpdated() { + return this.updated; + } + + public Map getMetadata() { + return this.metadata; + } + + @Override + public int hashCode() { + return Objects.hashCode(id, name, availabilityZone, hosts, state, created, updated, metadata); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + HostAggregate that = HostAggregate.class.cast(obj); + return Objects.equal(this.id, that.id) + && Objects.equal(this.name, that.name) + && Objects.equal(this.availabilityZone, that.availabilityZone) + && Objects.equal(this.hosts, that.hosts) + && Objects.equal(this.state, that.state) + && Objects.equal(this.created, that.created) + && Objects.equal(this.updated, that.updated) + && Objects.equal(this.metadata, that.metadata) + ; + } + + protected ToStringHelper string() { + return Objects.toStringHelper("") + .add("id", id) + .add("name", name) + .add("availabilityZone", availabilityZone) + .add("hosts", hosts) + .add("state", state) + .add("created", created) + .add("updated", updated) + .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/extensions/ExtensionNamespaces.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/ExtensionNamespaces.java index 3c172cf9e3..d4a9e86147 100644 --- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/ExtensionNamespaces.java +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/ExtensionNamespaces.java @@ -20,7 +20,7 @@ package org.jclouds.openstack.nova.v1_1.extensions; /** * Extension namespaces - * + * * @author Adrian Cole * @see */ @@ -93,6 +93,20 @@ public interface ExtensionNamespaces { /** * Extended Server Status extension */ - public static final String EXTENDED_STATUS = "http://docs.openstack.org/ext/extended_status/api/v1.1"; + public static final String EXTENDED_STATUS = "http://docs.openstack.org/compute/ext/extended_status/api/v1.1"; -} + /** + * Quota Classes extension + */ + public static final String QUOTA_CLASSES = "http://docs.openstack.org/ext/quota-classes-sets/api/v1.1"; + + /** + * Disk Config extension + */ + public static final String DISK_CONFIG = "http://docs.openstack.org/compute/ext/disk_config/api/v1.1"; + + /** + * Aggregates extension + */ + public static final String AGGREGATES = "http://docs.openstack.org/ext/aggregates/api/v1.1"; +} \ No newline at end of file diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/HostAggregateAsyncClient.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/HostAggregateAsyncClient.java new file mode 100644 index 0000000000..79cb209bba --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/HostAggregateAsyncClient.java @@ -0,0 +1,155 @@ +/** + * 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.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +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.concurrent.Timeout; +import org.jclouds.openstack.filters.AuthenticateRequest; +import org.jclouds.openstack.nova.v1_1.binders.BindAggregateMetadataToJsonPayload; +import org.jclouds.openstack.nova.v1_1.domain.HostAggregate; +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.functions.ReturnEmptySetOnNotFoundOr404; +import org.jclouds.rest.functions.ReturnFalseOnNotFoundOr404; +import org.jclouds.rest.functions.ReturnNullOnNotFoundOr404; + +import com.google.common.util.concurrent.ListenableFuture; + +/** + * Provide access to Aggregates in Nova. + * + * @author Adam Lowe + * @see HostAggregateClient + */ +@Extension(of = ServiceType.COMPUTE, namespace = ExtensionNamespaces.AGGREGATES) +@Timeout(duration = 180, timeUnit = TimeUnit.SECONDS) +@RequestFilters(AuthenticateRequest.class) +@Path("/os-aggregates") +public interface HostAggregateAsyncClient { + + /** + * @see HostAggregateClient#listAggregates() + */ + @GET + @SelectJson("aggregates") + @Consumes(MediaType.APPLICATION_JSON) + @ExceptionParser(ReturnEmptySetOnNotFoundOr404.class) + ListenableFuture> listAggregates(); + + /** + * @see HostAggregateClient#getAggregate(String) + */ + @GET + @Path("/{id}") + @SelectJson("aggregate") + @Consumes(MediaType.APPLICATION_JSON) + @ExceptionParser(ReturnNullOnNotFoundOr404.class) + ListenableFuture getAggregate(@PathParam("id") String id); + + /** + * @see HostAggregateClient#createAggregate(String, String) + */ + @POST + @SelectJson("aggregate") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Payload("%7B\"aggregate\":%7B\"name\":\"{name}\",\"availability_zone\":\"{zone}\"%7D%7D") + ListenableFuture createAggregate(@PayloadParam("name") String name, @PayloadParam("zone") String availablityZone); + + /** + * @see HostAggregateClient#updateName + */ + @POST + @Path("/{id}") + @SelectJson("aggregate") + @Consumes(MediaType.APPLICATION_JSON) + @Payload("%7B\"aggregate\":%7B\"name\":\"{name}\"%7D%7D") + ListenableFuture updateName(@PathParam("id") String id, @PayloadParam("name") String name); + + /** + * @see HostAggregateClient#updateAvailabilityZone + */ + @POST + @Path("/{id}") + @SelectJson("aggregate") + @Consumes(MediaType.APPLICATION_JSON) + @Payload("%7B\"aggregate\":%7B\"availability_zone\":\"{zone}\"%7D%7D") + ListenableFuture updateAvailabilityZone(@PathParam("id") String id, @PayloadParam("zone") String availabilityZone); + + /** + * @see HostAggregateClient#deleteAggregate(String) + */ + @DELETE + @Path("/{id}") + @Consumes(MediaType.APPLICATION_JSON) + @ExceptionParser(ReturnFalseOnNotFoundOr404.class) + ListenableFuture deleteAggregate(@PathParam("id") String id); + + /** + * @see HostAggregateClient#addHost(String,String) + */ + @POST + @Path("/{id}/action") + @SelectJson("aggregate") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Payload("%7B\"add_host\":%7B\"host\":\"{host}\"%7D%7D") + ListenableFuture addHost(@PathParam("id") String id, @PayloadParam("host") String host); + + + /** + * @see HostAggregateClient#removeHost(String,String) + */ + @POST + @Path("/{id}/action") + @SelectJson("aggregate") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Payload("%7B\"remove_host\":%7B\"host\":\"{host}\"%7D%7D") + ListenableFuture removeHost(@PathParam("id") String id, @PayloadParam("host") String host); + + /** + * @see HostAggregateClient#setMetadata + */ + @POST + @Path("/{id}/action") + @SelectJson("aggregate") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @MapBinder(BindAggregateMetadataToJsonPayload.class) + ListenableFuture setMetadata(@PathParam("id") String id, Map metadata); +} diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/HostAggregateClient.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/HostAggregateClient.java new file mode 100644 index 0000000000..2548e2e301 --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/HostAggregateClient.java @@ -0,0 +1,93 @@ +/** + * 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.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.jclouds.concurrent.Timeout; +import org.jclouds.openstack.filters.AuthenticateRequest; +import org.jclouds.openstack.nova.v1_1.domain.HostAggregate; +import org.jclouds.openstack.services.Extension; +import org.jclouds.openstack.services.ServiceType; +import org.jclouds.rest.annotations.RequestFilters; + +/** + * Provide access to Host Aggregates in Nova (alias "OS-AGGREGATES") + * + * @author Adam Lowe + * @see HostAggregateAsyncClient + * @see + * @see + */ +@Extension(of = ServiceType.COMPUTE, namespace = ExtensionNamespaces.AGGREGATES) +@Timeout(duration = 180, timeUnit = TimeUnit.SECONDS) +@RequestFilters(AuthenticateRequest.class) +public interface HostAggregateClient { + + /** + * @return the set of host aggregates. + */ + Set listAggregates(); + + /** + * Retrieves the details of an aggregate, hosts and metadata included. + * + * @return the details of the aggregate requested. + */ + HostAggregate getAggregate(String id); + + /** + * Creates an aggregate, given its name and availability zone. + * + * @return the newly created Aggregate + */ + HostAggregate createAggregate(String name, String availabilityZone); + + /** + * Updates the name of an aggregate. + */ + HostAggregate updateName(String id, String name); + + /** + * Updates the availability zone an aggregate. + */ + HostAggregate updateAvailabilityZone(String id, String availabilityZone); + + /** + * Removes an aggregate. + */ + Boolean deleteAggregate(String id); + + /** + * Adds a host to an aggregate + */ + HostAggregate addHost(String id, String host); + + /** + * Removes a host from an aggregate + */ + HostAggregate removeHost(String id, String host); + + /** + * Adds metadata to an aggregate + */ + HostAggregate setMetadata(String id, Map metadata); +} 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 3d023c397f..c5d3219083 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 @@ -74,8 +74,16 @@ public class PresentWhenExtensionAnnotationNamespaceEqualsAnyNamespaceInExtensio URI.create("http://docs.openstack.org/compute/ext/createserverext/api/v1.1")) .put(URI.create(ExtensionNamespaces.ADMIN_ACTIONS), URI.create("http://docs.openstack.org/compute/ext/admin-actions/api/v1.1")) - .put(URI.create(ExtensionNamespaces.EXTENDED_STATUS), - URI.create("http://docs.openstack.org/compute/ext/extended_status/api/v1.1")) + .put(URI.create(ExtensionNamespaces.AGGREGATES), + URI.create("http://docs.openstack.org/compute/ext/aggregates/api/v1.1")) + .put(URI.create(ExtensionNamespaces.FLAVOR_EXTRA_SPECS), + URI.create("http://docs.openstack.org/compute/ext/flavor_extra_specs/api/v1.1")) + .put(URI.create(ExtensionNamespaces.QUOTAS), + URI.create("http://docs.openstack.org/compute/ext/quotas-sets/api/v1.1")) + .put(URI.create(ExtensionNamespaces.QUOTA_CLASSES), + URI.create("http://docs.openstack.org/compute/ext/quota-classes-sets/api/v1.1")) + .put(URI.create(ExtensionNamespaces.VOLUME_TYPES), + URI.create("http://docs.openstack.org/compute/ext/volume_types/api/v1.1")) .build(); @Inject diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/HostAggregateClientExpectTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/HostAggregateClientExpectTest.java new file mode 100644 index 0000000000..55ea9d6dc7 --- /dev/null +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/HostAggregateClientExpectTest.java @@ -0,0 +1,180 @@ +/** + * 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.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.net.URI; + +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.HostAggregate; +import org.jclouds.openstack.nova.v1_1.internal.BaseNovaClientExpectTest; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; + +/** + * Tests HostAggregateClient guice wiring and parsing + * + * @author Adam Lowe + */ +@Test(groups = "unit", testName = "HostAggregateClientExpectTest") +public class HostAggregateClientExpectTest extends BaseNovaClientExpectTest { + private DateService dateService = new SimpleDateFormatDateService(); + + public void testList() { + URI endpoint = URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/3456/os-aggregates"); + HostAggregateClient client = requestsSendResponses(keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(200).payload(payloadFromResource("/host_aggregate_list.json")).build()) + .getHostAggregateExtensionForZone("az-1.region-a.geo-1").get(); + + HostAggregate result = Iterables.getOnlyElement(client.listAggregates()); + assertEquals(result, exampleHostAggregate()); + } + + public void testGet() { + URI endpoint = URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/3456/os-aggregates/1"); + HostAggregateClient client = requestsSendResponses(keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(200).payload(payloadFromResource("/host_aggregate_with_host_details.json")).build()) + .getHostAggregateExtensionForZone("az-1.region-a.geo-1").get(); + + assertEquals(client.getAggregate("1"), exampleHostAggregateWithHost()); + } + + public void testGetFailNotFound() { + URI endpoint = URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/3456/os-aggregates/1"); + HostAggregateClient client = requestsSendResponses(keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).build(), + standardResponseBuilder(404).build()).getHostAggregateExtensionForZone("az-1.region-a.geo-1").get(); + + assertNull(client.getAggregate("1")); + } + + public void testCreateAggregate() { + URI endpoint = URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/3456/os-aggregates"); + HostAggregateClient client = requestsSendResponses(keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).method("POST") + .payload(payloadFromStringWithContentType("{\"aggregate\":{\"name\":\"ubuntu1\",\"availability_zone\":\"nova\"}}", MediaType.APPLICATION_JSON)) + .endpoint(endpoint).build(), + standardResponseBuilder(200).payload(payloadFromResource("/host_aggregate_details.json")).build()) + .getHostAggregateExtensionForZone("az-1.region-a.geo-1").get(); + + assertEquals(client.createAggregate("ubuntu1", "nova"), exampleHostAggregate()); + } + + public void testDeleteAggregate() { + URI endpoint = URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/3456/os-aggregates/1"); + HostAggregateClient client = requestsSendResponses(keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).method("DELETE").build(), + standardResponseBuilder(200).build()).getHostAggregateExtensionForZone("az-1.region-a.geo-1").get(); + + assertTrue(client.deleteAggregate("1")); + } + + public void testDeleteAggregateFailNotFound() { + URI endpoint = URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/3456/os-aggregates/1"); + HostAggregateClient client = requestsSendResponses(keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).method("DELETE").build(), + standardResponseBuilder(404).build()).getHostAggregateExtensionForZone("az-1.region-a.geo-1").get(); + + assertFalse(client.deleteAggregate("1")); + } + + public void testUpdateName() { + URI endpoint = URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/3456/os-aggregates/1"); + HostAggregateClient client = requestsSendResponses(keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).method("POST") + .payload(payloadFromStringWithContentType("{\"aggregate\":{\"name\":\"newaggregatename\"}}", MediaType.APPLICATION_JSON)).build(), + standardResponseBuilder(200).payload(payloadFromResource("/host_aggregate_details.json")).build()).getHostAggregateExtensionForZone("az-1.region-a.geo-1").get(); + + assertEquals(client.updateName("1", "newaggregatename"), exampleHostAggregate()); + } + + public void testUpdateAvailabilityZone() { + URI endpoint = URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/3456/os-aggregates/1"); + HostAggregateClient client = requestsSendResponses(keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).method("POST") + .payload(payloadFromStringWithContentType("{\"aggregate\":{\"availability_zone\":\"zone1\"}}", MediaType.APPLICATION_JSON)).build(), + standardResponseBuilder(200).payload(payloadFromResource("/host_aggregate_details.json")).build()).getHostAggregateExtensionForZone("az-1.region-a.geo-1").get(); + + assertEquals(client.updateAvailabilityZone("1", "zone1"), exampleHostAggregate()); + } + + public void testAddHost() { + URI endpoint = URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/3456/os-aggregates/1/action"); + HostAggregateClient client = requestsSendResponses(keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).method("POST") + .payload(payloadFromStringWithContentType("{\"add_host\":{\"host\":\"ubuntu\"}}", MediaType.APPLICATION_JSON)).build(), + standardResponseBuilder(200).payload(payloadFromResource("/host_aggregate_details.json")).build()).getHostAggregateExtensionForZone("az-1.region-a.geo-1").get(); + + assertEquals(client.addHost("1", "ubuntu"), exampleHostAggregate()); + } + + public void testRemoveHost() { + URI endpoint = URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/3456/os-aggregates/1/action"); + HostAggregateClient client = requestsSendResponses(keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).method("POST") + .payload(payloadFromStringWithContentType("{\"remove_host\":{\"host\":\"ubuntu\"}}", MediaType.APPLICATION_JSON)).build(), + standardResponseBuilder(200).payload(payloadFromResource("/host_aggregate_details.json")).build()).getHostAggregateExtensionForZone("az-1.region-a.geo-1").get(); + + assertEquals(client.removeHost("1", "ubuntu"), exampleHostAggregate()); + } + + + public void testSetMetadata() { + URI endpoint = URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/3456/os-aggregates/1/action"); + HostAggregateClient client = requestsSendResponses(keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse, + standardRequestBuilder(endpoint).method("POST") + .payload(payloadFromStringWithContentType("{\"set_metadata\":{\"metadata\":{\"mykey\":\"some value or other\"}}}", MediaType.APPLICATION_JSON)).build(), + standardResponseBuilder(200).payload(payloadFromResource("/host_aggregate_details.json")).build()).getHostAggregateExtensionForZone("az-1.region-a.geo-1").get(); + + assertEquals(client.setMetadata("1", ImmutableMap.of("mykey", "some value or other")), exampleHostAggregate()); + } + + public HostAggregate exampleHostAggregate() { + return HostAggregate.builder().name("jclouds-test-a").availabilityZone("nova") + .created(dateService.iso8601SecondsDateParse("2012-05-11 11:40:17")) + .updated(dateService.iso8601SecondsDateParse("2012-05-11 11:46:44")) + .state("created").id("1").metadata(ImmutableMap.of("somekey", "somevalue", "anotherkey", "another val")).build(); + } + + public HostAggregate exampleHostAggregateWithHost() { + return exampleHostAggregate().toBuilder().hosts("ubuntu").build(); + } +} diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/HostAggregateClientLiveTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/HostAggregateClientLiveTest.java new file mode 100644 index 0000000000..f81c7e6b1b --- /dev/null +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/HostAggregateClientLiveTest.java @@ -0,0 +1,150 @@ +/** + * 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.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Map; +import java.util.Set; + +import org.jclouds.openstack.nova.v1_1.domain.Host; +import org.jclouds.openstack.nova.v1_1.domain.HostAggregate; +import org.jclouds.openstack.nova.v1_1.internal.BaseNovaClientLiveTest; +import org.testng.annotations.AfterGroups; +import org.testng.annotations.BeforeGroups; +import org.testng.annotations.Test; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; + +/** + * Tests behavior of AggregateClient + * + * @author Adam Lowe + */ +@Test(groups = "live", testName = "AggregateClientLiveTest", singleThreaded = true) +public class HostAggregateClientLiveTest extends BaseNovaClientLiveTest { + private Optional clientOption; + private Optional hostAdminOption; + + private HostAggregate testAggregate; + + @BeforeGroups(groups = {"integration", "live"}) + @Override + public void setupContext() { + super.setupContext(); + String zone = Iterables.getLast(novaContext.getApi().getConfiguredZones(), "nova"); + clientOption = novaContext.getApi().getHostAggregateExtensionForZone(zone); + hostAdminOption = novaContext.getApi().getHostAdministrationExtensionForZone(zone); + } + + @Override + @AfterGroups(groups = {"integration", "live"}) + public void tearDown() { + if (testAggregate != null) { + assertTrue(clientOption.get().deleteAggregate(testAggregate.getId())); + } + super.tearDown(); + } + + public void testCreateAggregate() { + if (clientOption.isPresent()) { + // TODO assuming "nova" availability zone is present + testAggregate = clientOption.get().createAggregate("jclouds-test-a", "nova"); + } + } + + @Test(dependsOnMethods = "testCreateAggregate") + public void testListAndGetAggregate() { + if (clientOption.isPresent()) { + HostAggregateClient client = clientOption.get(); + Set aggregates = client.listAggregates(); + for (HostAggregate aggregate : aggregates) { + assertNotNull(aggregate.getId()); + assertNotNull(aggregate.getName()); + assertNotNull(aggregate.getAvailabilityZone()); + + HostAggregate details = client.getAggregate(aggregate.getId()); + assertEquals(details.getId(), aggregate.getId()); + assertEquals(details.getName(), aggregate.getName()); + assertEquals(details.getAvailabilityZone(), aggregate.getAvailabilityZone()); + assertEquals(details.getHosts(), aggregate.getHosts()); + } + } + } + + @Test(dependsOnMethods = "testCreateAggregate") + public void testModifyMetadata() { + if (clientOption.isPresent()) { + HostAggregateClient client = clientOption.get(); + for (Map theMetaData : ImmutableSet.of( + ImmutableMap.of("somekey", "somevalue"), + ImmutableMap.of("somekey", "some other value", "anotherkey", "another val") + )) { + // Apply changes + HostAggregate details = client.setMetadata(testAggregate.getId(), theMetaData); + + // bug in openstack - metadata values are never removed, so we just checking what we've set + for (String key : theMetaData.keySet()) { + assertEquals(details.getMetadata().get(key), theMetaData.get(key)); + } + + // Re-fetch to double-check + details = client.getAggregate(testAggregate.getId()); + for (String key : theMetaData.keySet()) { + assertEquals(details.getMetadata().get(key), theMetaData.get(key)); + } + } + } + } + + // Note the host will be added, but cannot remove it til + @Test(enabled = false, dependsOnMethods = "testCreateAggregate") + public void testModifyHosts() { + if (clientOption.isPresent() && hostAdminOption.isPresent()) { + HostAggregateClient client = clientOption.get(); + Host host = Iterables.getFirst(hostAdminOption.get().listHosts(), null); + assertNotNull(host); + + String host_id = host.getName(); + assertNotNull(host_id); + HostAggregate details; + + try { + details = client.addHost(testAggregate.getId(), host_id); + + assertEquals(details.getHosts(), ImmutableSet.of(host_id)); + + // re-fetch to double-check + details = client.getAggregate(testAggregate.getId()); + assertEquals(details.getHosts(), ImmutableSet.of(host_id)); + + // TODO wait until status of aggregate isn't CHANGING (hostAdministration.shutdown?) + } finally { + details = client.removeHost(testAggregate.getId(), host_id); + } + + assertEquals(details.getHosts(), ImmutableSet.of()); + } + } +} diff --git a/apis/openstack-nova/src/test/resources/host_aggregate_details.json b/apis/openstack-nova/src/test/resources/host_aggregate_details.json new file mode 100644 index 0000000000..5e939ddab0 --- /dev/null +++ b/apis/openstack-nova/src/test/resources/host_aggregate_details.json @@ -0,0 +1 @@ +{"aggregate": {"name": "jclouds-test-a", "availability_zone": "nova", "deleted": false, "created_at": "2012-05-11 11:40:17", "updated_at": "2012-05-11 11:46:44", "operational_state": "created", "hosts": [], "deleted_at": null, "id": 1, "metadata": {"somekey": "somevalue", "anotherkey": "another val"}} \ No newline at end of file diff --git a/apis/openstack-nova/src/test/resources/host_aggregate_list.json b/apis/openstack-nova/src/test/resources/host_aggregate_list.json new file mode 100644 index 0000000000..92dfd383f1 --- /dev/null +++ b/apis/openstack-nova/src/test/resources/host_aggregate_list.json @@ -0,0 +1 @@ +{"aggregates": [{"name": "jclouds-test-a", "availability_zone": "nova", "deleted": false, "created_at": "2012-05-11 11:40:17", "updated_at": "2012-05-11 11:46:44", "operational_state": "created", "hosts": [], "deleted_at": null, "id": 1, "metadata": {"somekey": "somevalue", "anotherkey": "another val"}}]} \ No newline at end of file diff --git a/apis/openstack-nova/src/test/resources/host_aggregate_with_host_details.json b/apis/openstack-nova/src/test/resources/host_aggregate_with_host_details.json new file mode 100644 index 0000000000..2d132f4698 --- /dev/null +++ b/apis/openstack-nova/src/test/resources/host_aggregate_with_host_details.json @@ -0,0 +1 @@ +{"aggregate": {"name": "jclouds-test-a", "availability_zone": "nova", "deleted": false, "created_at": "2012-05-11 11:40:17", "updated_at": "2012-05-11 11:46:44", "operational_state": "created", "hosts": ["ubuntu"], "deleted_at": null, "id": 1, "metadata": {"somekey": "somevalue", "anotherkey": "another val"}} \ No newline at end of file