You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ro...@apache.org on 2018/07/25 09:18:06 UTC

[cloudstack] branch master updated: asyncjobs: add endtime to async jobs (#2739)

This is an automated email from the ASF dual-hosted git repository.

rohit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/master by this push:
     new 542d4da  asyncjobs: add endtime to async jobs (#2739)
542d4da is described below

commit 542d4da16c18e9c2c49730e6507d1dcf021f1724
Author: ernjvr <er...@gmail.com>
AuthorDate: Wed Jul 25 11:18:01 2018 +0200

    asyncjobs: add endtime to async jobs (#2739)
    
    There is currently no functional mechanism that captures or persists the end time of when an asynchronous job has finished. As a result, users are not able to do any reporting about the duration of various asynchronous jobs in Cloudstack.
    Link to FS:
    https://cwiki.apache.org/confluence/display/CLOUDSTACK/Add+End+Time+To+Asynchronous+Jobs
---
 .travis.yml                                        |   1 +
 .../org/apache/cloudstack/api/ApiConstants.java    |   1 +
 .../cloudstack/api/response/AsyncJobResponse.java  |   8 ++
 .../java/org/apache/cloudstack/jobs/JobInfo.java   |   2 +
 .../resources/META-INF/db/schema-41110to41200.sql  |   4 +-
 .../cloudstack/framework/jobs/dao/AsyncJobDao.java |   1 +
 .../framework/jobs/dao/AsyncJobDaoImpl.java        |  11 +-
 .../framework/jobs/impl/AsyncJobManagerImpl.java   |  29 +++--
 .../cloudstack/framework/jobs/impl/AsyncJobVO.java |  10 ++
 .../main/java/com/cloud/api/ApiResponseHelper.java |  10 +-
 .../cloud/api/query/dao/AsyncJobJoinDaoImpl.java   |  11 +-
 .../com/cloud/storage/dao/AsyncJobJoinDaoTest.java |  89 ++++++++++++++
 test/integration/smoke/test_async_job.py           | 135 +++++++++++++++++++++
 tools/marvin/setup.py                              |   3 +-
 14 files changed, 286 insertions(+), 29 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 130e907..e89d4cb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -42,6 +42,7 @@ env:
     - TESTS="smoke/test_accounts
              smoke/test_affinity_groups
              smoke/test_affinity_groups_projects
+             smoke/test_async_job
              smoke/test_deploy_vgpu_enabled_vm
              smoke/test_deploy_vm_iso
              smoke/test_deploy_vm_root_resize
diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
index 275a3cc..f03ddc7 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -549,6 +549,7 @@ public class ApiConstants {
     public static final String IPSEC_PSK = "ipsecpsk";
     public static final String GUEST_IP = "guestip";
     public static final String REMOVED = "removed";
+    public static final String COMPLETED = "completed";
     public static final String IKE_POLICY = "ikepolicy";
     public static final String ESP_POLICY = "esppolicy";
     public static final String IKE_LIFETIME = "ikelifetime";
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/AsyncJobResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/AsyncJobResponse.java
index 70bbeee..eecd6be 100644
--- a/api/src/main/java/org/apache/cloudstack/api/response/AsyncJobResponse.java
+++ b/api/src/main/java/org/apache/cloudstack/api/response/AsyncJobResponse.java
@@ -75,6 +75,10 @@ public class AsyncJobResponse extends BaseResponse {
     @Param(description = "  the created date of the job")
     private Date created;
 
+    @SerializedName(ApiConstants.COMPLETED)
+    @Param(description = "  the completed date of the job")
+    private Date removed;
+
     public void setAccountId(String accountId) {
         this.accountId = accountId;
     }
@@ -119,4 +123,8 @@ public class AsyncJobResponse extends BaseResponse {
     public void setCreated(Date created) {
         this.created = created;
     }
+
+    public void setRemoved(final Date removed) {
+        this.removed = removed;
+    }
 }
diff --git a/api/src/main/java/org/apache/cloudstack/jobs/JobInfo.java b/api/src/main/java/org/apache/cloudstack/jobs/JobInfo.java
index c7c9b96..5b63e62 100644
--- a/api/src/main/java/org/apache/cloudstack/jobs/JobInfo.java
+++ b/api/src/main/java/org/apache/cloudstack/jobs/JobInfo.java
@@ -68,6 +68,8 @@ public interface JobInfo extends Identity, InternalIdentity {
 
     Date getCreated();
 
+    Date getRemoved();
+
     Date getLastUpdated();
 
     Date getLastPolled();
diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41110to41200.sql b/engine/schema/src/main/resources/META-INF/db/schema-41110to41200.sql
index d5e6d61..de6865f 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-41110to41200.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-41110to41200.sql
@@ -32,4 +32,6 @@ ALTER TABLE `vlan` CHANGE `description` `ip4_range` varchar(255);
 -- We are only adding the permission to the default rules. Any custom rule must be configured by the root admin.
 INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`, `sort_order`) values (UUID(), 2, 'moveNetworkAclItem', 'ALLOW', 100) ON DUPLICATE KEY UPDATE rule=rule;
 INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`, `sort_order`) values (UUID(), 3, 'moveNetworkAclItem', 'ALLOW', 302) ON DUPLICATE KEY UPDATE rule=rule;
-INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`, `sort_order`) values (UUID(), 4, 'moveNetworkAclItem', 'ALLOW', 260) ON DUPLICATE KEY UPDATE rule=rule;
\ No newline at end of file
+INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`, `sort_order`) values (UUID(), 4, 'moveNetworkAclItem', 'ALLOW', 260) ON DUPLICATE KEY UPDATE rule=rule;
+
+UPDATE `cloud`.`async_job` SET `removed` = now() WHERE `removed` IS NULL;
\ No newline at end of file
diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java
index 8778bef..b2b685d 100644
--- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java
+++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java
@@ -24,6 +24,7 @@ import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO;
 import com.cloud.utils.db.GenericDao;
 
 public interface AsyncJobDao extends GenericDao<AsyncJobVO, Long> {
+
     AsyncJobVO findInstancePendingAsyncJob(String instanceType, long instanceId);
 
     List<AsyncJobVO> findInstancePendingAsyncJobs(String instanceType, Long accountId);
diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java
index 0ccd4ad..ef992ff 100644
--- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java
+++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java
@@ -21,6 +21,7 @@ import java.sql.SQLException;
 import java.util.Date;
 import java.util.List;
 
+import org.apache.cloudstack.api.ApiConstants;
 import org.apache.log4j.Logger;
 
 import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO;
@@ -71,7 +72,7 @@ public class AsyncJobDaoImpl extends GenericDaoBase<AsyncJobVO, Long> implements
         expiringUnfinishedAsyncJobSearch.done();
 
         expiringCompletedAsyncJobSearch = createSearchBuilder();
-        expiringCompletedAsyncJobSearch.and("created", expiringCompletedAsyncJobSearch.entity().getCreated(), SearchCriteria.Op.LTEQ);
+        expiringCompletedAsyncJobSearch.and(ApiConstants.REMOVED, expiringCompletedAsyncJobSearch.entity().getRemoved(), SearchCriteria.Op.LTEQ);
         expiringCompletedAsyncJobSearch.and("completeMsId", expiringCompletedAsyncJobSearch.entity().getCompleteMsid(), SearchCriteria.Op.NNULL);
         expiringCompletedAsyncJobSearch.and("jobStatus", expiringCompletedAsyncJobSearch.entity().getStatus(), SearchCriteria.Op.NEQ);
         expiringCompletedAsyncJobSearch.done();
@@ -168,11 +169,11 @@ public class AsyncJobDaoImpl extends GenericDaoBase<AsyncJobVO, Long> implements
     }
 
     @Override
-    public List<AsyncJobVO> getExpiredCompletedJobs(Date cutTime, int limit) {
-        SearchCriteria<AsyncJobVO> sc = expiringCompletedAsyncJobSearch.create();
-        sc.setParameters("created", cutTime);
+    public List<AsyncJobVO> getExpiredCompletedJobs(final Date cutTime, final int limit) {
+        final SearchCriteria<AsyncJobVO> sc = expiringCompletedAsyncJobSearch.create();
+        sc.setParameters(ApiConstants.REMOVED, cutTime);
         sc.setParameters("jobStatus", JobInfo.Status.IN_PROGRESS);
-        Filter filter = new Filter(AsyncJobVO.class, "created", true, 0L, (long)limit);
+        final Filter filter = new Filter(AsyncJobVO.class, ApiConstants.REMOVED, true, 0L, (long)limit);
         return listIncludingRemovedBy(sc, filter);
     }
 
diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java
index 174f1f3..1845dbf 100644
--- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java
+++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java
@@ -161,7 +161,7 @@ public class AsyncJobManagerImpl extends ManagerBase implements AsyncJobManager,
 
     @Override
     public AsyncJobVO getAsyncJob(long jobId) {
-        return _jobDao.findById(jobId);
+        return _jobDao.findByIdIncludingRemoved(jobId);
     }
 
     @Override
@@ -286,9 +286,9 @@ public class AsyncJobManagerImpl extends ManagerBase implements AsyncJobManager,
         if (s_logger.isDebugEnabled()) {
             s_logger.debug("Wake up jobs related to job-" + jobId);
         }
-        List<Long> wakeupList = Transaction.execute(new TransactionCallback<List<Long>>() {
+        final List<Long> wakeupList = Transaction.execute(new TransactionCallback<List<Long>>() {
             @Override
-            public List<Long> doInTransaction(TransactionStatus status) {
+            public List<Long> doInTransaction(final TransactionStatus status) {
                 if (s_logger.isDebugEnabled()) {
                     s_logger.debug("Update db status for job-" + jobId);
                 }
@@ -302,14 +302,16 @@ public class AsyncJobManagerImpl extends ManagerBase implements AsyncJobManager,
                     job.setResult(null);
                 }
 
-                job.setLastUpdated(DateUtil.currentGMTTime());
+                final Date currentGMTTime = DateUtil.currentGMTTime();
+                job.setLastUpdated(currentGMTTime);
+                job.setRemoved(currentGMTTime);
                 job.setExecutingMsid(null);
                 _jobDao.update(jobId, job);
 
                 if (s_logger.isDebugEnabled()) {
                     s_logger.debug("Wake up jobs joined with job-" + jobId + " and disjoin all subjobs created from job- " + jobId);
                 }
-                List<Long> wakeupList = wakeupByJoinedJobCompletion(jobId);
+                final List<Long> wakeupList = wakeupByJoinedJobCompletion(jobId);
                 _joinMapDao.disjoinAllJobs(jobId);
 
                 // purge the job sync item from queue
@@ -445,8 +447,8 @@ public class AsyncJobManagerImpl extends ManagerBase implements AsyncJobManager,
     }
 
     @Override
-    public AsyncJob queryJob(long jobId, boolean updatePollTime) {
-        AsyncJobVO job = _jobDao.findById(jobId);
+    public AsyncJob queryJob(final long jobId, final boolean updatePollTime) {
+        final AsyncJobVO job = _jobDao.findByIdIncludingRemoved(jobId);
 
         if (updatePollTime) {
             job.setLastPolled(DateUtil.currentGMTTime());
@@ -1025,8 +1027,8 @@ public class AsyncJobManagerImpl extends ManagerBase implements AsyncJobManager,
                     // purge sync queue item running on this ms node
                     _queueMgr.cleanupActiveQueueItems(msid, true);
                     // reset job status for all jobs running on this ms node
-                    List<AsyncJobVO> jobs = _jobDao.getResetJobs(msid);
-                    for (AsyncJobVO job : jobs) {
+                    final List<AsyncJobVO> jobs = _jobDao.getResetJobs(msid);
+                    for (final AsyncJobVO job : jobs) {
                         if (s_logger.isDebugEnabled()) {
                             s_logger.debug("Cancel left-over job-" + job.getId());
                         }
@@ -1034,12 +1036,15 @@ public class AsyncJobManagerImpl extends ManagerBase implements AsyncJobManager,
                         job.setResultCode(ApiErrorCode.INTERNAL_ERROR.getHttpCode());
                         job.setResult("job cancelled because of management server restart or shutdown");
                         job.setCompleteMsid(msid);
+                        final Date currentGMTTime = DateUtil.currentGMTTime();
+                        job.setLastUpdated(currentGMTTime);
+                        job.setRemoved(currentGMTTime);
                         _jobDao.update(job.getId(), job);
                         if (s_logger.isDebugEnabled()) {
                             s_logger.debug("Purge queue item for cancelled job-" + job.getId());
                         }
                         _queueMgr.purgeAsyncJobQueueItemId(job.getId());
-                        if (job.getInstanceType().equals(ApiCommandJobType.Volume.toString())) {
+                        if (ApiCommandJobType.Volume.toString().equals(job.getInstanceType())) {
 
                             try {
                                 _volumeDetailsDao.removeDetail(job.getInstanceId(), "SNAPSHOT_ID");
@@ -1049,8 +1054,8 @@ public class AsyncJobManagerImpl extends ManagerBase implements AsyncJobManager,
                             }
                         }
                     }
-                    List<SnapshotDetailsVO> snapshotList = _snapshotDetailsDao.findDetails(AsyncJob.Constants.MS_ID, Long.toString(msid), false);
-                    for (SnapshotDetailsVO snapshotDetailsVO : snapshotList) {
+                    final List<SnapshotDetailsVO> snapshotList = _snapshotDetailsDao.findDetails(AsyncJob.Constants.MS_ID, Long.toString(msid), false);
+                    for (final SnapshotDetailsVO snapshotDetailsVO : snapshotList) {
                         SnapshotInfo snapshot = snapshotFactory.getSnapshot(snapshotDetailsVO.getResourceId(), DataStoreRole.Primary);
                         snapshotSrv.processEventOnSnapshotObject(snapshot, Snapshot.Event.OperationFailed);
                         _snapshotDetailsDao.removeDetail(snapshotDetailsVO.getResourceId(), AsyncJob.Constants.MS_ID);
diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobVO.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobVO.java
index 0ca9ed5..9d30c2c 100644
--- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobVO.java
+++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobVO.java
@@ -373,6 +373,15 @@ public class AsyncJobVO implements AsyncJob, JobInfo {
     }
 
     @Override
+    public Date getRemoved() {
+        return removed;
+    }
+
+    public void setRemoved(final Date removed) {
+        this.removed = removed;
+    }
+
+    @Override
     public String toString() {
         StringBuffer sb = new StringBuffer();
         sb.append("AsyncJobVO {id:").append(getId());
@@ -392,6 +401,7 @@ public class AsyncJobVO implements AsyncJob, JobInfo {
         sb.append(", lastUpdated: ").append(getLastUpdated());
         sb.append(", lastPolled: ").append(getLastPolled());
         sb.append(", created: ").append(getCreated());
+        sb.append(", removed: ").append(getRemoved());
         sb.append("}");
         return sb.toString();
     }
diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
index 4d7de2a..4177223 100644
--- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java
+++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
@@ -1808,16 +1808,16 @@ public class ApiResponseHelper implements ResponseGenerator {
     }
 
     @Override
-    public AsyncJobResponse queryJobResult(QueryAsyncJobResultCmd cmd) {
-        Account caller = CallContext.current().getCallingAccount();
+    public AsyncJobResponse queryJobResult(final QueryAsyncJobResultCmd cmd) {
+        final Account caller = CallContext.current().getCallingAccount();
 
-        AsyncJob job = _entityMgr.findById(AsyncJob.class, cmd.getId());
+        final AsyncJob job = _entityMgr.findByIdIncludingRemoved(AsyncJob.class, cmd.getId());
         if (job == null) {
             throw new InvalidParameterValueException("Unable to find a job by id " + cmd.getId());
         }
 
-        User userJobOwner = _accountMgr.getUserIncludingRemoved(job.getUserId());
-        Account jobOwner = _accountMgr.getAccount(userJobOwner.getAccountId());
+        final User userJobOwner = _accountMgr.getUserIncludingRemoved(job.getUserId());
+        final Account jobOwner = _accountMgr.getAccount(userJobOwner.getAccountId());
 
         //check permissions
         if (_accountMgr.isNormalUser(caller.getId())) {
diff --git a/server/src/main/java/com/cloud/api/query/dao/AsyncJobJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/AsyncJobJoinDaoImpl.java
index fefc896..bd11015 100644
--- a/server/src/main/java/com/cloud/api/query/dao/AsyncJobJoinDaoImpl.java
+++ b/server/src/main/java/com/cloud/api/query/dao/AsyncJobJoinDaoImpl.java
@@ -50,12 +50,13 @@ public class AsyncJobJoinDaoImpl extends GenericDaoBase<AsyncJobJoinVO, Long> im
     }
 
     @Override
-    public AsyncJobResponse newAsyncJobResponse(AsyncJobJoinVO job) {
-        AsyncJobResponse jobResponse = new AsyncJobResponse();
+    public AsyncJobResponse newAsyncJobResponse(final AsyncJobJoinVO job) {
+        final AsyncJobResponse jobResponse = new AsyncJobResponse();
         jobResponse.setAccountId(job.getAccountUuid());
         jobResponse.setUserId(job.getUserUuid());
         jobResponse.setCmd(job.getCmd());
         jobResponse.setCreated(job.getCreated());
+        jobResponse.setRemoved(job.getRemoved());
         jobResponse.setJobId(job.getUuid());
         jobResponse.setJobStatus(job.getStatus());
         jobResponse.setJobProcStatus(job.getProcessStatus());
@@ -68,15 +69,15 @@ public class AsyncJobJoinDaoImpl extends GenericDaoBase<AsyncJobJoinVO, Long> im
         }
         jobResponse.setJobResultCode(job.getResultCode());
 
-        boolean savedValue = SerializationContext.current().getUuidTranslation();
+        final boolean savedValue = SerializationContext.current().getUuidTranslation();
         SerializationContext.current().setUuidTranslation(false);
 
-        Object resultObject = ApiSerializerHelper.fromSerializedString(job.getResult());
+        final Object resultObject = ApiSerializerHelper.fromSerializedString(job.getResult());
         jobResponse.setJobResult((ResponseObject)resultObject);
         SerializationContext.current().setUuidTranslation(savedValue);
 
         if (resultObject != null) {
-            Class<?> clz = resultObject.getClass();
+            final Class<?> clz = resultObject.getClass();
             if (clz.isPrimitive() || clz.getSuperclass() == Number.class || clz == String.class || clz == Date.class) {
                 jobResponse.setJobResultType("text");
             } else {
diff --git a/server/src/test/java/com/cloud/storage/dao/AsyncJobJoinDaoTest.java b/server/src/test/java/com/cloud/storage/dao/AsyncJobJoinDaoTest.java
new file mode 100644
index 0000000..ed93698
--- /dev/null
+++ b/server/src/test/java/com/cloud/storage/dao/AsyncJobJoinDaoTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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 com.cloud.storage.dao;
+
+import com.cloud.api.query.dao.AsyncJobJoinDaoImpl;
+import com.cloud.api.query.vo.AsyncJobJoinVO;
+import org.apache.cloudstack.api.ApiCommandJobType;
+import org.apache.cloudstack.api.response.AsyncJobResponse;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.Date;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AsyncJobJoinDaoTest {
+
+    @InjectMocks
+    AsyncJobJoinDaoImpl dao;
+
+    @Test
+    public void testNewAsyncJobResponseValidValues() {
+        final AsyncJobJoinVO job = new AsyncJobJoinVO();
+        ReflectionTestUtils.setField(job,"uuid","a2b22932-1b61-4406-8e89-4ae19968e8d3");
+        ReflectionTestUtils.setField(job,"accountUuid","4dea2836-72cc-11e8-b2de-107b4429825a");
+        ReflectionTestUtils.setField(job,"domainUuid","4dea136b-72cc-11e8-b2de-107b4429825a");
+        ReflectionTestUtils.setField(job,"userUuid","4decc724-72cc-11e8-b2de-107b4429825a");
+        ReflectionTestUtils.setField(job,"cmd","org.apache.cloudstack.api.command.admin.vm.StartVMCmdByAdmin");
+        ReflectionTestUtils.setField(job,"status",0);
+        ReflectionTestUtils.setField(job,"resultCode",0);
+        ReflectionTestUtils.setField(job,"result",null);
+        ReflectionTestUtils.setField(job,"created",new Date());
+        ReflectionTestUtils.setField(job,"removed",new Date());
+        ReflectionTestUtils.setField(job,"instanceType",ApiCommandJobType.VirtualMachine);
+        ReflectionTestUtils.setField(job,"instanceId",3L);
+        final AsyncJobResponse response = dao.newAsyncJobResponse(job);
+        Assert.assertEquals(job.getUuid(),response.getJobId());
+        Assert.assertEquals(job.getAccountUuid(), ReflectionTestUtils.getField(response, "accountId"));
+        Assert.assertEquals(job.getUserUuid(), ReflectionTestUtils.getField(response, "userId"));
+        Assert.assertEquals(job.getCmd(), ReflectionTestUtils.getField(response, "cmd"));
+        Assert.assertEquals(job.getStatus(), ReflectionTestUtils.getField(response, "jobStatus"));
+        Assert.assertEquals(job.getStatus(), ReflectionTestUtils.getField(response, "jobProcStatus"));
+        Assert.assertEquals(job.getResultCode(), ReflectionTestUtils.getField(response, "jobResultCode"));
+        Assert.assertEquals(null, ReflectionTestUtils.getField(response, "jobResultType"));
+        Assert.assertEquals(job.getResult(), ReflectionTestUtils.getField(response, "jobResult"));
+        Assert.assertEquals(job.getInstanceType().toString(), ReflectionTestUtils.getField(response, "jobInstanceType"));
+        Assert.assertEquals(job.getInstanceUuid(), ReflectionTestUtils.getField(response, "jobInstanceId"));
+        Assert.assertEquals(job.getCreated(), ReflectionTestUtils.getField(response, "created"));
+        Assert.assertEquals(job.getRemoved(), ReflectionTestUtils.getField(response, "removed"));
+    }
+
+    @Test
+    public void testNewAsyncJobResponseNullValues() {
+        final AsyncJobJoinVO job = new AsyncJobJoinVO();
+        final AsyncJobResponse response = dao.newAsyncJobResponse(job);
+        Assert.assertEquals(job.getUuid(),response.getJobId());
+        Assert.assertEquals(job.getAccountUuid(), ReflectionTestUtils.getField(response, "accountId"));
+        Assert.assertEquals(job.getUserUuid(), ReflectionTestUtils.getField(response, "userId"));
+        Assert.assertEquals(job.getCmd(), ReflectionTestUtils.getField(response, "cmd"));
+        Assert.assertEquals(job.getStatus(), ReflectionTestUtils.getField(response, "jobStatus"));
+        Assert.assertEquals(job.getStatus(), ReflectionTestUtils.getField(response, "jobProcStatus"));
+        Assert.assertEquals(job.getResultCode(), ReflectionTestUtils.getField(response, "jobResultCode"));
+        Assert.assertEquals(null, ReflectionTestUtils.getField(response, "jobResultType"));
+        Assert.assertEquals(job.getResult(), ReflectionTestUtils.getField(response, "jobResult"));
+        Assert.assertEquals(job.getInstanceType(), ReflectionTestUtils.getField(response, "jobInstanceType"));
+        Assert.assertEquals(job.getInstanceUuid(), ReflectionTestUtils.getField(response, "jobInstanceId"));
+        Assert.assertEquals(job.getCreated(), ReflectionTestUtils.getField(response, "created"));
+        Assert.assertEquals(job.getRemoved(), ReflectionTestUtils.getField(response, "removed"));
+    }
+}
diff --git a/test/integration/smoke/test_async_job.py b/test/integration/smoke/test_async_job.py
new file mode 100644
index 0000000..f727dd8
--- /dev/null
+++ b/test/integration/smoke/test_async_job.py
@@ -0,0 +1,135 @@
+# 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.
+
+from nose.plugins.attrib import attr
+from marvin.cloudstackTestCase import cloudstackTestCase
+from marvin.lib.utils import cleanup_resources
+from marvin.lib.base import ServiceOffering, DiskOffering, Account, VirtualMachine,\
+    queryAsyncJobResult, PASS
+from marvin.lib.common import get_domain, get_zone, get_test_template
+from pytz import timezone
+
+
+class TestAsyncJob(cloudstackTestCase):
+    """
+    Test queryAsyncJobResult
+    """
+    @classmethod
+    def setUpClass(cls):
+        cls.testClient = super(TestAsyncJob, cls).getClsTestClient()
+        cls.api_client = cls.testClient.getApiClient()
+
+        cls.testdata = cls.testClient.getParsedTestDataConfig()
+        # Get Zone, Domain and templates
+        cls.domain = get_domain(cls.api_client)
+        cls.zone = get_zone(cls.api_client, cls.testClient.getZoneForTests())
+        cls.hypervisor = cls.testClient.getHypervisorInfo()
+
+        cls.template = get_test_template(
+            cls.api_client,
+            cls.zone.id,
+            cls.hypervisor
+        )
+
+        # Create service, disk offerings  etc
+        cls.service_offering = ServiceOffering.create(
+            cls.api_client,
+            cls.testdata["service_offering"]
+        )
+
+        cls.disk_offering = DiskOffering.create(
+            cls.api_client,
+            cls.testdata["disk_offering"]
+        )
+
+        cls._cleanup = [
+            cls.service_offering,
+            cls.disk_offering
+        ]
+
+    @classmethod
+    def tearDownClass(cls):
+        try:
+            cleanup_resources(cls.api_client, cls._cleanup)
+        except Exception as exception:
+            raise Exception("Warning: Exception during cleanup : %s" % exception)
+
+    def setUp(self):
+        self.apiclient = self.testClient.getApiClient()
+        self.dbclient = self.testClient.getDbConnection()
+        self.hypervisor = self.testClient.getHypervisorInfo()
+        self.testdata["virtual_machine"]["zoneid"] = self.zone.id
+        self.testdata["virtual_machine"]["template"] = self.template.id
+        self.testdata["iso"]["zoneid"] = self.zone.id
+        self.account = Account.create(
+            self.apiclient,
+            self.testdata["account"],
+            domainid=self.domain.id
+        )
+        self.cleanup = [self.account]
+
+    def tearDown(self):
+        try:
+            self.debug("Cleaning up the resources")
+            cleanup_resources(self.apiclient, self.cleanup)
+            self.debug("Cleanup complete!")
+        except Exception as exception:
+            self.debug("Warning! Exception in tearDown: %s" % exception)
+
+    @attr(tags=["advanced", "eip", "advancedns", "basic", "sg"], required_hardware="false")
+    def test_query_async_job_result(self):
+        """
+        Test queryAsyncJobResult API for expected values
+        """
+        self.debug("Deploying instance in the account: %s" %
+                   self.account.name)
+        virtual_machine = VirtualMachine.create(
+            self.apiclient,
+            self.testdata["virtual_machine"],
+            accountid=self.account.name,
+            domainid=self.account.domainid,
+            serviceofferingid=self.service_offering.id,
+            diskofferingid=self.disk_offering.id,
+            hypervisor=self.hypervisor
+        )
+
+        response = virtual_machine.getState(
+            self.apiclient,
+            VirtualMachine.RUNNING)
+        self.assertEqual(response[0], PASS, response[1])
+
+        cmd = queryAsyncJobResult.queryAsyncJobResultCmd()
+        cmd.jobid = virtual_machine.jobid
+        cmd_response = self.apiclient.queryAsyncJobResult(cmd)
+
+        db_result = self.dbclient.execute("select * from async_job where uuid='%s'" %
+                                          virtual_machine.jobid)
+
+        # verify that 'completed' value from api equals 'removed' db column value
+        completed = cmd_response.completed
+        removed = timezone('UTC').localize(db_result[0][17])
+        removed = removed.strftime("%Y-%m-%dT%H:%M:%S%z")
+        self.assertEqual(completed, removed,
+                         "Expected 'completed' timestamp value %s to be equal to "
+                         "'removed' db column value %s." % (completed, removed))
+
+        # verify that api job_status value equals db job_status value
+        jobstatus_db = db_result[0][8]
+        jobstatus_api = cmd_response.jobstatus
+        self.assertEqual(jobstatus_api, jobstatus_db,
+                         "Expected 'jobstatus' api value %s to be equal to "
+                         "'job_status' db column value %s." % (jobstatus_api, jobstatus_db))
diff --git a/tools/marvin/setup.py b/tools/marvin/setup.py
index 23c50e1..864d856 100644
--- a/tools/marvin/setup.py
+++ b/tools/marvin/setup.py
@@ -54,7 +54,8 @@ setup(name="Marvin",
           "pyvmomi >= 5.5.0",
           "netaddr >= 0.7.14",
           "dnspython",
-          "ipmisim >= 0.7"
+          "ipmisim >= 0.7",
+          "pytz"
       ],
       extras_require={
         "nuagevsp": ["vspk", "PyYAML", "futures", "netaddr", "retries", "jpype1"]