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
This commit is contained in:
ernjvr 2018-07-25 11:18:01 +02:00 committed by Rohit Yadav
parent 443490179c
commit 542d4da16c
14 changed files with 286 additions and 29 deletions

View File

@ -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

View File

@ -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";

View File

@ -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;
}
}

View File

@ -68,6 +68,8 @@ public interface JobInfo extends Identity, InternalIdentity {
Date getCreated();
Date getRemoved();
Date getLastUpdated();
Date getLastPolled();

View File

@ -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;
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;

View File

@ -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);

View File

@ -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);
}

View File

@ -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);

View File

@ -372,6 +372,15 @@ public class AsyncJobVO implements AsyncJob, JobInfo {
this.uuid = uuid;
}
@Override
public Date getRemoved() {
return removed;
}
public void setRemoved(final Date removed) {
this.removed = removed;
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
@ -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();
}

View File

@ -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())) {

View File

@ -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 {

View File

@ -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"));
}
}

View File

@ -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))

View File

@ -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"]