From b31c5894624c90b66d51ecf07fdf18585fd1701a Mon Sep 17 00:00:00 2001 From: Richard Downer Date: Wed, 16 Nov 2011 15:23:53 +0000 Subject: [PATCH 1/5] Add TemplateExtract domain model to Cloudstack --- .../cloudstack/domain/TemplateExtraction.java | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 apis/cloudstack/src/main/java/org/jclouds/cloudstack/domain/TemplateExtraction.java diff --git a/apis/cloudstack/src/main/java/org/jclouds/cloudstack/domain/TemplateExtraction.java b/apis/cloudstack/src/main/java/org/jclouds/cloudstack/domain/TemplateExtraction.java new file mode 100644 index 0000000000..5972ea53dd --- /dev/null +++ b/apis/cloudstack/src/main/java/org/jclouds/cloudstack/domain/TemplateExtraction.java @@ -0,0 +1,292 @@ +/** + * 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.cloudstack.domain; + +import com.google.gson.annotations.SerializedName; + +import java.util.Date; + +/** + * @author Richard Downer + */ +public class TemplateExtraction implements Comparable { + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private long id; + private long accountId; + private Date created; + private long extractId; + private ExtractMode extractMode; + private String name; + private String state; + private String status; + private String storageType; + private int uploadPercentage; + private String url; + private long zoneId; + private String zoneName; + + /** + * @param id the id of extracted object + */ + public Builder id(long id) { + this.id = id; + return this; + } + + /** + * @param accountId the account id to which the extracted object belongs + */ + public Builder accountId(long accountId) { + this.accountId = accountId; + return this; + } + + /** + * @param created the time and date the object was created + */ + public Builder created(Date created) { + this.created = created; + return this; + } + + /** + * @param extractId the upload id of extracted object + */ + public Builder extractId(long extractId) { + this.extractId = extractId; + return this; + } + + /** + * @param extractMode the mode of extraction - upload or download + */ + public Builder extractMode(ExtractMode extractMode) { + this.extractMode = extractMode; + return this; + } + + /** + * @param name the name of the extracted object + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * @param state the state of the extracted object + */ + public Builder state(String state) { + this.state = state; + return this; + } + + /** + * @param status the status of the extraction + */ + public Builder status(String status) { + this.status = status; + return this; + } + + /** + * @param storageType type of the storage + */ + public Builder storageType(String storageType) { + this.storageType = storageType; + return this; + } + + /** + * @param uploadPercentage the percentage of the entity uploaded to the specified location + */ + public Builder uploadPercentage(int uploadPercentage) { + this.uploadPercentage = uploadPercentage; + return this; + } + + /** + * @param url if mode = upload then url of the uploaded entity. if mode = download the url from which the entity can be downloaded + */ + public Builder url(String url) { + this.url = url; + return this; + } + + /** + * @param zoneId zone ID the object was extracted from + */ + public Builder zoneId(long zoneId) { + this.zoneId = zoneId; + return this; + } + + /** + * @param zoneName zone name the object was extracted from + */ + public Builder zoneName(String zoneName) { + this.zoneName = zoneName; + return this; + } + + } + + private long id; + @SerializedName("accountid") + private long accountId; + private Date created; + private long extractId; + private ExtractMode extractMode; + private String name; + private String state; + private String status; + @SerializedName("storagetype") + private String storageType; + @SerializedName("uploadpercentage") + private int uploadPercentage; + private String url; + @SerializedName("zoneid") + private long zoneId; + @SerializedName("zonename") + private String zoneName; + + /** + * present only for serializer + */ + TemplateExtraction() { + } + + /** + * @return the id of extracted object + */ + public long getId() { + return id; + } + + /** + * @return the account id to which the extracted object belongs + */ + public long getAccountId() { + return accountId; + } + + /** + * @return the time and date the object was created + */ + public Date getCreated() { + return created; + } + + /** + * @return the upload id of extracted object + */ + public long getExtractId() { + return extractId; + } + + /** + * @return the mode of extraction - upload or download + */ + public ExtractMode getExtractMode() { + return extractMode; + } + + /** + * @return the name of the extracted object + */ + public String getName() { + return name; + } + + /** + * @return the state of the extracted object + */ + public String getState() { + return state; + } + + /** + * @return the status of the extraction + */ + public String getStatus() { + return status; + } + + /** + * @return type of the storage + */ + public String getStorageType() { + return storageType; + } + + /** + * @return the percentage of the entity uploaded to the specified location + */ + public int getUploadPercentage() { + return uploadPercentage; + } + + /** + * @return if mode = upload then url of the uploaded entity. if mode = download the url from which the entity can be downloaded + */ + public String getUrl() { + return url; + } + + /** + * @return zone ID the object was extracted from + */ + public long getZoneId() { + return zoneId; + } + + /** + * @return zone name the object was extracted from + */ + public String getZoneName() { + return zoneName; + } + + @Override + public boolean equals(Object o) { + throw new RuntimeException("FIXME: Implement me"); + } + + @Override + public int hashCode() { + throw new RuntimeException("FIXME: Implement me"); + } + + @Override + public String toString() { + throw new RuntimeException("FIXME: Implement me"); + } + + @Override + public int compareTo(TemplateExtraction other) { + throw new RuntimeException("FIXME: Implement me"); + } + +} From a2b6eac75f0b2fe305c6b1d2113ca0a98a0faa4a Mon Sep 17 00:00:00 2001 From: Richard Downer Date: Wed, 16 Nov 2011 15:23:38 +0000 Subject: [PATCH 2/5] Cloudstack uses the JSON key "template" to mean a different type in different contexts, confusing ParseAsyncJobFromHttpResponse. Add a (currently failing) unit test to reproduce this issue. --- .../ParseAsyncJobFromHttpResponseTest.java | 16 ++++++++++++---- ...eryasyncjobresultresponse-createtemplate.json | 1 + ...ryasyncjobresultresponse-extracttemplate.json | 1 + 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 apis/cloudstack/src/test/resources/queryasyncjobresultresponse-createtemplate.json create mode 100644 apis/cloudstack/src/test/resources/queryasyncjobresultresponse-extracttemplate.json diff --git a/apis/cloudstack/src/test/java/org/jclouds/cloudstack/functions/ParseAsyncJobFromHttpResponseTest.java b/apis/cloudstack/src/test/java/org/jclouds/cloudstack/functions/ParseAsyncJobFromHttpResponseTest.java index 400a162d08..8e6b539ff2 100644 --- a/apis/cloudstack/src/test/java/org/jclouds/cloudstack/functions/ParseAsyncJobFromHttpResponseTest.java +++ b/apis/cloudstack/src/test/java/org/jclouds/cloudstack/functions/ParseAsyncJobFromHttpResponseTest.java @@ -19,13 +19,11 @@ package org.jclouds.cloudstack.functions; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; import java.io.InputStream; -import org.jclouds.cloudstack.domain.AsyncJob; -import org.jclouds.cloudstack.domain.AsyncJobError; -import org.jclouds.cloudstack.domain.IPForwardingRule; -import org.jclouds.cloudstack.domain.PublicIPAddress; +import org.jclouds.cloudstack.domain.*; import org.jclouds.date.internal.SimpleDateFormatDateService; import org.jclouds.domain.JsonBall; import org.jclouds.http.HttpResponse; @@ -187,4 +185,14 @@ public class ParseAsyncJobFromHttpResponseTest { assertEquals(response, expects); } + public void testOverloadedKeyName() { + InputStream is = getClass().getResourceAsStream("/queryasyncjobresultresponse-createtemplate.json"); + ParseAsyncJobFromHttpResponse parser = i.getInstance(ParseAsyncJobFromHttpResponse.class); + AsyncJob response = parser.apply(new HttpResponse(200, "ok", Payloads.newInputStreamPayload(is))); + assertTrue(response.getResult() instanceof Template, "response expected to be Template, actually is "+response.getResult().getClass()); + + is = getClass().getResourceAsStream("/queryasyncjobresultresponse-extracttemplate.json"); + response = parser.apply(new HttpResponse(200, "ok", Payloads.newInputStreamPayload(is))); + assertTrue(response.getResult() instanceof TemplateExtraction, "response expected to be TemplateExtraction, actually is "+response.getResult().getClass()); + } } diff --git a/apis/cloudstack/src/test/resources/queryasyncjobresultresponse-createtemplate.json b/apis/cloudstack/src/test/resources/queryasyncjobresultresponse-createtemplate.json new file mode 100644 index 0000000000..ed2b886e7b --- /dev/null +++ b/apis/cloudstack/src/test/resources/queryasyncjobresultresponse-createtemplate.json @@ -0,0 +1 @@ +{ "queryasyncjobresultresponse" : {"jobid":2716,"jobstatus":1,"jobprocstatus":0,"jobresultcode":0,"jobresulttype":"object","jobresult":{"template":{"id":249,"name":"jclouds-c5c7dce0","displaytext":"jclouds live testCreateTemplate","ispublic":false,"created":"2011-11-16T06:43:00-0800","isready":true,"passwordenabled":false,"format":"VHD","isfeatured":false,"crossZones":false,"ostypeid":12,"ostypename":"CentOS 5.3 (64-bit)","account":"jcloud2","zoneid":1,"zonename":"Demo5","status":"Download Complete","size":8589934592,"templatetype":"USER","hypervisor":"XenServer","domain":"jCloud","domainid":11,"isextractable":true,"checksum":"bdc1c2c49b747694f97be5042323a1fa","sourcetemplateid":202}}} } \ No newline at end of file diff --git a/apis/cloudstack/src/test/resources/queryasyncjobresultresponse-extracttemplate.json b/apis/cloudstack/src/test/resources/queryasyncjobresultresponse-extracttemplate.json new file mode 100644 index 0000000000..387f56fcd4 --- /dev/null +++ b/apis/cloudstack/src/test/resources/queryasyncjobresultresponse-extracttemplate.json @@ -0,0 +1 @@ +{ "queryasyncjobresultresponse" : {"jobid":2720,"jobstatus":1,"jobprocstatus":0,"jobresultcode":0,"jobresulttype":"object","jobresult":{"template":{"id":249,"name":"jclouds-c5c7dce0","extractId":6,"accountid":19,"state":"DOWNLOAD_URL_CREATED","zoneid":1,"zonename":"Demo5","extractMode":"HTTP_DOWNLOAD","url":"https:%2F%2F72-52-126-96.realhostip.com%2Fuserdata%2Ff52f8e7d-7c89-4cf7-8e5a-b5ea17366d73.vhd"}}} } \ No newline at end of file From 1e7592ac983f9aff02a4a25e62ad3f7c7dfd9bb6 Mon Sep 17 00:00:00 2001 From: Richard Downer Date: Wed, 16 Nov 2011 20:38:13 +0000 Subject: [PATCH 3/5] Add a special case to distinguish when Cloudstack says 'template' but means 'TemplateExtract' - fixes the failing unit test --- .../cloudstack/functions/ParseTypedAsyncJob.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apis/cloudstack/src/main/java/org/jclouds/cloudstack/functions/ParseTypedAsyncJob.java b/apis/cloudstack/src/main/java/org/jclouds/cloudstack/functions/ParseTypedAsyncJob.java index b1cfba3514..4f358b32e0 100644 --- a/apis/cloudstack/src/main/java/org/jclouds/cloudstack/functions/ParseTypedAsyncJob.java +++ b/apis/cloudstack/src/main/java/org/jclouds/cloudstack/functions/ParseTypedAsyncJob.java @@ -27,6 +27,7 @@ import javax.annotation.Resource; import javax.inject.Named; import javax.inject.Singleton; +import com.google.common.base.Strings; import org.jclouds.cloudstack.domain.AsyncJob; import org.jclouds.cloudstack.domain.AsyncJob.Builder; import org.jclouds.cloudstack.domain.AsyncJobError; @@ -36,6 +37,7 @@ import org.jclouds.cloudstack.domain.PortForwardingRule; import org.jclouds.cloudstack.domain.PublicIPAddress; import org.jclouds.cloudstack.domain.SecurityGroup; import org.jclouds.cloudstack.domain.Template; +import org.jclouds.cloudstack.domain.TemplateExtraction; import org.jclouds.cloudstack.domain.VirtualMachine; import org.jclouds.domain.JsonBall; import org.jclouds.json.Json; @@ -61,7 +63,7 @@ public class ParseTypedAsyncJob implements Function> typeMap = ImmutableMap.> builder().put("securitygroup", SecurityGroup.class) .put("portforwardingrule", PortForwardingRule.class).put("ipforwardingrule", IPForwardingRule.class) - .put("template", Template.class).put("network", Network.class).put("ipaddress", PublicIPAddress.class) + .put("network", Network.class).put("ipaddress", PublicIPAddress.class) .put("virtualmachine", VirtualMachine.class).build(); private final Json json; @@ -80,7 +82,15 @@ public class ParseTypedAsyncJob implements Function entry = Iterables.get(toParse.getResult().entrySet(), 0); - if (typeMap.containsKey(entry.getKey())) { + if ("template".equals(entry.getKey())) { + // Sometimes Cloudstack will say 'template' and the payload is a Template object. + // Sometimes Cloudstack will say 'template' and the payload is a TemplateExtraction object. + // The 'state' field only exists on TemplateExtraction, so we can test this to work out what we have actually been given. + Template template = json.fromJson(entry.getValue().toString(), Template.class); + TemplateExtraction templateExtraction = json.fromJson(entry.getValue().toString(), TemplateExtraction.class); + boolean isTemplate = Strings.isNullOrEmpty(templateExtraction.getState()); + builder.result(isTemplate ? template : templateExtraction); + } else if (typeMap.containsKey(entry.getKey())) { builder.result(json.fromJson(entry.getValue().toString(), typeMap.get(entry.getKey()))); } else { logger.warn( From 75752db64c5a73a5939f24d01e0d011c1e9d4c37 Mon Sep 17 00:00:00 2001 From: Richard Downer Date: Wed, 16 Nov 2011 21:09:18 +0000 Subject: [PATCH 4/5] Add TemplateClientLiveTest.testExtractTemplate() --- .../features/TemplateClientLiveTest.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/apis/cloudstack/src/test/java/org/jclouds/cloudstack/features/TemplateClientLiveTest.java b/apis/cloudstack/src/test/java/org/jclouds/cloudstack/features/TemplateClientLiveTest.java index 4fc59ff138..91ba0160af 100644 --- a/apis/cloudstack/src/test/java/org/jclouds/cloudstack/features/TemplateClientLiveTest.java +++ b/apis/cloudstack/src/test/java/org/jclouds/cloudstack/features/TemplateClientLiveTest.java @@ -28,6 +28,9 @@ import org.testng.annotations.AfterGroups; import org.testng.annotations.Test; import javax.annotation.Nullable; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLDecoder; import java.util.Random; import java.util.Set; @@ -73,6 +76,7 @@ public class TemplateClientLiveTest extends BaseCloudStackClientLiveTest { } } + @Test(enabled = true) public void testCreateTemplate() throws Exception { Zone zone = Iterables.getFirst(client.getZoneClient().listZones(), null); assertNotNull(zone); @@ -121,4 +125,25 @@ public class TemplateClientLiveTest extends BaseCloudStackClientLiveTest { super.tearDown(); } + @Test(enabled = true, dependsOnMethods = "testCreateTemplate") + public void testExtractTemplate() throws Exception { + // Initiate the extraction and wait for it to complete + AsyncCreateResponse response = client.getTemplateClient().extractTemplate(template.getId(), ExtractMode.HTTP_DOWNLOAD, template.getZoneId()); + assert jobComplete.apply(response.getJobId()) : template; + + // Get the result + AsyncJob asyncJob = client.getAsyncJobClient().getAsyncJob(response.getJobId()); + TemplateExtraction extract = asyncJob.getResult(); + assertNotNull(extract); + + // Check that the URL can be retrieved + String extractUrl = extract.getUrl(); + assertNotNull(extractUrl); + URL url = new URL(URLDecoder.decode(extractUrl, "utf-8")); + HttpURLConnection connection = (HttpURLConnection)url.openConnection(); + connection.setRequestMethod("GET"); + connection.connect(); + assertEquals(connection.getResponseCode(), 200); + connection.disconnect(); + } } From e1bfb077805d1111dae78cfb8d80be5479043009 Mon Sep 17 00:00:00 2001 From: Richard Downer Date: Thu, 17 Nov 2011 10:28:37 +0000 Subject: [PATCH 5/5] Changes to TemplateClientLiveTest following review. Test now passes without a java SSL certificate exception. --- .../cloudstack/features/TemplateClientLiveTest.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/apis/cloudstack/src/test/java/org/jclouds/cloudstack/features/TemplateClientLiveTest.java b/apis/cloudstack/src/test/java/org/jclouds/cloudstack/features/TemplateClientLiveTest.java index 91ba0160af..24674ce04d 100644 --- a/apis/cloudstack/src/test/java/org/jclouds/cloudstack/features/TemplateClientLiveTest.java +++ b/apis/cloudstack/src/test/java/org/jclouds/cloudstack/features/TemplateClientLiveTest.java @@ -28,8 +28,7 @@ import org.testng.annotations.AfterGroups; import org.testng.annotations.Test; import javax.annotation.Nullable; -import java.net.HttpURLConnection; -import java.net.URL; +import java.net.URI; import java.net.URLDecoder; import java.util.Random; import java.util.Set; @@ -132,18 +131,14 @@ public class TemplateClientLiveTest extends BaseCloudStackClientLiveTest { assert jobComplete.apply(response.getJobId()) : template; // Get the result - AsyncJob asyncJob = client.getAsyncJobClient().getAsyncJob(response.getJobId()); + AsyncJob asyncJob = client.getAsyncJobClient().getAsyncJob(response.getJobId()); TemplateExtraction extract = asyncJob.getResult(); assertNotNull(extract); // Check that the URL can be retrieved String extractUrl = extract.getUrl(); assertNotNull(extractUrl); - URL url = new URL(URLDecoder.decode(extractUrl, "utf-8")); - HttpURLConnection connection = (HttpURLConnection)url.openConnection(); - connection.setRequestMethod("GET"); - connection.connect(); - assertEquals(connection.getResponseCode(), 200); - connection.disconnect(); + URI uri = new URI(URLDecoder.decode(extractUrl, "utf-8")); + assertTrue(context.utils().http().exists(uri), "does not exist: " + uri); } }