From f1a0370bd0564084ce795a3ce8ea9b94d523713d Mon Sep 17 00:00:00 2001 From: Everett Toews Date: Fri, 25 Apr 2014 17:01:58 -0500 Subject: [PATCH] ServerPredicates to make waiting easier. --- apis/openstack-nova/pom.xml | 12 + .../v2_0/predicates/ServerPredicates.java | 118 +++++++++ .../nova/v2_0/features/ServerApiLiveTest.java | 45 ++-- .../v2_0/internal/BaseNovaApiLiveTest.java | 22 +- .../predicates/ServerPredicatesMockTest.java | 101 ++++++++ .../src/test/resources/access.json | 228 ++++++++++++++++++ 6 files changed, 498 insertions(+), 28 deletions(-) create mode 100644 apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/predicates/ServerPredicates.java create mode 100644 apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/predicates/ServerPredicatesMockTest.java create mode 100644 apis/openstack-nova/src/test/resources/access.json diff --git a/apis/openstack-nova/pom.xml b/apis/openstack-nova/pom.xml index 66cfbe2efd..023311d331 100644 --- a/apis/openstack-nova/pom.xml +++ b/apis/openstack-nova/pom.xml @@ -101,6 +101,18 @@ logback-classic test + + com.squareup.okhttp + mockwebserver + test + + + + org.bouncycastle + bcprov-jdk15on + + + diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/predicates/ServerPredicates.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/predicates/ServerPredicates.java new file mode 100644 index 0000000000..47812eb136 --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/predicates/ServerPredicates.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jclouds.openstack.nova.v2_0.predicates; + +import com.google.common.base.Predicate; +import org.jclouds.openstack.nova.v2_0.domain.Server; +import org.jclouds.openstack.nova.v2_0.domain.ServerCreated; +import org.jclouds.openstack.nova.v2_0.features.ServerApi; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.jclouds.openstack.nova.v2_0.domain.Server.Status; +import static org.jclouds.openstack.nova.v2_0.domain.Server.Status.ACTIVE; +import static org.jclouds.openstack.nova.v2_0.domain.Server.Status.SHUTOFF; +import static org.jclouds.util.Predicates2.retry; + +/** + * This class tests to see if a Server or ServerCreated has reached a desired status. This class is most useful when + * paired with a RetryablePredicate as in the code below. Together these classes can be used to block execution until + * the Server or ServerCreated has reached that desired status. This is useful when your Server needs to be 100% ready + * before you can continue with execution. + *

+ * For example, you can use the factory methods like so. + *

+ *

+ * {@code
+ * ServerCreated serverCreated = serverApi.create("my-server", image.getId(), flavor.getId());
+ *
+ * if (!ServerPredicates.awaitActive(serverApi).apply(serverCreated.getId())) {
+ *     throw new TimeoutException("Timeout on server: " + serverCreated);
+ * }
+ * 
+ * + *
+ * {@code
+ * if (!ServerPredicates.awaitStatus(serverApi, ACTIVE, 300, 2).apply(server.getId())) {
+ *   throw new TimeoutException("Timeout on server: " + serverCreated);
+ * }
+ * 
+ */ +public class ServerPredicates { + private static final int TEN_MINUTES = 600; + private static final int FIVE_SECONDS = 5; + + /** + * Waits until a Server is ACTIVE. + * + * @param serverApi The ServerApi in the zone where your Server resides. + * @return Predicate that will check the status every 5 seconds for a maximum of 10 minutes. + */ + public static Predicate awaitActive(ServerApi serverApi) { + return awaitStatus(serverApi, ACTIVE, TEN_MINUTES, FIVE_SECONDS); + } + + /** + * Waits until a Server is SHUTOFF. + * + * @param serverApi The ServerApi in the zone where your Server resides. + * @return Predicate that will check the status every 5 seconds for a maximum of 10 minutes. + */ + public static Predicate awaitShutoff(ServerApi serverApi) { + return awaitStatus(serverApi, SHUTOFF, TEN_MINUTES, FIVE_SECONDS); + } + + /** + * Waits until a Server reaches Status. + * + * @param serverApi The ServerApi in the zone where your Server resides. + * @return Predicate that will check the status every periodInSec seconds for a maximum of maxWaitInSec minutes. + */ + public static Predicate awaitStatus( + ServerApi serverApi, Status status, long maxWaitInSec, long periodInSec) { + ServerStatusPredicate statusPredicate = new ServerStatusPredicate(serverApi, status); + + return retry(statusPredicate, maxWaitInSec, periodInSec, periodInSec, SECONDS); + } + + public static class ServerStatusPredicate implements Predicate { + private final ServerApi serverApi; + private final Status status; + + public ServerStatusPredicate(ServerApi serverApi, Status status) { + this.serverApi = checkNotNull(serverApi, "serverApi must be defined"); + this.status = checkNotNull(status, "status must be defined"); + } + + /** + * @return boolean Return true when the Server reaches the Status, false otherwise + * @throws IllegalStateException if the Server associated with serverId does not exist + */ + @Override + public boolean apply(String serverId) { + checkNotNull(serverId, "server must be defined"); + + Server server = serverApi.get(serverId); + + if (server == null) { + throw new IllegalStateException(String.format("Server %s not found.", serverId)); + } + + return status.equals(server.getStatus()); + } + } +} diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/features/ServerApiLiveTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/features/ServerApiLiveTest.java index de53d3e0e0..ff4cc4462e 100644 --- a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/features/ServerApiLiveTest.java +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/features/ServerApiLiveTest.java @@ -16,11 +16,14 @@ */ package org.jclouds.openstack.nova.v2_0.features; +import static org.jclouds.openstack.nova.v2_0.domain.Server.Status.ACTIVE; +import static org.jclouds.openstack.nova.v2_0.predicates.ServerPredicates.awaitActive; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; +import org.jclouds.http.HttpResponseException; import org.jclouds.openstack.nova.v2_0.domain.Network; import org.jclouds.openstack.nova.v2_0.domain.Server; import org.jclouds.openstack.nova.v2_0.domain.ServerCreated; @@ -84,9 +87,9 @@ public class ServerApiLiveTest extends BaseNovaApiLiveTest { for (String zoneId : zones) { ServerApi serverApi = api.getServerApiForZone(zoneId); try { - serverId = createServer(zoneId, "nova", Server.Status.ACTIVE).getId(); + serverId = createServer(zoneId, "nova").getId(); Server server = serverApi.get(serverId); - assertEquals(server.getStatus(), Server.Status.ACTIVE); + assertEquals(server.getStatus(), ACTIVE); } finally { if (serverId!=null) { serverApi.delete(serverId); @@ -114,9 +117,10 @@ public class ServerApiLiveTest extends BaseNovaApiLiveTest { ServerCreated server = serverApi.create(hostName, imageIdForZone(zoneId), "1", options); serverId = server.getId(); - blockUntilServerInState(server.getId(), serverApi, Server.Status.ACTIVE); + awaitActive(serverApi).apply(server.getId()); + Server serverCheck = serverApi.get(serverId); - assertEquals(serverCheck.getStatus(), Server.Status.ACTIVE); + assertEquals(serverCheck.getStatus(), ACTIVE); } finally { if (serverId != null) { serverApi.delete(serverId); @@ -131,9 +135,12 @@ public class ServerApiLiveTest extends BaseNovaApiLiveTest { for (String zoneId : zones) { ServerApi serverApi = api.getServerApiForZone(zoneId); try { - serverId = createServer(zoneId, "err", Server.Status.ERROR).getId(); - Server server = serverApi.get(serverId); - assertEquals(server.getStatus(), Server.Status.ERROR); + serverId = createServer(zoneId, "err").getId(); + } catch (HttpResponseException e) { + // Here is an implementation detail difference between OpenStack and some providers. + // Some providers accept a bad availability zone and create the server in the zoneId. + // Vanilla OpenStack will error out with a 400 Bad Request + assertEquals(e.getResponse().getStatusCode(), 400); } finally { if (serverId!=null) { serverApi.delete(serverId); @@ -150,11 +157,11 @@ public class ServerApiLiveTest extends BaseNovaApiLiveTest { for (String zoneId : zones) { ServerApi serverApi = api.getServerApiForZone(zoneId); try { - serverId = createServer(zoneId, Server.Status.ACTIVE).getId(); + serverId = createServer(zoneId, null).getId(); Server server = serverApi.get(serverId); - assertEquals(server.getStatus(), Server.Status.ACTIVE); + assertEquals(server.getStatus(), ACTIVE); RebuildServerOptions options = new RebuildServerOptions(). withImage(server.getImage().getId()). @@ -179,22 +186,18 @@ public class ServerApiLiveTest extends BaseNovaApiLiveTest { } } - private Server createServer(String regionId, Server.Status serverStatus) { - ServerApi serverApi = api.getServerApiForZone(regionId); + private Server createServer(String zoneId, String availabilityZoneId) { + ServerApi serverApi = api.getServerApiForZone(zoneId); + CreateServerOptions options = new CreateServerOptions(); - ServerCreated server = serverApi.create(hostName, imageIdForZone(regionId), flavorRefForZone(regionId), options); + if (availabilityZoneId != null) { + options = options.availabilityZone(availabilityZoneId); + } - blockUntilServerInState(server.getId(), serverApi, serverStatus); + ServerCreated server = serverApi.create(hostName, imageIdForZone(zoneId), flavorRefForZone(zoneId), options); - return serverApi.get(server.getId()); - } + awaitActive(serverApi).apply(server.getId()); - private Server createServer(String regionId, String availabilityZoneId, Server.Status serverStatus) { - ServerApi serverApi = api.getServerApiForZone(regionId); - CreateServerOptions options = new CreateServerOptions(); - options = options.availabilityZone(availabilityZoneId); - ServerCreated server = serverApi.create(hostName, imageIdForZone(regionId), flavorRefForZone(regionId), options); - blockUntilServerInState(server.getId(), serverApi, serverStatus); return serverApi.get(server.getId()); } diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/internal/BaseNovaApiLiveTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/internal/BaseNovaApiLiveTest.java index da1847bbe2..fd8ad6fd9d 100644 --- a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/internal/BaseNovaApiLiveTest.java +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/internal/BaseNovaApiLiveTest.java @@ -19,6 +19,7 @@ package org.jclouds.openstack.nova.v2_0.internal; import java.util.Properties; import java.util.Set; +import com.google.common.collect.*; import org.jclouds.apis.BaseApiLiveTest; import org.jclouds.openstack.keystone.v2_0.config.KeystoneProperties; import org.jclouds.openstack.nova.v2_0.NovaApi; @@ -35,9 +36,6 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import com.google.common.base.Throwables; -import com.google.common.collect.ComparisonChain; -import com.google.common.collect.Iterables; -import com.google.common.collect.Ordering; /** * Tests behavior of {@code NovaApi} @@ -58,7 +56,15 @@ public class BaseNovaApiLiveTest extends BaseApiLiveTest { @Override public void setup() { super.setup(); - zones = api.getConfiguredZones(); + + String testZone = System.getProperty("test." + provider + ".zone"); + + if (testZone != null) { + zones = ImmutableSet.of(testZone); + } else { + zones = api.getConfiguredZones(); + } + for (String zone : zones) { ServerApi serverApi = api.getServerApiForZone(zone); for (Resource server : serverApi.list().concat()) { @@ -83,7 +89,7 @@ public class BaseNovaApiLiveTest extends BaseApiLiveTest { return serverApi.get(server.getId()); } - /** + /** * Will block until the requested server is in the correct state, if Extended Server Status extension is loaded * this will continue to block while any task is in progress. */ @@ -100,10 +106,12 @@ public class BaseNovaApiLiveTest extends BaseApiLiveTest { } } } - + protected String imageIdForZone(String zoneId) { ImageApi imageApi = api.getImageApiForZone(zoneId); - return Iterables.getLast(imageApi.list().concat()).getId(); + + // Get the first image from the list as it tends to be "lighter" and faster to start + return Iterables.get(imageApi.list().concat(), 0).getId(); } protected String flavorRefForZone(String zoneId) { diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/predicates/ServerPredicatesMockTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/predicates/ServerPredicatesMockTest.java new file mode 100644 index 0000000000..4498c7c43f --- /dev/null +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/predicates/ServerPredicatesMockTest.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jclouds.openstack.nova.v2_0.predicates; + +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.MockWebServer; +import org.jclouds.openstack.nova.v2_0.NovaApi; +import org.jclouds.openstack.nova.v2_0.features.ServerApi; +import org.jclouds.openstack.v2_0.internal.BaseOpenStackMockTest; +import org.testng.annotations.Test; + +import static org.jclouds.openstack.nova.v2_0.domain.Server.Status.ACTIVE; +import static org.jclouds.openstack.nova.v2_0.domain.Server.Status.SHUTOFF; +import static org.jclouds.openstack.nova.v2_0.predicates.ServerPredicates.*; +import static org.testng.Assert.*; + +@Test(groups = "unit", testName = "ServerPredicatesMockTest") +public class ServerPredicatesMockTest extends BaseOpenStackMockTest { + public void testAwaitActive() throws Exception { + MockWebServer server = mockOpenStackServer(); + server.enqueue(new MockResponse().setBody(stringFromResource("/access.json"))); + server.enqueue(new MockResponse().setBody(stringFromResource("/server_details.json"))); + server.enqueue(new MockResponse().setBody(stringFromResource("/server_details.json"))); + server.enqueue(new MockResponse().setBody(stringFromResource("/server_details.json"))); + String serverDetailsActive = stringFromResource("/server_details.json").replace("BUILD(scheduling)", ACTIVE.value()); + server.enqueue(new MockResponse().setBody(serverDetailsActive)); + + try { + NovaApi novaApi = api(server.getUrl("/").toString(), "openstack-nova"); + ServerApi serverApi = novaApi.getServerApiForZone(("RegionOne")); + + boolean result = awaitActive(serverApi).apply("52415800-8b69-11e0-9b19-734f000004d2"); + + assertTrue(result); + assertEquals(server.getRequestCount(), 5); + assertAuthentication(server); + } finally { + server.shutdown(); + } + } + + public void testAwaitShutoff() throws Exception { + MockWebServer server = mockOpenStackServer(); + server.enqueue(new MockResponse().setBody(stringFromResource("/access.json"))); + server.enqueue(new MockResponse().setBody(stringFromResource("/server_details.json"))); + server.enqueue(new MockResponse().setBody(stringFromResource("/server_details.json"))); + server.enqueue(new MockResponse().setBody(stringFromResource("/server_details.json"))); + server.enqueue(new MockResponse().setBody(stringFromResource("/server_details.json"))); + server.enqueue(new MockResponse().setBody(stringFromResource("/server_details.json"))); + String serverDetailsShutoff = stringFromResource("/server_details.json").replace("BUILD(scheduling)", SHUTOFF.value()); + server.enqueue(new MockResponse().setBody(serverDetailsShutoff)); + + try { + NovaApi novaApi = api(server.getUrl("/").toString(), "openstack-nova"); + ServerApi serverApi = novaApi.getServerApiForZone(("RegionOne")); + + boolean result = awaitShutoff(serverApi).apply("52415800-8b69-11e0-9b19-734f000004d2"); + + assertTrue(result); + assertEquals(server.getRequestCount(), 7); + assertAuthentication(server); + } finally { + server.shutdown(); + } + } + + public void testAwaitTimeout() throws Exception { + MockWebServer server = mockOpenStackServer(); + server.enqueue(new MockResponse().setBody(stringFromResource("/access.json"))); + + for (int i=0; i < 20; i++) { + server.enqueue(new MockResponse().setBody(stringFromResource("/server_details.json"))); + } + + try { + NovaApi novaApi = api(server.getUrl("/").toString(), "openstack-nova"); + ServerApi serverApi = novaApi.getServerApiForZone(("RegionOne")); + + boolean result = awaitStatus(serverApi, ACTIVE, 3, 1).apply("52415800-8b69-11e0-9b19-734f000004d2"); + + assertFalse(result); + assertAuthentication(server); + } finally { + server.shutdown(); + } + } +} diff --git a/apis/openstack-nova/src/test/resources/access.json b/apis/openstack-nova/src/test/resources/access.json new file mode 100644 index 0000000000..84a962531a --- /dev/null +++ b/apis/openstack-nova/src/test/resources/access.json @@ -0,0 +1,228 @@ +{ + "access": { + "metadata": { + "roles": [ + "9fe2ff9ee4384b1894a90878d3e92bab", + "b926cb0f4e2642678735f86c2b06205e", + "33484487e73d4da0918a19b9c7e1f8ae", + "f2e54c2105fb49e29479af047115cebc" + ], + "is_admin": 0 + }, + "user": { + "name":"joe", + "roles": [ + { + "name":"_member_" + }, + { + "name":"anotherrole" + }, + { + "name":"heat_stack_owner" + }, + { + "name":"Member" + } + ], + "id":"8fbf8e68d36e4ac7bcf912a26213bd49", + "roles_links": [], + "username":"joe" + }, + "serviceCatalog": [ + { + "name":"nova", + "type":"compute", + "endpoints_links": [], + "endpoints": [ + { + "publicURL":"URL/v2/da0d12be20394afb851716e10a49e4a7", + "id":"2122bcaa704343c19ad2578410d4961d", + "internalURL":"URL/v2/da0d12be20394afb851716e10a49e4a7", + "region":"RegionOne", + "adminURL":"URL/v2/da0d12be20394afb851716e10a49e4a7" + } + ] + }, + { + "name":"neutron", + "type":"network", + "endpoints_links": [], + "endpoints": [ + { + "publicURL":"URL/", + "id":"65a4d3f13cfb49a6a57a04e205cc2158", + "internalURL":"URL/", + "region":"RegionOne", + "adminURL":"URL/" + } + ] + }, + { + "name":"cinderv2", + "type":"volumev2", + "endpoints_links": [], + "endpoints": [ + { + "publicURL":"URL/v2/da0d12be20394afb851716e10a49e4a7", + "id":"31fe4d92eac44044b05be21c6f44cebc", + "internalURL":"URL/v2/da0d12be20394afb851716e10a49e4a7", + "region":"RegionOne", + "adminURL":"URL/v2/da0d12be20394afb851716e10a49e4a7" + } + ] + }, + { + "name":"trove", + "type":"database", + "endpoints_links": [], + "endpoints": [ + { + "publicURL":"URL/v1.0/da0d12be20394afb851716e10a49e4a7", + "id":"06b7a7dbd25c4a01819c879700a9712a", + "internalURL":"URL/v1.0/da0d12be20394afb851716e10a49e4a7", + "region":"RegionOne", + "adminURL":"URL/v1.0/da0d12be20394afb851716e10a49e4a7" + } + ] + }, + { + "name":"s3", + "type":"s3", + "endpoints_links": [], + "endpoints": [ + { + "publicURL":"URL", + "id":"93b0b67091324e8ba01b62ee0584994c", + "internalURL":"URL", + "region":"RegionOne", + "adminURL":"URL" + } + ] + }, + { + "name":"glance", + "type":"image", + "endpoints_links": [], + "endpoints": [ + { + "publicURL":"URL", + "id":"a542e91bcfa046bfa1bf2397356d1414", + "internalURL":"URL", + "region":"RegionOne", + "adminURL":"URL" + } + ] + }, + { + "name":"novav3", + "type":"computev3", + "endpoints_links": [], + "endpoints": [ + { + "publicURL":"URL/v3", + "id":"9c3e8abb576d483db93bcef70c67bc1d", + "internalURL":"URL/v3", + "region":"RegionOne", + "adminURL":"URL/v3" + } + ] + }, + { + "name":"heat", + "type":"cloudformation", + "endpoints_links": [], + "endpoints": [ + { + "publicURL":"URL/v1", + "id":"6f4ca5ca9698425b85c300b3fc176c39", + "internalURL":"URL/v1", + "region":"RegionOne", + "adminURL":"URL/v1" + } + ] + }, + { + "name":"cinder", + "type":"volume", + "endpoints_links": [], + "endpoints": [ + { + "publicURL":"URL/v1/da0d12be20394afb851716e10a49e4a7", + "id":"037039c676694a35aa28d34fce09e51d", + "internalURL":"URL/v1/da0d12be20394afb851716e10a49e4a7", + "region":"RegionOne", + "adminURL":"URL/v1/da0d12be20394afb851716e10a49e4a7" + } + ] + }, + { + "name":"ec2", + "type":"ec2", + "endpoints_links": [], + "endpoints": [ + { + "publicURL":"URL/services/Cloud", + "id":"1d242631bccb4ff4ba7a395dbcb51648", + "internalURL":"URL/services/Cloud", + "region":"RegionOne", + "adminURL":"URL/services/Admin" + } + ] + }, + { + "name":"heat", + "type":"orchestration", + "endpoints_links": [], + "endpoints": [ + { + "publicURL":"URL/v1/da0d12be20394afb851716e10a49e4a7", + "id":"199d00075e4a40308a6ad2aa8980d0cd", + "internalURL":"URL/v1/da0d12be20394afb851716e10a49e4a7", + "region":"RegionOne", + "adminURL":"URL/v1/da0d12be20394afb851716e10a49e4a7" + } + ] + }, + { + "name":"swift", + "type":"object-store", + "endpoints_links": [], + "endpoints": [ + { + "publicURL":"URL/v1/AUTH_da0d12be20394afb851716e10a49e4a7", + "id":"26b2cb1efb044193b847fc3f2fb12e82", + "internalURL":"URL/v1/AUTH_da0d12be20394afb851716e10a49e4a7", + "region":"RegionOne", + "adminURL":"URL" + } + ] + }, + { + "name":"keystone", + "type":"identity", + "endpoints_links": [], + "endpoints": [ + { + "publicURL":"URL/v2.0", + "id":"1bbfe80b50df4c4a84040aa782e42140", + "internalURL":"URL/v2.0", + "region":"RegionOne", + "adminURL":"URL/v2.0" + } + ] + } + ], + "token": { + "tenant": { + "name":"jclouds", + "id":"da0d12be20394afb851716e10a49e4a7", + "enabled": true, + "description": null + }, + "id":"TOKEN", + "expires":"2014-04-28T22:48:24Z", + "issued_at":"2014-04-28T21:48:24.972896" + } + } +} \ No newline at end of file