added generic typed asyncjob fro cloudstack so we can access VM passwords, which are only stored in the async job result

This commit is contained in:
Adrian Cole 2011-02-26 10:20:47 -08:00
parent 06355017b0
commit bc5b4e8ab3
10 changed files with 392 additions and 46 deletions

View File

@ -20,16 +20,105 @@
package org.jclouds.cloudstack.domain;
import java.util.Date;
import java.util.Map;
import com.google.common.collect.ImmutableMap;
import com.google.gson.annotations.SerializedName;
/**
*
* @author Adrian Cole
*/
public class AsyncJob {
public class AsyncJob<T> {
public static <T> Builder<T> builder() {
return new Builder<T>();
}
public static class Builder<T> {
private long accountId = -1;
private String cmd;
private Date created;
private long id = -1;
private long instanceId = -1;
private String instanceType;
private int progress = -1;
private T result;
private int resultCode = -1;
private String resultType;
private int status = -1;
private int userId = -1;
public Builder<T> accountId(long accountId) {
this.accountId = accountId;
return this;
}
public Builder<T> cmd(String cmd) {
this.cmd = cmd;
return this;
}
public Builder<T> created(Date created) {
this.created = created;
return this;
}
public Builder<T> id(long id) {
this.id = id;
return this;
}
public Builder<T> instanceId(long instanceId) {
this.instanceId = instanceId;
return this;
}
public Builder<T> instanceType(String instanceType) {
this.instanceType = instanceType;
return this;
}
public Builder<T> progress(int progress) {
this.progress = progress;
return this;
}
public Builder<T> result(T result) {
this.result = result;
return this;
}
public Builder<T> resultCode(int resultCode) {
this.resultCode = resultCode;
return this;
}
public Builder<T> resultType(String resultType) {
this.resultType = resultType;
return this;
}
public Builder<T> status(int status) {
this.status = status;
return this;
}
public Builder<T> userId(int userId) {
this.userId = userId;
return this;
}
public AsyncJob<T> build() {
return new AsyncJob<T>(accountId, cmd, created, id, instanceId, instanceType, progress, result, resultCode,
resultType, status, userId);
}
public static <T> Builder<T> fromAsyncJobUntyped(AsyncJob<T> in) {
return new Builder<T>().accountId(in.accountId).cmd(in.cmd).created(in.created).id(in.id)
.instanceId(in.instanceId).instanceType(in.instanceType).progress(in.progress).result(in.result)
.resultCode(in.resultCode).resultType(in.resultType).status(in.status).userId(in.userId);
}
}
@SerializedName("accountid")
private long accountId = -1;
private String cmd;
@ -43,7 +132,7 @@ public class AsyncJob {
@SerializedName("jobprocstatus")
private int progress = -1;
@SerializedName("jobresult")
private Map<String, Object> result = ImmutableMap.of();
private T result;
@SerializedName("jobresultcode")
private int resultCode = -1;
@SerializedName("jobresulttype")
@ -53,8 +142,8 @@ public class AsyncJob {
@SerializedName("userid")
private int userId = -1;
public AsyncJob(int accountId, String cmd, Date created, long id, long instanceId, String instanceType,
int progress, Map<String, Object> result, int resultCode, String resultType, int status, int userId) {
public AsyncJob(long accountId, String cmd, Date created, long id, long instanceId, String instanceType,
int progress, T result, int resultCode, String resultType, int status, int userId) {
this.accountId = accountId;
this.cmd = cmd;
this.created = created;
@ -129,7 +218,7 @@ public class AsyncJob {
/**
* @return the result reason
*/
public Map<String, Object> getResult() {
public T getResult() {
return result;
}
@ -188,7 +277,7 @@ public class AsyncJob {
return false;
if (getClass() != obj.getClass())
return false;
AsyncJob other = (AsyncJob) obj;
AsyncJob<?> other = (AsyncJob<?>) obj;
if (accountId != other.accountId)
return false;
if (cmd == null) {

View File

@ -28,10 +28,12 @@ import javax.ws.rs.core.MediaType;
import org.jclouds.cloudstack.domain.AsyncJob;
import org.jclouds.cloudstack.filters.QuerySigner;
import org.jclouds.cloudstack.functions.ParseAsyncJob;
import org.jclouds.cloudstack.options.ListAsyncJobsOptions;
import org.jclouds.rest.annotations.ExceptionParser;
import org.jclouds.rest.annotations.QueryParams;
import org.jclouds.rest.annotations.RequestFilters;
import org.jclouds.rest.annotations.ResponseParser;
import org.jclouds.rest.annotations.Unwrap;
import org.jclouds.rest.functions.ReturnEmptySetOnNotFoundOr404;
import org.jclouds.rest.functions.ReturnNullOnNotFoundOr404;
@ -58,16 +60,16 @@ public interface AsyncJobAsyncClient {
@Unwrap(depth = 2)
@Consumes(MediaType.APPLICATION_JSON)
@ExceptionParser(ReturnEmptySetOnNotFoundOr404.class)
ListenableFuture<Set<AsyncJob>> listAsyncJobs(ListAsyncJobsOptions... options);
ListenableFuture<Set<AsyncJob<?>>> listAsyncJobs(ListAsyncJobsOptions... options);
/**
* @see AsyncJobClient#getAsyncJob
*/
@GET
@QueryParams(keys = "command", values = "queryAsyncJobResult")
@Unwrap
@Consumes(MediaType.APPLICATION_JSON)
@ResponseParser(ParseAsyncJob.class)
@ExceptionParser(ReturnNullOnNotFoundOr404.class)
ListenableFuture<AsyncJob> getAsyncJob(@QueryParam("jobid") long id);
<T> ListenableFuture<AsyncJob<T>> getAsyncJob(@QueryParam("jobid") long id);
}

View File

@ -43,7 +43,7 @@ public interface AsyncJobClient {
* if present, how to constrain the list.
* @return asyncJobs matching query, or empty set, if no asyncJobs are found
*/
Set<AsyncJob> listAsyncJobs(ListAsyncJobsOptions... options);
Set<AsyncJob<?>> listAsyncJobs(ListAsyncJobsOptions... options);
/**
* get a specific asyncJob by id
@ -52,5 +52,5 @@ public interface AsyncJobClient {
* asyncJob to get
* @return asyncJob or null if not found
*/
AsyncJob getAsyncJob(long id);
<T> AsyncJob<T> getAsyncJob(long id);
}

View File

@ -0,0 +1,101 @@
/**
*
* Copyright (C) 2010 Cloud Conscious, LLC. <info@cloudconscious.com>
*
* ====================================================================
* Licensed 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.functions;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Resource;
import javax.inject.Named;
import org.jclouds.cloudstack.domain.AsyncJob;
import org.jclouds.cloudstack.domain.AsyncJob.Builder;
import org.jclouds.cloudstack.domain.Network;
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.VirtualMachine;
import org.jclouds.domain.JsonBall;
import org.jclouds.http.HttpResponse;
import org.jclouds.http.functions.UnwrapOnlyJsonValue;
import org.jclouds.json.Json;
import org.jclouds.logging.Logger;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.inject.Inject;
/**
*
* @author Adrian Cole
*/
public class ParseAsyncJob implements Function<HttpResponse, AsyncJob<?>> {
@Resource
protected Logger logger = Logger.NULL;
private final Json json;
private final UnwrapOnlyJsonValue<AsyncJob<Map<String, JsonBall>>> parser;
@Inject(optional = true)
@VisibleForTesting
@Named("jclouds.cloudstack.jobresult-type-map")
Map<String, Class<?>> typeMap = ImmutableMap.<String, Class<?>> builder().put("securitygroup", SecurityGroup.class)
.put("portforwardingrule", PortForwardingRule.class).put("template", Template.class)
.put("network", Network.class).put("ipaddress", PublicIPAddress.class)
.put("virtualmachine", VirtualMachine.class).build();
@Inject
public ParseAsyncJob(Json json, UnwrapOnlyJsonValue<AsyncJob<Map<String, JsonBall>>> parser) {
this.json = checkNotNull(json, "json");
this.parser = checkNotNull(parser, "parser");
}
public AsyncJob<?> apply(HttpResponse response) {
checkNotNull(response, "response");
AsyncJob<Map<String, JsonBall>> toParse = parser.apply(response);
checkNotNull(toParse, "parsed result from %s", response);
AsyncJob<?> result = toParse;
if (toParse.getResult() != null) {
if (toParse.getResult().size() == 1) {
Entry<String, JsonBall> entry = Iterables.get(toParse.getResult().entrySet(), 0);
@SuppressWarnings({ "unchecked", "rawtypes" })
Builder<Object> builder = AsyncJob.Builder.fromAsyncJobUntyped((AsyncJob) toParse);
if (typeMap.containsKey(entry.getKey())) {
builder.result(json.fromJson(entry.getValue().toString(), typeMap.get(entry.getKey())));
} else {
logger.warn(
"type key % not configured. please override default for Map<String, Class<?>> bound to name jclouds.cloudstack.jobresult-type-map",
entry.getKey());
builder.result(entry.getValue().toString());
}
result = builder.build();
} else if (toParse.getResult().size() > 1) {
logger.warn("unexpected size of async job result; expecting a map with a single element",
toParse.getResult());
}
}
return result;
}
}

View File

@ -22,10 +22,12 @@ package org.jclouds.cloudstack.features;
import java.io.IOException;
import java.lang.reflect.Method;
import org.jclouds.cloudstack.functions.ParseAsyncJob;
import org.jclouds.cloudstack.options.ListAsyncJobsOptions;
import org.jclouds.http.HttpRequest;
import org.jclouds.http.functions.UnwrapOnlyNestedJsonValue;
import org.jclouds.rest.functions.ReturnEmptySetOnNotFoundOr404;
import org.jclouds.rest.functions.ReturnNullOnNotFoundOr404;
import org.jclouds.rest.internal.RestAnnotationProcessor;
import org.testng.annotations.Test;
@ -39,6 +41,24 @@ import com.google.inject.TypeLiteral;
// NOTE:without testName, this will not call @Before* and fail w/NPE during surefire
@Test(groups = "unit", testName = "AsyncJobAsyncClientTest")
public class AsyncJobAsyncClientTest extends BaseCloudStackAsyncClientTest<AsyncJobAsyncClient> {
public void testGetAsyncJob() throws SecurityException, NoSuchMethodException, IOException {
Method method = AsyncJobAsyncClient.class.getMethod("getAsyncJob", long.class);
HttpRequest httpRequest = processor.createRequest(method, 11l);
assertRequestLineEquals(httpRequest,
"GET http://localhost:8080/client/api?response=json&command=queryAsyncJobResult&jobid=11 HTTP/1.1");
assertNonPayloadHeadersEqual(httpRequest, "Accept: application/json\n");
assertPayloadEquals(httpRequest, null, null, false);
assertResponseParserClassEquals(method, httpRequest, ParseAsyncJob.class);
assertSaxResponseParserClassEquals(method, null);
assertExceptionParserClassEquals(method, ReturnNullOnNotFoundOr404.class);
checkFilters(httpRequest);
}
public void testListAsyncJobs() throws SecurityException, NoSuchMethodException, IOException {
Method method = AsyncJobAsyncClient.class.getMethod("listAsyncJobs", ListAsyncJobsOptions[].class);
HttpRequest httpRequest = processor.createRequest(method);

View File

@ -36,12 +36,12 @@ import org.testng.annotations.Test;
public class AsyncJobClientLiveTest extends BaseCloudStackClientLiveTest {
public void testListAsyncJobs() throws Exception {
Set<AsyncJob> response = client.getAsyncJobClient().listAsyncJobs();
Set<AsyncJob<?>> response = client.getAsyncJobClient().listAsyncJobs();
assert null != response;
long asyncJobCount = response.size();
assertTrue(asyncJobCount >= 0);
for (AsyncJob asyncJob : response) {
AsyncJob query = client.getAsyncJobClient().getAsyncJob(asyncJob.getId());
for (AsyncJob<?> asyncJob : response) {
AsyncJob<?> query = client.getAsyncJobClient().getAsyncJob(asyncJob.getId());
assertEquals(query.getId(), asyncJob.getId());
assert query.getStatus() >= 0 : query;
assert query.getResultCode() >= 0 : query;

View File

@ -50,6 +50,7 @@ import org.testng.annotations.Test;
import com.google.common.base.Predicate;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
/**
@ -63,13 +64,13 @@ public class VirtualMachineClientLiveTest extends BaseCloudStackClientLiveTest {
static final Ordering<ServiceOffering> DEFAULT_SIZE_ORDERING = new Ordering<ServiceOffering>() {
public int compare(ServiceOffering left, ServiceOffering right) {
return ComparisonChain.start().compare(left.getCpuNumber(), right.getCpuNumber()).compare(left.getMemory(),
right.getMemory()).result();
return ComparisonChain.start().compare(left.getCpuNumber(), right.getCpuNumber())
.compare(left.getMemory(), right.getMemory()).result();
}
};
public static VirtualMachine createVirtualMachine(CloudStackClient client, RetryablePredicate<Long> jobComplete,
RetryablePredicate<VirtualMachine> virtualMachineRunning) {
RetryablePredicate<VirtualMachine> virtualMachineRunning) {
final Zone zone = get(client.getZoneClient().listZones(), 0);
long serviceOfferingId = DEFAULT_SIZE_ORDERING.min(client.getOfferingClient().listServiceOfferings()).getId();
@ -79,11 +80,13 @@ public class VirtualMachineClientLiveTest extends BaseCloudStackClientLiveTest {
@Override
public boolean apply(Template arg0) {
return arg0.getZoneId() == zone.getId() && arg0.isFeatured() && arg0.isReady()
&& or(equalTo("Ubuntu 10.04 (64-bit)"), equalTo("CentOS 5.3 (64-bit)")).apply(arg0.getOSType());
&& or(equalTo("Ubuntu 10.04 (64-bit)"), equalTo("CentOS 5.3 (64-bit)")).apply(arg0.getOSType());
}
});
if (Iterables.size(templates) == 0) {
throw new NoSuchElementException();
}
long templateId;
try {
// prefer password enabled
@ -111,21 +114,21 @@ public class VirtualMachineClientLiveTest extends BaseCloudStackClientLiveTest {
}).getId());
} else {
options.securityGroupId(find(client.getSecurityGroupClient().listSecurityGroups(),
new Predicate<SecurityGroup>() {
new Predicate<SecurityGroup>() {
@Override
public boolean apply(SecurityGroup arg0) {
return arg0.getName().equals("default");
}
@Override
public boolean apply(SecurityGroup arg0) {
return arg0.getName().equals("default");
}
}).getId());
}).getId());
}
System.out.printf("serviceOfferingId %d, templateId %d, zoneId %d, options %s%n", serviceOfferingId, templateId,
zone.getId(), options);
zone.getId(), options);
AsyncCreateResponse job = client.getVirtualMachineClient().deployVirtualMachine(serviceOfferingId, templateId,
zone.getId(), options);
zone.getId(), options);
assert jobComplete.apply(job.getJobId());
VirtualMachine vm = client.getVirtualMachineClient().getVirtualMachine(job.getId());
VirtualMachine vm = client.getAsyncJobClient().<VirtualMachine> getAsyncJob(job.getJobId()).getResult();
if (vm.isPasswordEnabled())
assert vm.getPassword() != null : vm;
assert virtualMachineRunning.apply(vm);
@ -181,7 +184,7 @@ public class VirtualMachineClientLiveTest extends BaseCloudStackClientLiveTest {
assertTrue(response.size() >= 0);
for (VirtualMachine vm : response) {
VirtualMachine newDetails = getOnlyElement(client.getVirtualMachineClient().listVirtualMachines(
ListVirtualMachinesOptions.Builder.id(vm.getId())));
ListVirtualMachinesOptions.Builder.id(vm.getId())));
assertEquals(vm.getId(), newDetails.getId());
checkVm(vm);
}
@ -219,22 +222,22 @@ public class VirtualMachineClientLiveTest extends BaseCloudStackClientLiveTest {
assert nic.getTrafficType() != null : vm;
assert nic.getGuestIPType() != null : vm;
switch (vm.getState()) {
case RUNNING:
case RUNNING:
assert nic.getNetmask() != null : vm;
assert nic.getGateway() != null : vm;
assert nic.getIPAddress() != null : vm;
break;
default:
if (nic.getGuestIPType() == GuestIPType.VIRTUAL) {
assert nic.getNetmask() != null : vm;
assert nic.getGateway() != null : vm;
assert nic.getIPAddress() != null : vm;
break;
default:
if (nic.getGuestIPType() == GuestIPType.VIRTUAL) {
assert nic.getNetmask() != null : vm;
assert nic.getGateway() != null : vm;
assert nic.getIPAddress() != null : vm;
} else {
assert nic.getNetmask() == null : vm;
assert nic.getGateway() == null : vm;
assert nic.getIPAddress() == null : vm;
}
break;
} else {
assert nic.getNetmask() == null : vm;
assert nic.getGateway() == null : vm;
assert nic.getIPAddress() == null : vm;
}
break;
}
}

View File

@ -47,7 +47,7 @@ import com.google.inject.Module;
// NOTE:without testName, this will not call @Before* and fail w/NPE during surefire
@Test(groups = "unit", testName = "QuerySignerTest")
public class QuerySignerTest {
@SuppressWarnings("unchecked")
@SuppressWarnings({ "unchecked", "rawtypes" })
public static final RestContextSpec<Map, List> DUMMY_SPEC = new RestContextSpec<Map, List>("cloudstack",
"http://localhost:8080/client/api", "2.2", "", "apiKey", "secretKey", Map.class, List.class,
PropertiesBuilder.class, (Class) RestContextBuilder.class, ImmutableList.<Module> of(new MockModule(),

View File

@ -0,0 +1,130 @@
/**
*
* Copyright (C) 2010 Cloud Conscious) LLC. <info@cloudconscious.com>
*
* ====================================================================
* Licensed 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.functions;
import static org.testng.Assert.assertEquals;
import java.io.InputStream;
import org.jclouds.cloudstack.domain.AsyncJob;
import org.jclouds.cloudstack.domain.PublicIPAddress;
import org.jclouds.date.internal.SimpleDateFormatDateService;
import org.jclouds.domain.JsonBall;
import org.jclouds.http.HttpResponse;
import org.jclouds.io.Payloads;
import org.jclouds.json.config.GsonModule;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Guice;
import com.google.inject.Injector;
/**
*
* @author Adrian Cole
*/
@Test(groups = "unit")
public class ParseAsyncJobTest {
Injector i = Guice.createInjector(new GsonModule() {
@Override
protected void configure() {
bind(DateAdapter.class).to(Iso8601DateAdapter.class);
super.configure();
}
});
public void testWithNoResult() {
String input = "{ \"queryasyncjobresultresponse\" : {\"jobid\":860,\"jobstatus\":0,\"jobprocstatus\":0,\"jobresultcode\":0} }";
AsyncJob<PublicIPAddress> expects = AsyncJob.<PublicIPAddress> builder().id(860).status(0).progress(0)
.resultCode(0).build();
ParseAsyncJob parser = i.getInstance(ParseAsyncJob.class);
@SuppressWarnings("unchecked")
AsyncJob<PublicIPAddress> response = (AsyncJob<PublicIPAddress>) parser.apply(new HttpResponse(200, "ok",
Payloads.newStringPayload(input)));
assertEquals(response, expects);
}
public void testWithUnknownResultReturnsStringifiedJson() {
String input = "{ \"queryasyncjobresultresponse\" : {\"jobid\":860,\"jobstatus\":0,\"jobprocstatus\":0,\"jobresultcode\":0,\"jobresult\":{\"foo\":{\"bar\":1}}}}";
AsyncJob<?> expects = AsyncJob.builder().id(860).status(0).progress(0).resultCode(0).result("{\"bar\":1}")
.build();
ParseAsyncJob parser = i.getInstance(ParseAsyncJob.class);
@SuppressWarnings("unchecked")
AsyncJob<PublicIPAddress> response = (AsyncJob<PublicIPAddress>) parser.apply(new HttpResponse(200, "ok",
Payloads.newStringPayload(input)));
assertEquals(response, expects);
}
public void testWithBadResultReturnsMap() {
// Not the best result object, but this is an unexpected error case. Cloud.com have verified
// that this case will not happen. This code is only here to prevent exceptions from being
// thrown in case they change their minds.
String input = "{ \"queryasyncjobresultresponse\" : {\"jobid\":860,\"jobstatus\":0,\"jobprocstatus\":0,\"jobresultcode\":0,\"jobresult\":{\"foo\":{\"bar\":1},\"foo2\":{\"bar2\":2}}}}";
AsyncJob<?> expects = AsyncJob.builder().id(860).status(0).progress(0).resultCode(0)
.result(ImmutableMap.of("foo", new JsonBall("{\"bar\":1}"), "foo2", new JsonBall("{\"bar2\":2}"))).build();
ParseAsyncJob parser = i.getInstance(ParseAsyncJob.class);
@SuppressWarnings("unchecked")
AsyncJob<PublicIPAddress> response = (AsyncJob<PublicIPAddress>) parser.apply(new HttpResponse(200, "ok",
Payloads.newStringPayload(input)));
assertEquals(response, expects);
}
public void testPublicIPAddress() {
InputStream is = getClass().getResourceAsStream("/queryasyncjobresultresponse-ipaddress.json");
AsyncJob<PublicIPAddress> expects = AsyncJob
.<PublicIPAddress> builder()
.id(860)
.status(1)
.progress(0)
.resultType("object")
.resultCode(0)
.result(
PublicIPAddress
.builder()
.id(6)
.IPAddress("72.52.126.35")
.allocated(
new SimpleDateFormatDateService().iso8601SecondsDateParse("2011-02-23T20:15:01-0800"))
.zoneId(1).zoneName("San Jose 1").isSourceNAT(false).account("adrian").domainId(1)
.domain("ROOT").usesVirtualNetwork(true).isStaticNAT(false).associatedNetworkId(204)
.networkId(200).state(PublicIPAddress.State.ALLOCATING).build()
).build();
ParseAsyncJob parser = i.getInstance(ParseAsyncJob.class);
@SuppressWarnings("unchecked")
AsyncJob<PublicIPAddress> response = (AsyncJob<PublicIPAddress>) parser.apply(new HttpResponse(200, "ok",
Payloads.newInputStreamPayload(is)));
assertEquals(response, expects);
}
}

View File

@ -0,0 +1 @@
{ "queryasyncjobresultresponse" : {"jobid":860,"jobstatus":1,"jobprocstatus":0,"jobresultcode":0,"jobresulttype":"object","jobresult":{"ipaddress":{"id":6,"ipaddress":"72.52.126.35","allocated":"2011-02-23T20:15:01-0800","zoneid":1,"zonename":"San Jose 1","issourcenat":false,"account":"adrian","domainid":1,"domain":"ROOT","forvirtualnetwork":true,"isstaticnat":false,"associatednetworkid":204,"networkid":200,"state":"Allocating"}}} }