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 994dd290df..2640f7439e 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 @@ -34,6 +34,7 @@ import org.jclouds.openstack.domain.Resource; import org.jclouds.openstack.filters.AuthenticateRequest; 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; import org.jclouds.openstack.nova.v1_1.options.CreateServerOptions; import org.jclouds.openstack.nova.v1_1.options.RebuildServerOptions; import org.jclouds.rest.annotations.ExceptionParser; @@ -41,9 +42,11 @@ 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.ResponseParser; import org.jclouds.rest.annotations.SelectJson; import org.jclouds.rest.annotations.SkipEncoding; import org.jclouds.rest.annotations.Unwrap; +import org.jclouds.rest.functions.MapHttp4xxCodesToExceptions; import org.jclouds.rest.functions.ReturnEmptySetOnNotFoundOr404; import org.jclouds.rest.functions.ReturnFalseOnNotFoundOr404; import org.jclouds.rest.functions.ReturnNullOnNotFoundOr404; @@ -182,4 +185,17 @@ public interface ServerAsyncClient { @Produces(MediaType.APPLICATION_JSON) @Payload("%7B\"server\":%7B\"name\":\"{name}\"%7D%7D") ListenableFuture renameServer(@PathParam("id") String id, @PayloadParam("name") String newName); + + /** + * @see ServerClient#createImageFromServer + */ + @POST + @Path("/servers/{id}/action") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Payload("%7B\"createImage\":%7B\"name\":\"{name}\", \"metadata\": %7B%7D%7D%7D") + @ExceptionParser(MapHttp4xxCodesToExceptions.class) + @ResponseParser(ParseImageIdFromLocationHeader.class) + ListenableFuture createImageFromServer(@PayloadParam("name") String name, @PathParam("id") String id); + } 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 b0723f1568..51003d3340 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 @@ -156,4 +156,17 @@ public interface ServerClient { * The new name for the server */ void renameServer(String id, String newName); + + /** + * Create an image from a server. + * + * @param name + * The name of the new image + * @param id + * id of the server + * + * @return ID of the new / updated image + */ + String createImageFromServer(String name, String id); + } diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/functions/ParseImageIdFromLocationHeader.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/functions/ParseImageIdFromLocationHeader.java new file mode 100644 index 0000000000..1bdeb2c8f0 --- /dev/null +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/functions/ParseImageIdFromLocationHeader.java @@ -0,0 +1,45 @@ +/** + * 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.functions; + + +import javax.inject.Singleton; + +import javax.ws.rs.core.HttpHeaders; + +import org.jclouds.openstack.nova.v1_1.domain.Image; +import org.jclouds.http.HttpResponse; + +import com.google.common.base.Function; + + +/** + * This parses {@link Image} from the body of the link in the Location header of the HTTPResponse. + * + * @author Tim Miller + */ +@Singleton +public class ParseImageIdFromLocationHeader implements Function { + + public String apply(HttpResponse response) { + String location = response.getFirstHeaderOrNull(HttpHeaders.LOCATION); + String[] parts = location.split("/"); + return parts[parts.length - 1]; + } +} diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/features/ServerClientExpectTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/features/ServerClientExpectTest.java index c59690c0d9..35de130921 100644 --- a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/features/ServerClientExpectTest.java +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/features/ServerClientExpectTest.java @@ -20,6 +20,7 @@ package org.jclouds.openstack.nova.v1_1.features; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; import java.net.URI; @@ -37,7 +38,7 @@ import com.google.common.collect.ImmutableSet; /** * Tests annotation parsing of {@code ServerAsyncClient} - * + * * @author Adrian Cole */ @Test(groups = "unit", testName = "ServerAsyncClientTest") @@ -80,7 +81,7 @@ public class ServerClientExpectTest extends BaseNovaClientExpectTest { assertTrue(clientWhenNoServersExist.getServerClientForZone("az-1.region-a.geo-1").listServers().isEmpty()); } - + public void testCreateServerWhenResponseIs202() throws Exception { HttpRequest createServer = HttpRequest .builder() @@ -93,7 +94,7 @@ public class ServerClientExpectTest extends BaseNovaClientExpectTest { "{\"server\":{\"name\":\"test-e92\",\"imageRef\":\"1241\",\"flavorRef\":\"100\"}}","application/json")) .build(); - + HttpResponse createServerResponse = HttpResponse.builder().statusCode(202).message("HTTP/1.1 202 Accepted") .payload(payloadFromResourceWithContentType("/new_server.json","application/json; charset=UTF-8")).build(); @@ -117,7 +118,7 @@ public class ServerClientExpectTest extends BaseNovaClientExpectTest { "{\"server\":{\"name\":\"test-e92\",\"imageRef\":\"1241\",\"flavorRef\":\"100\",\"security_groups\":[{\"name\":\"group2\"},{\"name\":\"group1\"}]}}","application/json")) .build(); - + HttpResponse createServerResponse = HttpResponse.builder().statusCode(202).message("HTTP/1.1 202 Accepted") .payload(payloadFromResourceWithContentType("/new_server.json","application/json; charset=UTF-8")).build(); @@ -130,4 +131,61 @@ public class ServerClientExpectTest extends BaseNovaClientExpectTest { new ParseCreatedServerTest().expected().toString()); } + public void testCreateImageWhenResponseIs2xx() throws Exception { + String serverId = "123"; + String imageId = "456"; + String imageName = "foo"; + + HttpRequest createImage = HttpRequest + .builder() + .method("POST") + .endpoint(URI.create("https://compute.north.host/v1.1/3456/servers/" + serverId + "/action")) + .headers( + ImmutableMultimap. builder().put("Accept", "application/json") + .put("X-Auth-Token", authToken).build()) + .payload(payloadFromStringWithContentType( + "{\"createImage\":{\"name\":\"" + imageName + "\", \"metadata\": {}}}", "application/json")) + .build(); + + HttpResponse createImageResponse = HttpResponse.builder() + .statusCode(200) + .headers( + ImmutableMultimap. builder() + .put("Location", "https://compute.north.host/v1.1/3456/images/" + imageId).build()).build(); + + NovaClient clientWhenServerExists = requestsSendResponses(keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, createImage, createImageResponse); + + assertEquals(clientWhenServerExists.getServerClientForZone("az-1.region-a.geo-1").createImageFromServer(imageName, serverId), + imageId); + } + + public void testCreateImageWhenResponseIs404IsEmpty() throws Exception { + String serverId = "123"; + String imageName = "foo"; + + HttpRequest createImage = HttpRequest + .builder() + .method("POST") + .endpoint(URI.create("https://compute.north.host/v1.1/3456/servers/" + serverId + "/action")) + .headers( + ImmutableMultimap. builder().put("Accept", "application/json") + .put("X-Auth-Token", authToken) + .put("Content-Type", "application/json").build()) + .payload(payloadFromStringWithContentType( + "{\"createImage\":{\"name\":\"" + imageName + "\", \"metadata\": {}}}", "application/json")) + .build(); + + HttpResponse createImageResponse = HttpResponse.builder().statusCode(404).build(); + NovaClient clientWhenServerDoesNotExist = requestsSendResponses(keystoneAuthWithUsernameAndPassword, + responseWithKeystoneAccess, createImage, createImageResponse); + + try { + clientWhenServerDoesNotExist.getServerClientForZone("az-1.region-a.geo-1").createImageFromServer(imageName, serverId); + fail("Expected an exception."); + } catch (Exception e) { + ; + } + } + }