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 27b17d96b8fe..9747c1c99756 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -532,7 +532,6 @@ public class ApiConstants { public static final String SHOW_CAPACITIES = "showcapacities"; public static final String SHOW_REMOVED = "showremoved"; public static final String SHOW_RESOURCE_ICON = "showicon"; - public static final String SHOW_COMPLETED = "showcompleted"; public static final String SHOW_INACTIVE = "showinactive"; public static final String SHOW_UNIQUE = "showunique"; public static final String SIGNATURE = "signature"; @@ -582,6 +581,7 @@ public class ApiConstants { public static final String SUITABLE_FOR_VM = "suitableforvirtualmachine"; public static final String SUPPORTS_STORAGE_SNAPSHOT = "supportsstoragesnapshot"; public static final String TARGET_IQN = "targetiqn"; + public static final String TASKS_FILTER = "tasksfilter"; public static final String TEMPLATE_FILTER = "templatefilter"; public static final String TEMPLATE_ID = "templateid"; public static final String TEMPLATE_IDS = "templateids"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListImportVMTasksCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListImportVMTasksCmd.java index a0c8cb57ee61..94b547ff4267 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListImportVMTasksCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListImportVMTasksCmd.java @@ -75,11 +75,8 @@ public class ListImportVMTasksCmd extends BaseListCmd { description = "Conversion host of the importing task") private Long convertHostId; - @Parameter(name = ApiConstants.LIST_ALL, type = CommandType.BOOLEAN, description = "Whether to list all import tasks.") - private boolean listAll = false; - - @Parameter(name = ApiConstants.SHOW_COMPLETED, type = CommandType.BOOLEAN, description = "Whether to list completed tasks.") - private boolean showCompleted = false; + @Parameter(name = ApiConstants.TASKS_FILTER, type = CommandType.STRING, description = "Filter tasks by state, valid options are: All, Running, Completed, Failed") + private String tasksFilter; public Long getZoneId() { return zoneId; @@ -97,12 +94,8 @@ public Long getConvertHostId() { return convertHostId; } - public boolean isListAll() { - return listAll; - } - - public boolean isShowCompleted() { - return showCompleted; + public String getTasksFilter() { + return tasksFilter; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ImportVMTaskResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ImportVMTaskResponse.java index dd49127f1bd7..aa85554f5678 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ImportVMTaskResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ImportVMTaskResponse.java @@ -55,6 +55,10 @@ public class ImportVMTaskResponse extends BaseResponse { @Param(description = "the display name of the importing VM") private String displayName; + @SerializedName(ApiConstants.STATE) + @Param(description = "the state of the importing VM task") + private String state; + @SerializedName(ApiConstants.VCENTER) @Param(description = "the vcenter name of the importing VM task") private String vcenter; @@ -242,4 +246,12 @@ public Date getLastUpdated() { public void setLastUpdated(Date lastUpdated) { this.lastUpdated = lastUpdated; } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } } diff --git a/api/src/main/java/org/apache/cloudstack/vm/ImportVmTask.java b/api/src/main/java/org/apache/cloudstack/vm/ImportVmTask.java index 96d7cd346f31..5bb51dfcbcb9 100644 --- a/api/src/main/java/org/apache/cloudstack/vm/ImportVmTask.java +++ b/api/src/main/java/org/apache/cloudstack/vm/ImportVmTask.java @@ -23,6 +23,19 @@ public interface ImportVmTask extends Identity, InternalIdentity { enum Step { - Prepare, CloningInstance, ConvertingInstance, Importing, Cleaning, Completed + Prepare, CloningInstance, ConvertingInstance, Importing, Completed + } + + enum TaskState { + Running, Completed, Failed; + + public static TaskState getValue(String state) { + for (TaskState s : TaskState.values()) { + if (s.name().equalsIgnoreCase(state)) { + return s; + } + } + throw new IllegalArgumentException("Invalid task state: " + state); + } } } diff --git a/api/src/main/java/org/apache/cloudstack/vm/ImportVmTasksManager.java b/api/src/main/java/org/apache/cloudstack/vm/ImportVmTasksManager.java index 44eed8c531a5..233511fbce8d 100644 --- a/api/src/main/java/org/apache/cloudstack/vm/ImportVmTasksManager.java +++ b/api/src/main/java/org/apache/cloudstack/vm/ImportVmTasksManager.java @@ -34,5 +34,5 @@ ImportVmTask createImportVMTaskRecord(DataCenter zone, Account owner, long userI void updateImportVMTaskStep(ImportVmTask importVMTaskVO, DataCenter zone, Account owner, Host convertHost, Host importHost, Long vmId, ImportVmTask.Step step); - boolean removeImportVMTask(long taskId); + void updateImportVMTaskErrorState(ImportVmTask importVMTaskVO, ImportVmTask.TaskState state, String errorMsg); } diff --git a/engine/schema/src/main/java/com/cloud/vm/ImportVMTaskVO.java b/engine/schema/src/main/java/com/cloud/vm/ImportVMTaskVO.java index e472b56460a2..9a8a769f0a56 100644 --- a/engine/schema/src/main/java/com/cloud/vm/ImportVMTaskVO.java +++ b/engine/schema/src/main/java/com/cloud/vm/ImportVMTaskVO.java @@ -93,6 +93,9 @@ public ImportVMTaskVO() { @Column(name = "step") private Step step; + @Column(name = "state") + private TaskState state; + @Column(name = "description") private String description; @@ -217,6 +220,14 @@ public void setStep(Step step) { this.step = step; } + public TaskState getState() { + return state; + } + + public void setState(TaskState state) { + this.state = state; + } + public String getDescription() { return description; } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDao.java index 990dacb33fb5..a4f0a155da41 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDao.java @@ -18,12 +18,14 @@ // package com.cloud.vm.dao; +import com.cloud.utils.Pair; import com.cloud.utils.db.GenericDao; import com.cloud.vm.ImportVMTaskVO; - +import org.apache.cloudstack.vm.ImportVmTask; import java.util.List; public interface ImportVMTaskDao extends GenericDao { - List listImportVMTasks(Long zoneId, Long accountId, String vcenter, Long convertHostId, boolean showCompleted); + Pair, Integer> listImportVMTasks(Long zoneId, Long accountId, String vcenter, Long convertHostId, + ImportVmTask.TaskState state, Long startIndex, Long pageSizeVal); } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDaoImpl.java index 8bdae2e456ae..da9c391af9db 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDaoImpl.java @@ -16,10 +16,13 @@ // under the License. package com.cloud.vm.dao; +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.vm.ImportVMTaskVO; +import org.apache.cloudstack.vm.ImportVmTask; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; @@ -41,12 +44,14 @@ void init() { AllFieldsSearch.and("accountId", AllFieldsSearch.entity().getAccountId(), SearchCriteria.Op.EQ); AllFieldsSearch.and("vcenter", AllFieldsSearch.entity().getVcenter(), SearchCriteria.Op.EQ); AllFieldsSearch.and("convertHostId", AllFieldsSearch.entity().getConvertHostId(), SearchCriteria.Op.EQ); + AllFieldsSearch.and("state", AllFieldsSearch.entity().getState(), SearchCriteria.Op.EQ); AllFieldsSearch.done(); } @Override - public List listImportVMTasks(Long zoneId, Long accountId, String vcenter, Long convertHostId, boolean showCompleted) { + public Pair, Integer> listImportVMTasks(Long zoneId, Long accountId, String vcenter, Long convertHostId, + ImportVmTask.TaskState state, Long startIndex, Long pageSizeVal) { SearchCriteria sc = AllFieldsSearch.create(); if (zoneId != null) { sc.setParameters("zoneId", zoneId); @@ -60,6 +65,10 @@ public List listImportVMTasks(Long zoneId, Long accountId, Strin if (convertHostId != null) { sc.setParameters("convertHostId", convertHostId); } - return showCompleted ? listIncludingRemovedBy(sc) : listBy(sc); + if (state != null) { + sc.setParameters("state", state); + } + Filter filter = new Filter(ImportVMTaskVO.class, "created", false, startIndex, pageSizeVal); + return searchAndCount(sc, filter); } } diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql b/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql index a14a9fef8ad0..8a7b55470629 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql @@ -62,7 +62,8 @@ CREATE TABLE IF NOT EXISTS `cloud`.`import_vm_task`( `source_vm_name` varchar(255) COMMENT 'Source VM name on vCenter', `convert_host_id` bigint unsigned COMMENT 'Convert Host ID', `import_host_id` bigint unsigned COMMENT 'Import Host ID', - `step` varchar(20) NOT NULL COMMENT 'Importing VM Task Step', + `step` varchar(20) COMMENT 'Importing VM Task Step', + `state` varchar(20) COMMENT 'Importing VM Task State', `description` varchar(255) COMMENT 'Importing VM Task Description', `duration` bigint unsigned COMMENT 'Duration in milliseconds for the completed tasks', `created` datetime NOT NULL COMMENT 'date created', diff --git a/server/src/main/java/org/apache/cloudstack/vm/ImportVmTasksManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/ImportVmTasksManagerImpl.java index fa36627e8649..88b8f0f425d4 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/ImportVmTasksManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/ImportVmTasksManagerImpl.java @@ -25,13 +25,17 @@ import com.cloud.user.Account; import com.cloud.user.AccountService; import com.cloud.utils.DateUtil; +import com.cloud.utils.Pair; import com.cloud.vm.ImportVMTaskVO; import com.cloud.vm.UserVmVO; import com.cloud.vm.dao.ImportVMTaskDao; import com.cloud.vm.dao.UserVmDao; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.admin.vm.ListImportVMTasksCmd; import org.apache.cloudstack.api.response.ImportVMTaskResponse; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -72,25 +76,33 @@ public ListResponse listImportVMTasks(ListImportVMTasksCmd Long accountId = cmd.getAccountId(); String vcenter = cmd.getVcenter(); Long convertHostId = cmd.getConvertHostId(); - boolean listAll = cmd.isListAll(); - boolean showCompleted = cmd.isShowCompleted(); + Long startIndex = cmd.getStartIndex(); + Long pageSizeVal = cmd.getPageSizeVal(); - List tasks; - if (listAll) { - tasks = importVMTaskDao.listAll(); - } else { - tasks = importVMTaskDao.listImportVMTasks(zoneId, accountId, vcenter, convertHostId, showCompleted); - } + ImportVmTask.TaskState state = getStateFromFilter(cmd.getTasksFilter()); + Pair, Integer> result = importVMTaskDao.listImportVMTasks(zoneId, accountId, vcenter, convertHostId, state, startIndex, pageSizeVal); + List tasks = result.first(); List responses = new ArrayList<>(); for (ImportVMTaskVO task : tasks) { responses.add(createImportVMTaskResponse(task)); } ListResponse listResponses = new ListResponse<>(); - listResponses.setResponses(responses, responses.size()); + listResponses.setResponses(responses, result.second()); return listResponses; } + private ImportVmTask.TaskState getStateFromFilter(String tasksFilter) { + if (StringUtils.isBlank(tasksFilter) || tasksFilter.equalsIgnoreCase("all")) { + return null; + } + try { + return ImportVmTask.TaskState.getValue(tasksFilter); + } catch (IllegalArgumentException e) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Invalid value for task state: %s", tasksFilter)); + } + } + @Override public ImportVmTask createImportVMTaskRecord(DataCenter zone, Account owner, long userId, String displayName, String vcenter, String datacenterName, String sourceVMName, Host convertHost, Host importHost) { logger.debug("Creating import VM task entry for VM: {} for account {} on zone {} " + @@ -98,6 +110,7 @@ public ImportVmTask createImportVMTaskRecord(DataCenter zone, Account owner, lon sourceVMName, owner.getAccountName(), zone.getName(), displayName, vcenter, datacenterName); ImportVMTaskVO importVMTaskVO = new ImportVMTaskVO(zone.getId(), owner.getAccountId(), userId, displayName, vcenter, datacenterName, sourceVMName, convertHost.getId(), importHost.getId()); + importVMTaskVO.setState(ImportVmTask.TaskState.Running); return importVMTaskDao.persist(importVMTaskVO); } @@ -111,7 +124,6 @@ private String getStepDescription(ImportVMTaskVO importVMTaskVO, Host convertHos if (Completed == step) { stringBuilder.append("Completed at ").append(DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), updatedDate)); } else { - stringBuilder.append(String.format("[%s] ", DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), updatedDate))); if (CloningInstance == step) { stringBuilder.append(String.format("Cloning source instance: %s on vCenter: %s / datacenter: %s", sourceVMName, vcenter, datacenter)); } else if (ConvertingInstance == step) { @@ -142,13 +154,19 @@ public void updateImportVMTaskStep(ImportVmTask importVMTask, DataCenter zone, A Duration duration = Duration.between(importVMTaskVO.getCreated().toInstant(), updatedDate.toInstant()); importVMTaskVO.setDuration(duration.toMillis()); importVMTaskVO.setVmId(vmId); + importVMTaskVO.setState(ImportVmTask.TaskState.Completed); } importVMTaskDao.update(importVMTaskVO.getId(), importVMTaskVO); } @Override - public boolean removeImportVMTask(long taskId) { - return importVMTaskDao.remove(taskId); + public void updateImportVMTaskErrorState(ImportVmTask importVMTask, ImportVmTask.TaskState state, String errorMsg) { + ImportVMTaskVO importVMTaskVO = (ImportVMTaskVO) importVMTask; + Date updatedDate = DateUtil.now(); + importVMTaskVO.setUpdated(updatedDate); + importVMTaskVO.setState(state); + importVMTaskVO.setDescription(errorMsg); + importVMTaskDao.update(importVMTaskVO.getId(), importVMTaskVO); } private ImportVMTaskResponse createImportVMTaskResponse(ImportVMTaskVO task) { @@ -169,19 +187,21 @@ private ImportVMTaskResponse createImportVMTaskResponse(ImportVMTaskVO task) { response.setDisplayName(task.getDisplayName()); response.setStep(getStepDisplayField(task.getStep())); response.setDescription(task.getDescription()); + response.setState(task.getState().name()); Date updated = task.getUpdated(); Date currentDate = new Date(); - if (updated != null && Completed != task.getStep()) { - Duration stepDuration = Duration.between(updated.toInstant(), currentDate.toInstant()); - response.setStepDuration(getDurationDisplay(stepDuration.toMillis())); - } - if (Completed == task.getStep()) { - response.setStepDuration(getDurationDisplay(task.getDuration())); - } else { - Duration totalDuration = Duration.between(task.getCreated().toInstant(), currentDate.toInstant()); - response.setDuration(getDurationDisplay(totalDuration.toMillis())); + + if (updated != null) { + if (ImportVmTask.TaskState.Running == task.getState()) { + Duration stepDuration = Duration.between(updated.toInstant(), currentDate.toInstant()); + response.setStepDuration(getDurationDisplay(stepDuration.toMillis())); + } else { + Duration totalDuration = Duration.between(task.getCreated().toInstant(), updated.toInstant()); + response.setDuration(getDurationDisplay(totalDuration.toMillis())); + } } + HostVO host = hostDao.findById(task.getConvertHostId()); if (host != null) { response.setConvertInstanceHostId(host.getUuid()); diff --git a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java index 51623b56ec8a..ebf73cb3ddb5 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -1779,10 +1779,10 @@ protected UserVm importUnmanagedInstanceFromVmwareToKvm(DataCenter zone, Cluster logger.debug(String.format("VMware VM %s imported successfully to CloudStack instance %s (%s), Time taken: %d secs, OVF files imported from %s, Source VMware VM details - OS: %s, PowerState: %s, Disks: %s, NICs: %s", sourceVMName, displayName, displayName, timeElapsedInSecs, (ovfTemplateOnConvertLocation != null)? "MS" : "KVM Host", sourceVMwareInstance.getOperatingSystem(), sourceVMwareInstance.getPowerState(), sourceVMwareInstance.getDisks(), sourceVMwareInstance.getNics())); importVmTasksManager.updateImportVMTaskStep(importVMTask, zone, owner, convertHost, importHost, userVm.getId(), Completed); - importVmTasksManager.removeImportVMTask(importVMTask.getId()); return userVm; } catch (CloudRuntimeException e) { logger.error(String.format("Error importing VM: %s", e.getMessage()), e); + importVmTasksManager.updateImportVMTaskErrorState(importVMTask, ImportVmTask.TaskState.Failed, e.getMessage()); ActionEventUtils.onCompletedActionEvent(userId, owner.getId(), EventVO.LEVEL_ERROR, EventTypes.EVENT_VM_IMPORT, cmd.getEventDescription(), null, null, 0); throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); diff --git a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java index 465cea3c74af..60ce662b4f37 100644 --- a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java @@ -782,7 +782,6 @@ private void baseTestImportVmFromVmwareToKvm(VcenterParameter vcenterParameter, when(agentManager.send(Mockito.eq(convertHostId), Mockito.any(CheckConvertInstanceCommand.class))).thenReturn(checkConvertInstanceAnswer); } - when(importVMTaskVO.getId()).thenReturn(1L); when(importVmTasksManager.createImportVMTaskRecord(any(DataCenter.class), any(Account.class), anyLong(), anyString(), anyString(), anyString(), anyString(), any(Host.class), any(Host.class))).thenReturn(importVMTaskVO); when(volumeApiService.doesStoragePoolSupportDiskOffering(any(StoragePool.class), any(DiskOffering.class))).thenReturn(true); diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 5f1621c334e5..c9c343c1181f 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -635,6 +635,7 @@ "label.continue": "Continue", "label.continue.install": "Continue with installation", "label.controlnodes": "Control nodes", +"label.conversionhost": "Conversion Host", "label.copied.clipboard": "Copied to clipboard", "label.copy": "Copy", "label.copy.clipboard": "Copy to clipboard", @@ -699,6 +700,8 @@ "label.crosszones": "Cross Zones", "label.currency": "Currency", "label.current": "Current", +"label.currentstep": "Current step", +"label.currentstep.duration": "Current step duration", "label.current.storage": "Current storage", "label.currentpassword": "Current password", "label.custom": "Custom", @@ -1244,6 +1247,7 @@ "label.import.instance": "Import Instance", "label.import.offering": "Import Offering", "label.import.role": "Import Role", +"label.import.vm.tasks": "Import VM Tasks", "label.import.volume": "Import Volume", "label.inactive": "Inactive", "label.inbuilt": "Inbuilt", @@ -2312,6 +2316,7 @@ "label.sourcenattype": "Supported source NAT type", "label.sourceport": "Source port", "label.sourcetype": "Source type", +"label.sourcevmname": "Source VM Name", "label.specifyasnumber": "Specify AS Number", "label.specifyipranges": "Specify IP ranges", "label.specifyvlan": "Specify VLAN", @@ -2879,6 +2884,7 @@ "label.leaseexpiryaction": "Lease expiry action", "label.remainingdays": "Lease", "label.leased": "Leased", +"label.totalduration": "Total duration", "label.usestoragereplication": "Use primary storage replication", "message.acquire.ip.failed": "Failed to acquire IP.", "message.action.acquire.ip": "Please confirm that you want to acquire new IP.", @@ -3520,6 +3526,7 @@ "message.host.dedication.released": "Host dedication released.", "message.host.external.datadisk": "Usage of data disks for the selected template is not applicable", "message.import.running.instance.warning": "The selected VM is powered-on on the VMware Datacenter. The recommended state to convert a VMware VM into KVM is powered-off after a graceful shutdown of the guest OS.", +"message.import.vm.tasks": "Import from VMware to KVM tasks", "message.import.volume": "Please specify the domain, account or project name.
If not set, the volume will be imported for the caller.", "message.info.cloudian.console": "Cloudian Management Console should open in another window.", "message.installwizard.cloudstack.helptext.website": " * Project website:\t ", diff --git a/ui/src/views/tools/ImportVmTasks.vue b/ui/src/views/tools/ImportVmTasks.vue index 51741f18e699..37725337947e 100644 --- a/ui/src/views/tools/ImportVmTasks.vue +++ b/ui/src/views/tools/ImportVmTasks.vue @@ -16,56 +16,83 @@ // under the License.