From 1797b27ed489a5b464352ba230c69c1e9c00edf8 Mon Sep 17 00:00:00 2001 From: Adam Lowe Date: Tue, 8 May 2012 14:31:11 +0100 Subject: [PATCH] Adding OptionalTypeAdaptorFactory to handle the parsing of Optional values --- .../compute/NovaComputeServiceAdapter.java | 10 +- .../nova/v1_1/config/NovaParserModule.java | 39 ++- .../openstack/nova/v1_1/domain/Server.java | 232 ++++++------------ .../nova/v1_1/domain/ServerCreated.java | 95 +++++++ .../v1_1/domain/ServerExtendedAttributes.java | 146 +++++++++++ .../v1_1/domain/ServerExtendedStatus.java | 158 ++++++++++++ .../nova/v1_1/features/ServerAsyncClient.java | 3 +- .../nova/v1_1/features/ServerClient.java | 3 +- .../functions/OrphanedGroupsByZoneIdTest.java | 4 +- .../ServerInZoneToNodeMetadataTest.java | 41 +++- .../AdminActionsClientLiveTest.java | 14 +- .../v1_1/internal/BaseNovaClientLiveTest.java | 9 +- .../v1_1/parse/ParseCreatedServerTest.java | 33 +-- .../parse/ParseServerDetailsEssexTest.java | 140 ++++++----- .../ParseServerWithAllExtensionsTest.java | 106 ++++++++ .../resources/server_details_devstack.json | 44 ++++ .../jclouds/openstack/domain/Resource.java | 6 + .../org/jclouds/json/config/GsonModule.java | 2 + .../internal/OptionalTypeAdapterFactory.java | 79 ++++++ .../OptionalTypeAdapterFactoryTest.java | 130 ++++++++++ 20 files changed, 1016 insertions(+), 278 deletions(-) create mode 100644 apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/ServerCreated.java create mode 100644 apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/ServerExtendedAttributes.java create mode 100644 apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/ServerExtendedStatus.java create mode 100644 apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/parse/ParseServerWithAllExtensionsTest.java create mode 100644 apis/openstack-nova/src/test/resources/server_details_devstack.json create mode 100644 core/src/main/java/org/jclouds/json/internal/OptionalTypeAdapterFactory.java create mode 100644 core/src/test/java/org/jclouds/json/internal/OptionalTypeAdapterFactoryTest.java diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/compute/NovaComputeServiceAdapter.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/compute/NovaComputeServiceAdapter.java index 1bc2e8a52e..6e5e26502a 100644 --- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/compute/NovaComputeServiceAdapter.java +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/compute/NovaComputeServiceAdapter.java @@ -42,6 +42,7 @@ import org.jclouds.openstack.nova.v1_1.compute.strategy.ApplyNovaTemplateOptions import org.jclouds.openstack.nova.v1_1.domain.Flavor; import org.jclouds.openstack.nova.v1_1.domain.Image; import org.jclouds.openstack.nova.v1_1.domain.KeyPair; +import org.jclouds.openstack.nova.v1_1.domain.ServerCreated; import org.jclouds.openstack.nova.v1_1.domain.RebootType; import org.jclouds.openstack.nova.v1_1.domain.Server; import org.jclouds.openstack.nova.v1_1.domain.zonescoped.FlavorInZone; @@ -119,17 +120,14 @@ public class NovaComputeServiceAdapter implements String flavorId = template.getHardware().getProviderId(); logger.debug(">> creating new server zone(%s) name(%s) image(%s) flavor(%s) options(%s)", zoneId, name, imageId, flavorId, options); - Server lightweightServer = novaClient.getServerClientForZone(zoneId).createServer(name, imageId, flavorId, options); - Server heavyweightServer = novaClient.getServerClientForZone(zoneId).getServer(lightweightServer.getId()); - Server server = Server.builder().fromServer(heavyweightServer) - .adminPass(lightweightServer.getAdminPass()) - .build(); + ServerCreated lightweightServer = novaClient.getServerClientForZone(zoneId).createServer(name, imageId, flavorId, options); + Server server = novaClient.getServerClientForZone(zoneId).getServer(lightweightServer.getId()); logger.trace("<< server(%s)", server.getId()); ServerInZone serverInZone = new ServerInZone(server, zoneId); if (!privateKey.isPresent()) - credentialsBuilder.password(server.getAdminPass()); + credentialsBuilder.password(lightweightServer.getAdminPass()); return new NodeAndInitialCredentials(serverInZone, serverInZone.slashEncode(), credentialsBuilder .build()); } diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/config/NovaParserModule.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/config/NovaParserModule.java index 3911554c21..5915395cdf 100644 --- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/config/NovaParserModule.java +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/config/NovaParserModule.java @@ -28,8 +28,11 @@ import org.jclouds.json.config.GsonModule; import org.jclouds.json.config.GsonModule.DateAdapter; import org.jclouds.openstack.nova.v1_1.domain.HostResourceUsage; import org.jclouds.openstack.nova.v1_1.domain.Server; +import org.jclouds.openstack.nova.v1_1.domain.ServerExtendedAttributes; +import org.jclouds.openstack.nova.v1_1.domain.ServerExtendedStatus; import org.jclouds.openstack.nova.v1_1.domain.ServerWithSecurityGroups; +import com.google.common.base.Objects; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; import com.google.gson.JsonArray; @@ -51,9 +54,10 @@ public class NovaParserModule extends AbstractModule { @Provides @Singleton public Map provideCustomAdapterBindings() { - return ImmutableMap. of( + return ImmutableMap.of( HostResourceUsage.class, new HostResourceUsageAdapter(), - ServerWithSecurityGroups.class, new ServerWithSecurityGroupsAdapter() + ServerWithSecurityGroups.class, new ServerWithSecurityGroupsAdapter(), + Server.class, new ServerAdapter() ); } @@ -98,7 +102,7 @@ public class NovaParserModule extends AbstractModule { Set names = Sets.newLinkedHashSet(); if (jsonElement.getAsJsonObject().get("security_groups") != null) { JsonArray x = jsonElement.getAsJsonObject().get("security_groups").getAsJsonArray(); - for(JsonElement y : x) { + for (JsonElement y : x) { names.add(y.getAsJsonObject().get("name").getAsString()); } result.securityGroupNames(names); @@ -106,4 +110,33 @@ public class NovaParserModule extends AbstractModule { return result.build(); } } + + @Singleton + public static class ServerAdapter implements JsonDeserializer { + @Override + public Server deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext context) + throws JsonParseException { + Server serverBase = apply((ServerInternal) context.deserialize(jsonElement, ServerInternal.class)); + Server.Builder result = Server.builder().fromServer(serverBase); + ServerExtendedStatus extendedStatus = context.deserialize(jsonElement, ServerExtendedStatus.class); + if (!Objects.equal(extendedStatus, ServerExtendedStatus.builder().build())) { + result.extendedStatus(extendedStatus); + } + ServerExtendedAttributes extraAttributes = context.deserialize(jsonElement, ServerExtendedAttributes.class); + if (!Objects.equal(extraAttributes, ServerExtendedAttributes.builder().build())) { + result.extraAttributes(extraAttributes); + } + return result.build(); + } + + public Server apply(ServerInternal in) { + return in.toBuilder().build(); + } + + private static class ServerInternal extends Server { + protected ServerInternal() { + } + } + } + } diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/Server.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/Server.java index 452145d652..72cb75eb96 100644 --- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/Server.java +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/Server.java @@ -30,6 +30,8 @@ import org.jclouds.openstack.domain.Resource; import org.jclouds.openstack.nova.v1_1.extensions.KeyPairClient; import org.jclouds.util.Multimaps2; +import com.google.common.base.Objects.ToStringHelper; +import com.google.common.base.Optional; import com.google.common.base.Predicates; import com.google.common.base.Strings; import com.google.common.base.Objects.ToStringHelper; @@ -67,7 +69,7 @@ public class Server extends Resource { NodeState.TERMINATED), UNKNOWN(NodeState.UNRECOGNIZED), ERROR(NodeState.ERROR), UNRECOGNIZED( NodeState.UNRECOGNIZED), PAUSED(NodeState.SUSPENDED); - private final NodeState nodeState; + protected final NodeState nodeState; Status(NodeState nodeState) { this.nodeState = nodeState; @@ -111,17 +113,15 @@ public class Server extends Resource { private Server.Status status; private Resource image; private Resource flavor; - private String adminPass; private String keyName; private String configDrive; private Multimap addresses = ImmutableMultimap.of(); private Map metadata = ImmutableMap.of(); - private String taskState; - private String vmState; - private String powerState; - private String instanceName; - private String hostName; - private String hypervisorName; + // Extended status extension + private ServerExtendedStatus extendedStatus; + // Extended server attributes extension + private ServerExtendedAttributes extendedAttributes; + // Disk Config extension private String diskConfig; /** @@ -212,14 +212,6 @@ public class Server extends Resource { return self(); } - /** - * @see Server#getAdminPass() - */ - public T adminPass(String adminPass) { - this.adminPass = adminPass; - return self(); - } - /** * @see Server#getKeyName() */ @@ -251,52 +243,20 @@ public class Server extends Resource { this.metadata = metadata; return self(); } - + /** - * @see Server#getTaskState() + * @see Server#getExtendedStatus() */ - public T taskState(String taskState) { - this.taskState = taskState; + public T extendedStatus(ServerExtendedStatus extendedStatus) { + this.extendedStatus = extendedStatus; return self(); } /** - * @see Server#getVmState() + * @see Server#getExtendedAttributes() */ - public T vmState(String vmState) { - this.vmState = vmState; - return self(); - } - - /** - * @see Server#getPowerState() - */ - public T powerState(String powerState) { - this.powerState = powerState; - return self(); - } - - /** - * @see Server#getInstanceName() - */ - public T instanceName(String instanceName) { - this.instanceName = instanceName; - return self(); - } - - /** - * @see Server#getHostName() - */ - public T hostName(String hostName) { - this.hostName = hostName; - return self(); - } - - /** - * @see Server#getHypervisorName() - */ - public T hypervisorName(String hypervisorName) { - this.hypervisorName = hypervisorName; + public T extraAttributes(ServerExtendedAttributes extendedAttributes) { + this.extendedAttributes = extendedAttributes; return self(); } @@ -325,18 +285,13 @@ public class Server extends Resource { .status(in.getStatus()) .image(in.getImage()) .flavor(in.getFlavor()) - .adminPass(in.getAdminPass()) .keyName(in.getKeyName()) .configDrive(in.getConfigDrive()) .addresses(in.getAddresses()) .metadata(in.getMetadata()) - .taskState(in.getTaskState()) - .vmState(in.getVmState()) - .powerState(in.getPowerState()) - .instanceName(in.getInstanceName()) - .hostName(in.getHostName()) - .hypervisorName(in.getHypervisorName()) - .diskConfig(in.getDiskConfig()); + .extendedStatus(in.getExtendedStatus().orNull()) + .extraAttributes(in.getExtendedAttributes().orNull()) + .diskConfig(in.getDiskConfig().orNull()); } } @@ -347,47 +302,38 @@ public class Server extends Resource { } } - protected final String uuid; + private final String uuid; @SerializedName("tenant_id") - protected final String tenantId; + private final String tenantId; @SerializedName("user_id") - protected final String userId; - protected final Date updated; - protected final Date created; - protected final String hostId; - protected final String accessIPv4; - protected final String accessIPv6; - protected final Status status; - protected final Resource image; - protected final Resource flavor; - protected final String adminPass; + private final String userId; + private final Date updated; + private final Date created; + private final String hostId; + private final String accessIPv4; + private final String accessIPv6; + private final Status status; + private final Resource image; + private final Resource flavor; @SerializedName("key_name") - protected final String keyName; + private final String keyName; @SerializedName("config_drive") - protected final String configDrive; + private final String configDrive; // TODO: get gson multimap adapter! - protected final Map> addresses; - protected final Map metadata; + private final Map> addresses; + private final Map metadata; // Extended status extension - @SerializedName("OS-EXT-STS:task_state") - protected final String taskState; - @SerializedName("OS-EXT-STS:vm_state") - protected final String vmState; - @SerializedName("OS-EXT-STS:power_state") - protected final String powerState; + // @Prefixed("OS-EXT-STS:") + private final Optional extendedStatus; // Extended server attributes extension - @SerializedName("OS-EXT-SRV-ATTR:instance_name") - protected final String instanceName; - @SerializedName("OS-EXT-SRV-ATTR:host") - protected final String hostName; - @SerializedName("OS-EXT-SRV-ATTR:hypervisor_hostname") - protected final String hypervisorName; + // @Prefixed("OS-EXT-SRV-ATTR:") + private final Optional extendedAttributes; // Disk Config extension @SerializedName("OS-DCF:diskConfig") - protected final String diskConfig; + private final Optional diskConfig; protected Server(Builder builder) { super(builder); @@ -405,17 +351,34 @@ public class Server extends Resource { this.flavor = checkNotNull(builder.flavor, "flavor"); this.metadata = Maps.newHashMap(builder.metadata); this.addresses = Multimaps2.toOldSchool(ImmutableMultimap.copyOf(checkNotNull(builder.addresses, "addresses"))); - this.adminPass = builder.adminPass; this.keyName = builder.keyName; - this.taskState = builder.taskState; - this.vmState = builder.vmState; - this.powerState = builder.powerState; - this.instanceName = builder.instanceName; - this.hostName = builder.hostName; - this.hypervisorName = builder.hypervisorName; - this.diskConfig = builder.diskConfig; + this.extendedStatus = Optional.fromNullable(builder.extendedStatus); + this.extendedAttributes = Optional.fromNullable(builder.extendedAttributes); + this.diskConfig = builder.diskConfig == null ? Optional.absent() : Optional.of(builder.diskConfig); } + protected Server() { + // for GSON + this.uuid = null; + this.tenantId = null; + this.userId = null; + this.updated = null; + this.created = null; + this.hostId = null; + this.accessIPv4 = null; + this.accessIPv6 = null; + this.status = null; + this.configDrive = null; + this.image = null; + this.flavor = null; + this.metadata = ImmutableMap.of(); + this.addresses = ImmutableMap.of(); + this.keyName = null; + this.extendedStatus = Optional.absent(); + this.extendedAttributes = Optional.absent(); + this.diskConfig = Optional.absent(); + } + /** * only present until the id is in uuid form * @@ -489,14 +452,6 @@ public class Server extends Resource { return Multimaps2.fromOldSchool(addresses); } - /** - * @return the administrative password for this server; only present on first request. - */ - @Nullable - public String getAdminPass() { - return adminPass; - } - /** * @return keyName if extension is present and there is a valur for this server * @see KeyPairClient @@ -508,64 +463,21 @@ public class Server extends Resource { /** - * State of task running against this instance (e.g. "suspending") + * Retrieves the extended server status fields *

* NOTE: This field is only present if the Extended Status extension is installed. */ - @Nullable - public String getTaskState() { - return this.taskState; + public Optional getExtendedStatus() { + return this.extendedStatus; } /** - * State of task running against this instance (e.g. "suspending") - *

- * NOTE: This field is only present if the Extended Status extension is installed. - */ - @Nullable - public String getVmState() { - return this.vmState; - } - - /** - * State of task running against this instance (e.g. "suspending") - *

- * NOTE: This field is only present if the Extended Status extension is installed. - */ - @Nullable - public String getPowerState() { - return this.powerState; - } - - /** - * The name of the instance? + * Retrieves the extended server attributes fields *

* NOTE: This field is only present if the The Extended Server Attributes API extension is installed. */ - @Nullable - public String getInstanceName() { - return this.instanceName; - } - - /** - * The host name of the host this Server is running on - *

- * NOTE: This field is only present if the The Extended Server Attributes API extension is installed. - * @see #getHostId() - */ - @Nullable - public String getHostName() { - return this.hostName; - } - - /** - * The name of the hypervisor this Server is running on - *

- * NOTE: This field is only present if the The Extended Server Attributes API extension is installed. - */ - @Nullable - public String getHypervisorName() { - return this.hypervisorName; + public Optional getExtendedAttributes() { + return this.extendedAttributes; } /** @@ -573,8 +485,7 @@ public class Server extends Resource { *

* NOTE: This field is only present if the Disk Config extension is installed. */ - @Nullable - public String getDiskConfig() { + public Optional getDiskConfig() { return this.diskConfig; } @@ -587,6 +498,7 @@ public class Server extends Resource { "userId", userId).add("hostId", getHostId()).add("updated", updated).add("created", created).add( "accessIPv4", getAccessIPv4()).add("accessIPv6", getAccessIPv6()).add("status", status).add( "configDrive", getConfigDrive()).add("image", image).add("flavor", flavor).add("metadata", metadata) - .add("addresses", getAddresses()).add("adminPass", adminPass); + .add("addresses", getAddresses()).add("diskConfig", diskConfig) + .add("extendedStatus", extendedStatus).add("extendedAttributes", extendedAttributes); } } diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/ServerCreated.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/ServerCreated.java new file mode 100644 index 0000000000..607b2f3ae4 --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/ServerCreated.java @@ -0,0 +1,95 @@ +/** + * 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 org.jclouds.javax.annotation.Nullable; +import org.jclouds.openstack.domain.Resource; + +import com.google.common.base.Objects.ToStringHelper; + +/** + * Server Resource with administrative password returned by ServerClient#CreateServer calls + * + * @author Adam Lowe + * @see + */ +public class ServerCreated extends Resource { + + public static Builder builder() { + return new ConcreteBuilder(); + } + + public Builder toBuilder() { + return new ConcreteBuilder().fromServerCreated(this); + } + + public static abstract class Builder> extends Resource.Builder { + private String adminPass; + + /** + * @see ServerCreated#getAdminPass() + */ + public T adminPass(String adminPass) { + this.adminPass = adminPass; + return self(); + } + + public T fromServerCreated(ServerCreated in) { + return super.fromResource(in).adminPass(in.getAdminPass()); + } + + public ServerCreated build() { + return new ServerCreated(this); + } + } + + private static class ConcreteBuilder extends Builder { + @Override + protected ConcreteBuilder self() { + return this; + } + } + + private final String adminPass; + + protected ServerCreated(Builder builder) { + super(builder); + this.adminPass = builder.adminPass; + } + + protected ServerCreated() { + this.adminPass =null; + } + + /** + * @return the administrative password for this server. Note: this is not available in Server responses. + */ + public String getAdminPass() { + return adminPass; + } + + // hashCode/equals from super is ok + + @Override + protected ToStringHelper string() { + return super.string().add("adminPass", adminPass); + } +} diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/ServerExtendedAttributes.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/ServerExtendedAttributes.java new file mode 100644 index 0000000000..400f075a04 --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/ServerExtendedAttributes.java @@ -0,0 +1,146 @@ +/** + * 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 com.google.common.base.Objects; +import com.google.common.base.Objects.ToStringHelper; +import com.google.gson.annotations.SerializedName; + +/** + * Additional attributes delivered by Extended Server Attributes extension + * + * @author Adam Lowe + * @see + */ +public class ServerExtendedAttributes { + public static final String PREFIX = "OS-EXT-SRV-ATTR:"; + + public static Builder builder() { + return new ConcreteBuilder(); + } + + public Builder toBuilder() { + return new ConcreteBuilder().fromServerExtraAttributes(this); + } + + public static abstract class Builder> { + protected abstract T self(); + + private String instanceName; + private String hostName; + private String hypervisorHostName; + + /** + * @see ServerExtendedAttributes#getInstanceName() + */ + public T instanceName(String instanceName) { + this.instanceName = instanceName; + return self(); + } + + /** + * @see ServerExtendedAttributes#getHostName() + */ + public T hostName(String hostName) { + this.hostName = hostName; + return self(); + } + + /** + * @see ServerExtendedAttributes#getHypervisorHostName() + */ + public T hypervisorHostame(String hypervisorHostName) { + this.hypervisorHostName = hypervisorHostName; + return self(); + } + + public ServerExtendedAttributes build() { + return new ServerExtendedAttributes(this); + } + + public T fromServerExtraAttributes(ServerExtendedAttributes in) { + return this + .instanceName(in.getInstanceName()) + .hostName(in.getHostName()) + .hypervisorHostame(in.getHypervisorHostName()); + } + } + + private static class ConcreteBuilder extends Builder { + @Override + protected ConcreteBuilder self() { + return this; + } + } + + @SerializedName(value=PREFIX + "instance_name") + private final String instanceName; + @SerializedName(value=PREFIX + "host") + private final String hostName; + @SerializedName(value=PREFIX + "hypervisor_hostname") + private final String hypervisorHostName; + + protected ServerExtendedAttributes(Builder builder) { + this.instanceName = builder.instanceName; + this.hostName = builder.hostName; + this.hypervisorHostName = builder.hypervisorHostName; + } + + public String getInstanceName() { + return this.instanceName; + } + + public String getHostName() { + return this.hostName; + } + + public String getHypervisorHostName() { + return this.hypervisorHostName; + } + + @Override + public int hashCode() { + return Objects.hashCode(instanceName, hostName, hypervisorHostName); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + ServerExtendedAttributes that = ServerExtendedAttributes.class.cast(obj); + return Objects.equal(this.instanceName, that.instanceName) + && Objects.equal(this.hostName, that.hostName) + && Objects.equal(this.hypervisorHostName, that.hypervisorHostName); + } + + protected ToStringHelper string() { + return Objects.toStringHelper("") + .add("instanceName", instanceName) + .add("hostName", hostName) + .add("hypervisorHostName", hypervisorHostName); + } + + @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/ServerExtendedStatus.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/ServerExtendedStatus.java new file mode 100644 index 0000000000..8bef032193 --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/ServerExtendedStatus.java @@ -0,0 +1,158 @@ +/** + * 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 org.jclouds.javax.annotation.Nullable; + +import com.google.common.base.Objects; +import com.google.common.base.Objects.ToStringHelper; +import com.google.gson.annotations.SerializedName; + +/** + * Additional attributes delivered by Extended Server Status extension + * + * @author Adam Lowe + * @see + */ +public class ServerExtendedStatus { + public static final String PREFIX = "OS-EXT-STS:"; + + public static Builder builder() { + return new ConcreteBuilder(); + } + + public Builder toBuilder() { + return new ConcreteBuilder().fromServerExtendedStatus(this); + } + + public static abstract class Builder> { + protected abstract T self(); + + private String taskState; + private String vmState; + private int powerState = Integer.MIN_VALUE; + + /** + * @see ServerExtendedStatus#getTaskState() + */ + public T taskState(String taskState) { + this.taskState = taskState; + return self(); + } + + /** + * @see ServerExtendedStatus#getVmState() + */ + public T vmState(String vmState) { + this.vmState = vmState; + return self(); + } + + /** + * @see ServerExtendedStatus#getPowerState() + */ + public T powerState(int powerState) { + this.powerState = powerState; + return self(); + } + + public ServerExtendedStatus build() { + return new ServerExtendedStatus(this); + } + + public T fromServerExtendedStatus(ServerExtendedStatus in) { + return this + .taskState(in.getTaskState()) + .vmState(in.getVmState()) + .powerState(in.getPowerState()); + } + } + + private static class ConcreteBuilder extends Builder { + @Override + protected ConcreteBuilder self() { + return this; + } + } + + @SerializedName(value=PREFIX + "task_state") + private final String taskState; + @SerializedName(value=PREFIX + "vm_state") + private final String vmState; + @SerializedName(value=PREFIX + "power_state") + private final int powerState; + + protected ServerExtendedStatus(Builder builder) { + this.taskState = builder.taskState; + this.vmState = builder.vmState; + this.powerState = builder.powerState; + } + + protected ServerExtendedStatus() { + this.taskState = null; + this.vmState = null; + this.powerState = Integer.MIN_VALUE; + } + + @Nullable + public String getTaskState() { + return this.taskState; + } + + @Nullable + public String getVmState() { + return this.vmState; + } + + public int getPowerState() { + return this.powerState; + } + + @Override + public int hashCode() { + return Objects.hashCode(taskState, vmState, powerState); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + ServerExtendedStatus that = ServerExtendedStatus.class.cast(obj); + return Objects.equal(this.taskState, that.taskState) + && Objects.equal(this.vmState, that.vmState) + && Objects.equal(this.powerState, that.powerState) + ; + } + + protected ToStringHelper string() { + return Objects.toStringHelper("") + .add("taskState", taskState) + .add("vmState", vmState) + .add("powerState", powerState) + ; + } + + @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/features/ServerAsyncClient.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/features/ServerAsyncClient.java index 2640f7439e..93dbaea18b 100644 --- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/features/ServerAsyncClient.java +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/features/ServerAsyncClient.java @@ -32,6 +32,7 @@ import javax.ws.rs.core.MediaType; import org.jclouds.openstack.domain.Resource; import org.jclouds.openstack.filters.AuthenticateRequest; +import org.jclouds.openstack.nova.v1_1.domain.ServerCreated; import org.jclouds.openstack.nova.v1_1.domain.RebootType; import org.jclouds.openstack.nova.v1_1.domain.Server; import org.jclouds.openstack.nova.v1_1.functions.ParseImageIdFromLocationHeader; @@ -154,7 +155,7 @@ public interface ServerAsyncClient { @Consumes(MediaType.APPLICATION_JSON) @Path("/servers") @MapBinder(CreateServerOptions.class) - ListenableFuture createServer(@PayloadParam("name") String name, @PayloadParam("imageRef") String imageRef, + ListenableFuture createServer(@PayloadParam("name") String name, @PayloadParam("imageRef") String imageRef, @PayloadParam("flavorRef") String flavorRef, CreateServerOptions... options); /** diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/features/ServerClient.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/features/ServerClient.java index 51003d3340..064d05eed2 100644 --- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/features/ServerClient.java +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/features/ServerClient.java @@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit; import org.jclouds.concurrent.Timeout; import org.jclouds.openstack.domain.Resource; +import org.jclouds.openstack.nova.v1_1.domain.ServerCreated; import org.jclouds.openstack.nova.v1_1.domain.RebootType; import org.jclouds.openstack.nova.v1_1.domain.Server; import org.jclouds.openstack.nova.v1_1.options.CreateServerOptions; @@ -80,7 +81,7 @@ public interface ServerClient { */ // blocking call @Timeout(duration = 10, timeUnit = TimeUnit.MINUTES) - Server createServer(String name, String imageRef, String flavorRef, CreateServerOptions... options); + ServerCreated createServer(String name, String imageRef, String flavorRef, CreateServerOptions... options); /** * Terminate and delete a server. diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/compute/functions/OrphanedGroupsByZoneIdTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/compute/functions/OrphanedGroupsByZoneIdTest.java index 843407cbc3..4e9bd759d3 100644 --- a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/compute/functions/OrphanedGroupsByZoneIdTest.java +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/compute/functions/OrphanedGroupsByZoneIdTest.java @@ -64,7 +64,7 @@ public class OrphanedGroupsByZoneIdTest { @Test public void testWhenComputeServiceSaysAllNodesAreDeadBothGroupsAreReturned() { - ServerInZone withoutHost = new ServerInZone(new ParseCreatedServerTest().expected(), "az-1.region-a.geo-1"); + ServerInZone withoutHost = new ServerInZone(new ServerInZoneToNodeMetadataTest().expectedServer(), "az-1.region-a.geo-1"); ServerInZone withHost = new ServerInZone(new ParseServerTest().expected(), "az-1.region-a.geo-1"); ServerInZoneToNodeMetadata converter = new ServerInZoneToNodeMetadata(locationIndex, Suppliers @@ -80,7 +80,7 @@ public class OrphanedGroupsByZoneIdTest { @Test public void testWhenComputeServiceSaysAllNodesAreDeadNoGroupsAreReturned() { - ServerInZone withoutHost = new ServerInZone(new ParseCreatedServerTest().expected(), "az-1.region-a.geo-1"); + ServerInZone withoutHost = new ServerInZone(new ServerInZoneToNodeMetadataTest().expectedServer(), "az-1.region-a.geo-1"); ServerInZone withHost = new ServerInZone(new ParseServerTest().expected(), "az-1.region-a.geo-1"); ServerInZoneToNodeMetadata converter = new ServerInZoneToNodeMetadata(locationIndex, Suppliers diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/compute/functions/ServerInZoneToNodeMetadataTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/compute/functions/ServerInZoneToNodeMetadataTest.java index 08178fa869..c9bd2c0d6c 100644 --- a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/compute/functions/ServerInZoneToNodeMetadataTest.java +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/compute/functions/ServerInZoneToNodeMetadataTest.java @@ -21,6 +21,7 @@ package org.jclouds.openstack.nova.v1_1.compute.functions; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; +import java.net.URI; import java.util.Map; import java.util.Set; @@ -32,9 +33,12 @@ import org.jclouds.compute.domain.NodeMetadata; import org.jclouds.compute.domain.OperatingSystem; import org.jclouds.compute.domain.OsFamily; import org.jclouds.compute.functions.GroupNamingConvention; +import org.jclouds.date.internal.SimpleDateFormatDateService; import org.jclouds.domain.Location; import org.jclouds.domain.LocationBuilder; import org.jclouds.domain.LocationScope; +import org.jclouds.openstack.domain.Link; +import org.jclouds.openstack.domain.Resource; import org.jclouds.openstack.nova.v1_1.domain.Server; import org.jclouds.openstack.nova.v1_1.domain.zonescoped.ServerInZone; import org.jclouds.openstack.nova.v1_1.parse.ParseCreatedServerTest; @@ -144,7 +148,7 @@ public class ServerInZoneToNodeMetadataTest { Set images = ImmutableSet. of(); Set hardwares = ImmutableSet. of(); - Server serverToConvert = new ParseCreatedServerTest().expected(); + Server serverToConvert = expectedServer(); ServerInZone serverInZoneToConvert = new ServerInZone(serverToConvert, "az-1.region-a.geo-1"); @@ -160,4 +164,39 @@ public class ServerInZoneToNodeMetadataTest { assertEquals(convertedNodeMetadata.getLocation(), zone); } + + public Server expectedServer() { + return Server + .builder() + .id("71752") + .uuid("47491020-6a78-4f63-9475-23195ac4515c") + .tenantId("37936628937291") + .userId("54297837463082") + .name("test-e92") + .updated(new SimpleDateFormatDateService().iso8601SecondsDateParse("2012-03-19T06:21:13Z")) + .created(new SimpleDateFormatDateService().iso8601SecondsDateParse("2012-03-19T06:21:13Z")) + .status(Server.Status.BUILD) + .image( + Resource + .builder() + .id("1241") + .links( + Link.create( + Link.Relation.BOOKMARK, + URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/37936628937291/images/1241"))) + .build()) + .flavor( + Resource + .builder() + .id("100") + .links( + Link.create( + Link.Relation.BOOKMARK, + URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/37936628937291/flavors/100"))) + .build()) + .links( + Link.create(Link.Relation.SELF, URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/37936628937291/servers/71752")), + Link.create(Link.Relation.BOOKMARK, URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/37936628937291/servers/71752"))).build(); + + } } diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/AdminActionsClientLiveTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/AdminActionsClientLiveTest.java index 547675421e..048cc235bc 100644 --- a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/AdminActionsClientLiveTest.java +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/AdminActionsClientLiveTest.java @@ -26,7 +26,7 @@ import static org.testng.Assert.fail; import org.jclouds.http.HttpResponseException; import org.jclouds.openstack.nova.v1_1.domain.BackupType; import org.jclouds.openstack.nova.v1_1.domain.Image; -import org.jclouds.openstack.nova.v1_1.domain.Server; +import org.jclouds.openstack.nova.v1_1.domain.Server.Status; import org.jclouds.openstack.nova.v1_1.features.ExtensionClient; import org.jclouds.openstack.nova.v1_1.features.ImageClient; import org.jclouds.openstack.nova.v1_1.features.ServerClient; @@ -89,7 +89,7 @@ public class AdminActionsClientLiveTest extends BaseNovaClientLiveTest { @AfterMethod(alwaysRun = true) public void ensureServerIsActiveAgain() { - blockUntilServerInState(testServerId, serverClient, Server.Status.ACTIVE); + blockUntilServerInState(testServerId, serverClient, Status.ACTIVE); } public void testSuspendAndResume() { @@ -103,14 +103,14 @@ public class AdminActionsClientLiveTest extends BaseNovaClientLiveTest { } catch (HttpResponseException e) { } assertTrue(client.suspendServer(testServerId)); - blockUntilServerInState(testServerId, serverClient, Server.Status.SUSPENDED); + blockUntilServerInState(testServerId, serverClient, Status.SUSPENDED); try { client.suspendServer(testServerId); fail("Suspended an already suspended server!"); } catch (HttpResponseException e) { } assertTrue(client.resumeServer(testServerId)); - blockUntilServerInState(testServerId, serverClient, Server.Status.ACTIVE); + blockUntilServerInState(testServerId, serverClient, Status.ACTIVE); try { client.resumeServer(testServerId); fail("Resumed an already resumed server!"); @@ -153,14 +153,14 @@ public class AdminActionsClientLiveTest extends BaseNovaClientLiveTest { } catch (HttpResponseException e) { } assertTrue(client.pauseServer(testServerId)); - blockUntilServerInState(testServerId, serverClient, Server.Status.PAUSED); + blockUntilServerInState(testServerId, serverClient, Status.PAUSED); try { client.pauseServer(testServerId); fail("paused a paused server!"); } catch (HttpResponseException e) { } assertTrue(client.unpauseServer(testServerId)); - blockUntilServerInState(testServerId, serverClient, Server.Status.ACTIVE); + blockUntilServerInState(testServerId, serverClient, Status.ACTIVE); try { client.unpauseServer(testServerId); fail("Unpaused a server we just unpaused!"); @@ -182,7 +182,7 @@ public class AdminActionsClientLiveTest extends BaseNovaClientLiveTest { Thread.sleep(30000); } - blockUntilServerInState(testServerId, serverClient, Server.Status.ACTIVE); + blockUntilServerInState(testServerId, serverClient, Status.ACTIVE); Image backupImage = imageClient.getImage(backupImageId); assertEquals(backupImage.getId(), backupImageId); diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/internal/BaseNovaClientLiveTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/internal/BaseNovaClientLiveTest.java index e17572fbe6..fe683e4ea5 100644 --- a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/internal/BaseNovaClientLiveTest.java +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/internal/BaseNovaClientLiveTest.java @@ -26,6 +26,7 @@ import org.jclouds.openstack.nova.v1_1.NovaAsyncClient; import org.jclouds.openstack.nova.v1_1.NovaClient; import org.jclouds.openstack.nova.v1_1.config.NovaProperties; import org.jclouds.openstack.nova.v1_1.domain.Flavor; +import org.jclouds.openstack.nova.v1_1.domain.ServerCreated; import org.jclouds.openstack.nova.v1_1.domain.Server; import org.jclouds.openstack.nova.v1_1.domain.Server.Status; import org.jclouds.openstack.nova.v1_1.features.FlavorClient; @@ -78,9 +79,9 @@ public class BaseNovaClientLiveTest extends BaseComputeServiceContextLiveTest { protected Server createServerInZone(String zoneId) { ServerClient serverClient = novaContext.getApi().getServerClientForZone(zoneId); - Server server = serverClient.createServer("test", imageIdForZone(zoneId), flavorRefForZone(zoneId)); + ServerCreated server = serverClient.createServer("test", imageIdForZone(zoneId), flavorRefForZone(zoneId)); blockUntilServerInState(server.getId(), serverClient, Status.ACTIVE); - return server; + return serverClient.getServer(server.getId()); } /** @@ -89,7 +90,9 @@ public class BaseNovaClientLiveTest extends BaseComputeServiceContextLiveTest { */ protected void blockUntilServerInState(String serverId, ServerClient client, Status status) { Server currentDetails = null; - for (currentDetails = client.getServer(serverId); currentDetails.getStatus() != status || currentDetails.getTaskState() != null; currentDetails = client + for (currentDetails = client.getServer(serverId); currentDetails.getStatus() != status || + (currentDetails.getExtendedStatus().isPresent() && currentDetails.getExtendedStatus().get().getTaskState() != null); + currentDetails = client .getServer(serverId)) { System.out.printf("blocking on status %s%n%s%n", status, currentDetails); try { diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/parse/ParseCreatedServerTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/parse/ParseCreatedServerTest.java index b322aad8e5..944c8fd2d8 100644 --- a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/parse/ParseCreatedServerTest.java +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/parse/ParseCreatedServerTest.java @@ -27,11 +27,12 @@ import org.jclouds.date.internal.SimpleDateFormatDateService; import org.jclouds.json.BaseItemParserTest; import org.jclouds.json.config.GsonModule; import org.jclouds.openstack.domain.Link; -import org.jclouds.openstack.domain.Resource; import org.jclouds.openstack.domain.Link.Relation; +import org.jclouds.openstack.domain.Resource; import org.jclouds.openstack.nova.v1_1.config.NovaParserModule; import org.jclouds.openstack.nova.v1_1.domain.Server; import org.jclouds.openstack.nova.v1_1.domain.Server.Status; +import org.jclouds.openstack.nova.v1_1.domain.ServerCreated; import org.jclouds.rest.annotations.SelectJson; import org.testng.annotations.Test; @@ -42,7 +43,7 @@ import com.google.inject.Injector; * @author Adrian Cole */ @Test(groups = "unit", testName = "ParseCreatedServerTest") -public class ParseCreatedServerTest extends BaseItemParserTest { +public class ParseCreatedServerTest extends BaseItemParserTest { @Override public String resource() { @@ -52,36 +53,12 @@ public class ParseCreatedServerTest extends BaseItemParserTest { @Override @SelectJson("server") @Consumes(MediaType.APPLICATION_JSON) - public Server expected() { - return Server + public ServerCreated expected() { + return ServerCreated .builder() .id("71752") - .uuid("47491020-6a78-4f63-9475-23195ac4515c") - .tenantId("37936628937291") - .userId("54297837463082") .name("test-e92") - .updated(new SimpleDateFormatDateService().iso8601SecondsDateParse("2012-03-19T06:21:13Z")) - .created(new SimpleDateFormatDateService().iso8601SecondsDateParse("2012-03-19T06:21:13Z")) - .status(Status.BUILD) .adminPass("ZWuHcmTMQ7eXoHeM") - .image( - Resource - .builder() - .id("1241") - .links( - Link.create( - Relation.BOOKMARK, - URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/37936628937291/images/1241"))) - .build()) - .flavor( - Resource - .builder() - .id("100") - .links( - Link.create( - Relation.BOOKMARK, - URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/37936628937291/flavors/100"))) - .build()) .links( Link.create(Relation.SELF, URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/37936628937291/servers/71752")), Link.create(Relation.BOOKMARK, URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/37936628937291/servers/71752"))).build(); diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/parse/ParseServerDetailsEssexTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/parse/ParseServerDetailsEssexTest.java index 98f239593a..95f8a2ca7f 100644 --- a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/parse/ParseServerDetailsEssexTest.java +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/parse/ParseServerDetailsEssexTest.java @@ -33,6 +33,8 @@ import org.jclouds.openstack.domain.Link.Relation; import org.jclouds.openstack.nova.v1_1.config.NovaParserModule; import org.jclouds.openstack.nova.v1_1.domain.Address; import org.jclouds.openstack.nova.v1_1.domain.Server; +import org.jclouds.openstack.nova.v1_1.domain.Server.Status; +import org.jclouds.openstack.nova.v1_1.domain.ServerExtendedStatus; import org.jclouds.rest.annotations.SelectJson; import org.testng.annotations.Test; @@ -59,102 +61,108 @@ public class ParseServerDetailsEssexTest extends BaseSetParserTest { return ImmutableSet.of( Server.builder() .addresses(ImmutableMultimap.builder() - .putAll("Net TenantA Front-Middle", Address.createV4("172.16.11.5")) - .putAll("Public network", Address.createV4("172.16.1.13"), Address.createV4("10.193.112.119")).build()) + .putAll("Net TenantA Front-Middle", Address.createV4("172.16.11.5")) + .putAll("Public network", Address.createV4("172.16.1.13"), Address.createV4("10.193.112.119")).build()) .links( - Link.create( - Relation.SELF, - URI.create("http://nova:8774/v1.1/8d10e6646d5d4585937395b04839a353/servers/0c80b392-db30-4736-ae02-4480090f1207")), - Link.create( - Relation.BOOKMARK, - URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/servers/0c80b392-db30-4736-ae02-4480090f1207"))) + Link.create( + Relation.SELF, + URI.create("http://nova:8774/v1.1/8d10e6646d5d4585937395b04839a353/servers/0c80b392-db30-4736-ae02-4480090f1207")), + Link.create( + Relation.BOOKMARK, + URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/servers/0c80b392-db30-4736-ae02-4480090f1207"))) .image( - Resource.builder() - .id("416af940-2d3c-4a7c-977c-a9030685ad5e") - .links( - Link.create( - Relation.BOOKMARK, - URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/images/416af940-2d3c-4a7c-977c-a9030685ad5e"))).build()) + Resource.builder() + .id("416af940-2d3c-4a7c-977c-a9030685ad5e") + .links( + Link.create( + Relation.BOOKMARK, + URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/images/416af940-2d3c-4a7c-977c-a9030685ad5e"))).build()) .flavor( - Resource.builder() - .id("1") - .links( - Link.create( - Relation.BOOKMARK, - URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/flavors/1"))).build()) + Resource.builder() + .id("1") + .links( + Link.create( + Relation.BOOKMARK, + URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/flavors/1"))).build()) .id("0c80b392-db30-4736-ae02-4480090f1207") .userId("df13814f6c354d00a8acf66502836323") - .status(Server.Status.ACTIVE) + .status(Status.ACTIVE) .updated(new SimpleDateFormatDateService().iso8601SecondsDateParse("2012-04-12T11:21:33Z")) .hostId("03d796ebb52b1b555e5f6d9262f7dbd52b3f7c181e3aa89b34ca5408") .name("VM proxy") .created(new SimpleDateFormatDateService().iso8601SecondsDateParse("2012-04-12T11:21:23Z")) - .tenantId("8d10e6646d5d4585937395b04839a353").build(), + .tenantId("8d10e6646d5d4585937395b04839a353") + .extendedStatus(ServerExtendedStatus.builder().vmState("active").powerState(1).build()) + .diskConfig("MANUAL").build(), Server.builder() .addresses(ImmutableMultimap.builder() - .putAll("Net TenantA Front-Middle", Address.createV4("172.16.11.4")) - .putAll("Net TenantA Middle-Back", Address.createV4("172.16.12.5")).build()) + .putAll("Net TenantA Front-Middle", Address.createV4("172.16.11.4")) + .putAll("Net TenantA Middle-Back", Address.createV4("172.16.12.5")).build()) .links( - Link.create( - Relation.SELF, - URI.create("http://nova:8774/v1.1/8d10e6646d5d4585937395b04839a353/servers/b332b5cd-535e-4677-b68e-fc8badc13236")), - Link.create( - Relation.BOOKMARK, - URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/servers/b332b5cd-535e-4677-b68e-fc8badc13236"))) + Link.create( + Relation.SELF, + URI.create("http://nova:8774/v1.1/8d10e6646d5d4585937395b04839a353/servers/b332b5cd-535e-4677-b68e-fc8badc13236")), + Link.create( + Relation.BOOKMARK, + URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/servers/b332b5cd-535e-4677-b68e-fc8badc13236"))) .image( - Resource.builder() - .id("416af940-2d3c-4a7c-977c-a9030685ad5e") - .links( - Link.create( - Relation.BOOKMARK, - URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/images/416af940-2d3c-4a7c-977c-a9030685ad5e"))).build()) + Resource.builder() + .id("416af940-2d3c-4a7c-977c-a9030685ad5e") + .links( + Link.create( + Relation.BOOKMARK, + URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/images/416af940-2d3c-4a7c-977c-a9030685ad5e"))).build()) .flavor( - Resource.builder() - .id("1") - .links( - Link.create( - Relation.BOOKMARK, - URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/flavors/1"))).build()) + Resource.builder() + .id("1") + .links( + Link.create( + Relation.BOOKMARK, + URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/flavors/1"))).build()) .id("b332b5cd-535e-4677-b68e-fc8badc13236") .userId("df13814f6c354d00a8acf66502836323") - .status(Server.Status.ACTIVE) + .status(Status.ACTIVE) .updated(new SimpleDateFormatDateService().iso8601SecondsDateParse("2012-04-12T11:18:58Z")) .hostId("e5bbff80bebacfe1db63951e787b5341427060a602d33abfefb6a1bc") .name("VM blog") .created(new SimpleDateFormatDateService().iso8601SecondsDateParse("2012-04-12T11:18:48Z")) - .tenantId("8d10e6646d5d4585937395b04839a353").build(), - Server.builder() + .tenantId("8d10e6646d5d4585937395b04839a353") + .extendedStatus(ServerExtendedStatus.builder().vmState("active").powerState(1).build()) + .diskConfig("MANUAL").build(), + Server.builder() .addresses(ImmutableMultimap.builder() - .putAll("Net TenantA Middle-Back", Address.createV4("172.16.12.4")).build()) + .putAll("Net TenantA Middle-Back", Address.createV4("172.16.12.4")).build()) .links( - Link.create( - Relation.SELF, - URI.create("http://nova:8774/v1.1/8d10e6646d5d4585937395b04839a353/servers/f9d43436-4572-4c9b-9b74-5fa6890a2f21")), - Link.create( - Relation.BOOKMARK, - URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/servers/f9d43436-4572-4c9b-9b74-5fa6890a2f21"))) + Link.create( + Relation.SELF, + URI.create("http://nova:8774/v1.1/8d10e6646d5d4585937395b04839a353/servers/f9d43436-4572-4c9b-9b74-5fa6890a2f21")), + Link.create( + Relation.BOOKMARK, + URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/servers/f9d43436-4572-4c9b-9b74-5fa6890a2f21"))) .image( - Resource.builder() - .id("416af940-2d3c-4a7c-977c-a9030685ad5e") - .links( - Link.create( - Relation.BOOKMARK, - URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/images/416af940-2d3c-4a7c-977c-a9030685ad5e"))).build()) + Resource.builder() + .id("416af940-2d3c-4a7c-977c-a9030685ad5e") + .links( + Link.create( + Relation.BOOKMARK, + URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/images/416af940-2d3c-4a7c-977c-a9030685ad5e"))).build()) .flavor( - Resource.builder() - .id("1") - .links( - Link.create( - Relation.BOOKMARK, - URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/flavors/1"))).build()) + Resource.builder() + .id("1") + .links( + Link.create( + Relation.BOOKMARK, + URI.create("http://nova:8774/8d10e6646d5d4585937395b04839a353/flavors/1"))).build()) .id("f9d43436-4572-4c9b-9b74-5fa6890a2f21") .userId("df13814f6c354d00a8acf66502836323") - .status(Server.Status.ACTIVE) + .status(Status.ACTIVE) .updated(new SimpleDateFormatDateService().iso8601SecondsDateParse("2012-04-12T11:15:09Z")) .hostId("03d796ebb52b1b555e5f6d9262f7dbd52b3f7c181e3aa89b34ca5408") .name("VM MySQL") .created(new SimpleDateFormatDateService().iso8601SecondsDateParse("2012-04-12T11:14:56Z")) - .tenantId("8d10e6646d5d4585937395b04839a353").build()); + .tenantId("8d10e6646d5d4585937395b04839a353") + .extendedStatus(ServerExtendedStatus.builder().vmState("active").powerState(1).build()) + .diskConfig("MANUAL").build()); } diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/parse/ParseServerWithAllExtensionsTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/parse/ParseServerWithAllExtensionsTest.java new file mode 100644 index 0000000000..52139fabac --- /dev/null +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/parse/ParseServerWithAllExtensionsTest.java @@ -0,0 +1,106 @@ +/** + * 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.parse; + +import java.net.URI; + +import javax.ws.rs.Consumes; +import javax.ws.rs.core.MediaType; + +import org.jclouds.date.internal.SimpleDateFormatDateService; +import org.jclouds.json.BaseItemParserTest; +import org.jclouds.json.config.GsonModule; +import org.jclouds.openstack.domain.Link; +import org.jclouds.openstack.domain.Link.Relation; +import org.jclouds.openstack.domain.Resource; +import org.jclouds.openstack.nova.v1_1.config.NovaParserModule; +import org.jclouds.openstack.nova.v1_1.domain.Address; +import org.jclouds.openstack.nova.v1_1.domain.Server; +import org.jclouds.openstack.nova.v1_1.domain.Server.Status; +import org.jclouds.openstack.nova.v1_1.domain.ServerExtendedAttributes; +import org.jclouds.openstack.nova.v1_1.domain.ServerExtendedStatus; +import org.jclouds.rest.annotations.SelectJson; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableMultimap; +import com.google.inject.Guice; +import com.google.inject.Injector; + +/** + * @author Adam Lowe + */ +@Test(groups = "unit", testName = "ParseServerWithAllExtensionsTest") +public class ParseServerWithAllExtensionsTest extends BaseItemParserTest { + + @Override + public String resource() { + return "/server_details_devstack.json"; + } + + @Override + @SelectJson("server") + @Consumes(MediaType.APPLICATION_JSON) + public Server expected() { + return Server + .builder() + .id("141b775f-7ac1-45f0-9a95-146260f33a53") + .tenantId("7f312675f9b84c97bff8f5054e181419") + .userId("89c01b67395d4bea945f7f5bfd7f344a") + .name("test") + .updated(new SimpleDateFormatDateService().iso8601SecondsDateParse("2012-05-04T15:07:48Z")) + .created(new SimpleDateFormatDateService().iso8601SecondsDateParse("2012-05-04T15:07:36Z")) + .hostId("eab9a77d1c44b8833e4a3dc6d2d9d50de556e780a319f184d8c82d9b") + .status(Status.PAUSED) + .image( + Resource + .builder() + .id("8e6f5bc4-a210-45b2-841f-c510eae14300") + .links( + Link.create( + Relation.BOOKMARK, + URI.create("http://172.16.89.149:8774/7f312675f9b84c97bff8f5054e181419/images/8e6f5bc4-a210-45b2-841f-c510eae14300"))) + .build()) + .flavor( + Resource + .builder() + .id("1") + .links( + Link.create( + Relation.BOOKMARK, + URI.create("http://172.16.89.149:8774/7f312675f9b84c97bff8f5054e181419/flavors/1"))) + .build()) + .links( + Link.create( + Relation.SELF, + URI.create("http://172.16.89.149:8774/v2/7f312675f9b84c97bff8f5054e181419/servers/141b775f-7ac1-45f0-9a95-146260f33a53")), + Link.create( + Relation.BOOKMARK, + URI.create("http://172.16.89.149:8774/7f312675f9b84c97bff8f5054e181419/servers/141b775f-7ac1-45f0-9a95-146260f33a53"))) + .addresses(ImmutableMultimap.of("private", Address.createV4("10.0.0.8"))) + .diskConfig("MANUAL") + .extendedStatus(ServerExtendedStatus.builder().vmState("paused").powerState(3).build()) + .extraAttributes(ServerExtendedAttributes.builder().instanceName("instance-00000014").hostName("ubuntu").build()) + .build(); + } + + + protected Injector injector() { + return Guice.createInjector(new NovaParserModule(), new GsonModule()); + } +} diff --git a/apis/openstack-nova/src/test/resources/server_details_devstack.json b/apis/openstack-nova/src/test/resources/server_details_devstack.json new file mode 100644 index 0000000000..e41095f7af --- /dev/null +++ b/apis/openstack-nova/src/test/resources/server_details_devstack.json @@ -0,0 +1,44 @@ +{"server": { + "OS-EXT-STS:task_state": null, + "addresses": { + "private": [{"version": 4, "addr": "10.0.0.8"}] + }, + "links": + [ + { + "href": "http://172.16.89.149:8774/v2/7f312675f9b84c97bff8f5054e181419/servers/141b775f-7ac1-45f0-9a95-146260f33a53", "rel": "self" + }, + { + "href": "http://172.16.89.149:8774/7f312675f9b84c97bff8f5054e181419/servers/141b775f-7ac1-45f0-9a95-146260f33a53", "rel": "bookmark" + } + ], + "image": { + "id": "8e6f5bc4-a210-45b2-841f-c510eae14300", "links": [ + { + "href": "http://172.16.89.149:8774/7f312675f9b84c97bff8f5054e181419/images/8e6f5bc4-a210-45b2-841f-c510eae14300", "rel": "bookmark" + }] + }, + "OS-EXT-STS:vm_state": "paused", + "OS-EXT-SRV-ATTR:instance_name": "instance-00000014", + "flavor": { + "id": "1", + "links": [{"href": "http://172.16.89.149:8774/7f312675f9b84c97bff8f5054e181419/flavors/1", "rel": "bookmark"}] + }, + "id": "141b775f-7ac1-45f0-9a95-146260f33a53", + "user_id": "89c01b67395d4bea945f7f5bfd7f344a", + "OS-DCF:diskConfig": "MANUAL", + "accessIPv4": "", + "accessIPv6": "", + "OS-EXT-STS:power_state": 3, + "config_drive": "", + "status": "PAUSED", + "updated": "2012-05-04T15:07:48Z", + "hostId": "eab9a77d1c44b8833e4a3dc6d2d9d50de556e780a319f184d8c82d9b", + "OS-EXT-SRV-ATTR:host": "ubuntu", + "key_name": "", + "OS-EXT-SRV-ATTR:hypervisor_hostname": null, + "name": "test", + "created": "2012-05-04T15:07:36Z", + "tenant_id": "7f312675f9b84c97bff8f5054e181419", + "metadata": {} +}} \ No newline at end of file diff --git a/common/openstack/src/main/java/org/jclouds/openstack/domain/Resource.java b/common/openstack/src/main/java/org/jclouds/openstack/domain/Resource.java index 1deb011da9..a05e4b55e5 100644 --- a/common/openstack/src/main/java/org/jclouds/openstack/domain/Resource.java +++ b/common/openstack/src/main/java/org/jclouds/openstack/domain/Resource.java @@ -116,6 +116,12 @@ public class Resource implements Comparable { this.links = ImmutableSet.copyOf(checkNotNull(builder.links, "links")); } + protected Resource() { + this.id = null; + this.name = null; + this.links = ImmutableSet.of(); + } + /** * When providing an ID, it is assumed that the resource exists in the current OpenStack * deployment diff --git a/core/src/main/java/org/jclouds/json/config/GsonModule.java b/core/src/main/java/org/jclouds/json/config/GsonModule.java index 7e0712cacb..bc6585f0aa 100644 --- a/core/src/main/java/org/jclouds/json/config/GsonModule.java +++ b/core/src/main/java/org/jclouds/json/config/GsonModule.java @@ -37,6 +37,7 @@ import org.jclouds.json.Json; import org.jclouds.json.internal.EnumTypeAdapterThatReturnsFromValue; import org.jclouds.json.internal.GsonWrapper; import org.jclouds.json.internal.NullHackJsonLiteralAdapter; +import org.jclouds.json.internal.OptionalTypeAdapterFactory; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap.Builder; @@ -75,6 +76,7 @@ public class GsonModule extends AbstractModule { }.getType(), byteListAdapter.nullSafe()); builder.registerTypeAdapter(byte[].class, byteArrayAdapter.nullSafe()); builder.registerTypeAdapter(JsonBall.class, jsonAdapter.nullSafe()); + builder.registerTypeAdapterFactory(new OptionalTypeAdapterFactory()); // complicated (serializers/deserializers as they need context to operate) builder.registerTypeHierarchyAdapter(Enum.class, new EnumTypeAdapterThatReturnsFromValue()); diff --git a/core/src/main/java/org/jclouds/json/internal/OptionalTypeAdapterFactory.java b/core/src/main/java/org/jclouds/json/internal/OptionalTypeAdapterFactory.java new file mode 100644 index 0000000000..1855e8e7db --- /dev/null +++ b/core/src/main/java/org/jclouds/json/internal/OptionalTypeAdapterFactory.java @@ -0,0 +1,79 @@ +/** + * 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.json.internal; + +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import com.google.common.base.Optional; +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * Writes and reads Optional values as JSON + * + * @author Adam Lowe + */ +public class OptionalTypeAdapterFactory implements TypeAdapterFactory { + + @SuppressWarnings("unchecked") + @Override + public TypeAdapter create(Gson gson, TypeToken typeToken) { + Type type = typeToken.getType(); + if (typeToken.getRawType() != Optional.class || !(type instanceof ParameterizedType)) { + return null; + } + + Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0]; + TypeAdapter elementAdapter = gson.getAdapter(TypeToken.get(elementType)); + return (TypeAdapter) newMultisetAdapter(elementAdapter); + } + + private TypeAdapter> newMultisetAdapter( + final TypeAdapter elementAdapter) { + return new TypeAdapter>() { + public void write(JsonWriter out, Optional value) throws IOException { + if (!value.isPresent()) { + out.nullValue(); + return; + } + elementAdapter.write(out, value.get()); + } + + public Optional read(JsonReader in) throws IOException { + Optional result = Optional.absent(); + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + } else { + E element = elementAdapter.read(in); + if (element != null) { + result = Optional.of(element); + } + } + return result; + } + }; + } +} diff --git a/core/src/test/java/org/jclouds/json/internal/OptionalTypeAdapterFactoryTest.java b/core/src/test/java/org/jclouds/json/internal/OptionalTypeAdapterFactoryTest.java new file mode 100644 index 0000000000..6ccdd89816 --- /dev/null +++ b/core/src/test/java/org/jclouds/json/internal/OptionalTypeAdapterFactoryTest.java @@ -0,0 +1,130 @@ +/** + * 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.json.internal; + +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 org.testng.annotations.Test; + +import com.google.common.base.Objects; +import com.google.common.base.Optional; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * Tests to verify and illustrate the behavior of the OptionalTypeAdaptorFactory. + * + * @author Adam Lowe + */ +@Test(testName = "OptionalTypeAdapterFactoryTest") +public class OptionalTypeAdapterFactoryTest { + + /** + * Simple type with an Optional field + */ + static class SimpleBean { + private Optional value; + private final String someOtherValue; + + public SimpleBean(Optional value, String someOtherValue) { + this.value = value; + this.someOtherValue = someOtherValue; + } + + // Required to ensure GSON doesn't initialize our Optional to null! + private SimpleBean() { + this.value = Optional.absent(); + this.someOtherValue = null; + } + + public Optional getValue() { + return value; + } + + public String getSomeOtherValue() { + return someOtherValue; + } + + @Override + public int hashCode() { + return Objects.hashCode(value, someOtherValue); + } + + @Override + public boolean equals(Object that) { + if (that == null || that.getClass() != getClass()) + return false; + SimpleBean other = (SimpleBean) that; + return Objects.equal(value, other.value) && Objects.equal(someOtherValue, other.someOtherValue); + } + + @Override + public String toString() { + return Objects.toStringHelper("SimpleBean").add("value", value).add("someOtherValue", someOtherValue).toString(); + } + } + + // register the type adapter so that gson can serialize/deserialize to it + private Gson gsonAdapter = new GsonBuilder().registerTypeAdapterFactory(new OptionalTypeAdapterFactory()).create(); + + public void testValuePresent() { + String json = "{\"value\":\"a test string!\"}"; + SimpleBean expected = new SimpleBean(Optional.of("a test string!"), null); + + SimpleBean actual = gsonAdapter.fromJson(json, SimpleBean.class); + + assertTrue(actual.value.isPresent()); + assertEquals(actual.getValue().get(), "a test string!"); + assertNull(actual.getSomeOtherValue()); + + assertEquals(actual, expected); + assertEquals(gsonAdapter.toJson(actual), json); + } + + public void testValueAbsent() { + String json = "{\"someOtherValue\":\"testvalue\"}"; + SimpleBean expected = new SimpleBean(Optional.absent(), "testvalue"); + + SimpleBean actual = gsonAdapter.fromJson(json, SimpleBean.class); + + assertFalse(actual.value.isPresent()); + assertEquals(actual.getSomeOtherValue(), "testvalue"); + + assertEquals(actual, expected); + assertEquals(gsonAdapter.toJson(actual), json); + } + + public void testValueNull() { + String json = "{\"value\":null}"; + SimpleBean expected = new SimpleBean(Optional.absent(), null); + + SimpleBean actual = gsonAdapter.fromJson(json, SimpleBean.class); + + assertFalse(actual.value.isPresent()); + assertNull(actual.getSomeOtherValue()); + + assertEquals(actual, expected); + // Note: the field is omitted from serialized form! + assertEquals(gsonAdapter.toJson(actual), "{}"); + } + +}