diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceForAblestackN2KCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceForAblestackN2KCmd.java index fd8ba5e8fd5d..897d34281ea3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceForAblestackN2KCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceForAblestackN2KCmd.java @@ -75,6 +75,11 @@ public class ImportUnmanagedInstanceForAblestackN2KCmd extends ImportVmCmd { description = "source snapshot/recovery point retention time in seconds for ablestack-n2k. Default is 1209600 seconds (14 days)") private Long retentionSeconds; + @Parameter(name = "starttargetvm", + type = CommandType.BOOLEAN, + description = "start the imported Cloud target VM after ablestack-n2k phase2 cutover. Default is true") + private Boolean startTargetVm; + public String getSplitMode() { return StringUtils.defaultIfBlank(splitMode, DEFAULT_SPLIT_MODE); } @@ -103,6 +108,14 @@ public long getRetentionSeconds() { return retentionSeconds != null ? retentionSeconds : DEFAULT_RETENTION_SECONDS; } + public Boolean getRequestedStartTargetVm() { + return startTargetVm; + } + + public boolean isStartTargetVm() { + return BooleanUtils.toBooleanDefaultIfNull(startTargetVm, true); + } + @Override public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { diff --git a/core/src/main/java/com/cloud/agent/api/AblestackN2KConvertInstanceCommand.java b/core/src/main/java/com/cloud/agent/api/AblestackN2KConvertInstanceCommand.java index a311eac94ac5..f068d7b607ec 100644 --- a/core/src/main/java/com/cloud/agent/api/AblestackN2KConvertInstanceCommand.java +++ b/core/src/main/java/com/cloud/agent/api/AblestackN2KConvertInstanceCommand.java @@ -51,6 +51,7 @@ public class AblestackN2KConvertInstanceCommand extends Command { private String cloudDisplayName; private String cloudCpuSpeed; private Long retentionSeconds; + private boolean startTargetVm = true; private boolean resume; public AblestackN2KConvertInstanceCommand() { @@ -282,6 +283,14 @@ public void setRetentionSeconds(Long retentionSeconds) { this.retentionSeconds = retentionSeconds; } + public boolean isStartTargetVm() { + return startTargetVm; + } + + public void setStartTargetVm(boolean startTargetVm) { + this.startTargetVm = startTargetVm; + } + public boolean isResume() { return resume; } diff --git a/docs/ablestack_n2k_cloud_cutover_start_policy_design.md b/docs/ablestack_n2k_cloud_cutover_start_policy_design.md new file mode 100644 index 000000000000..b4efbaee1879 --- /dev/null +++ b/docs/ablestack_n2k_cloud_cutover_start_policy_design.md @@ -0,0 +1,53 @@ + + +# ABLESTACK-N2K Cloud Cutover Start Policy Design + +## 배경 + +`ablestack_n2k wizard --apply` 또는 `ablestack_n2k run --apply`는 Cloud 대상 VM을 생성하되 시작하지 않는다. 반면 Cloud UI/API를 통해 실행하는 N2K 경로는 KVM agent wrapper가 항상 `--start`를 추가했기 때문에, 사용자가 cutover까지 완료한 뒤 대상 VM을 정지 상태로 남기는 정책을 선택할 수 없었다. + +## 설계 + +### API + +`importUnmanagedInstanceForAblestackN2K`에 `starttargetvm` Boolean 파라미터를 추가한다. 기본값은 기존 동작과 동일하게 `true`이다. + +- `true`: Phase2 cutover 이후 Cloud 대상 VM을 시작한다. wrapper는 `--start`를 전달한다. +- `false`: Phase2 cutover 이후 Cloud 대상 VM을 정지 상태로 둔다. wrapper는 `--apply`만 전달한다. + +### 작업 컨텍스트 + +Phase1에서 선택한 정책은 import VM task의 source context JSON에 `startTargetVm`으로 저장한다. Phase2, resume, retry 요청에서 `starttargetvm`이 명시되지 않으면 저장된 값을 재사용한다. 저장값도 없으면 하위 호환을 위해 `true`로 처리한다. + +### Agent wrapper + +Cloud target provider(`ablestack-cloud`)인 경우 기존의 무조건 `--start` 호출을 `cmd.isStartTargetVm()`에 따라 분기한다. CLI/wizard의 기존 `--apply`/`--start` 의미는 변경하지 않는다. + +### UI + +초기 가져오기 대화상자의 N2K 섹션에 대상 VM 시작 여부 스위치를 추가한다. 기본값은 시작이다. Phase2 실행 모달에는 기존 설정 유지, 시작, 정지 유지 중 선택할 수 있는 드롭다운을 제공한다. 기본값은 기존 설정 유지로 두어 Phase1에서 저장한 정책을 보존한다. + +## 빌드/배포 + +Cloud는 릴리즈 빌드가 아니라 Maven module 빌드와 UI module 빌드만 수행한다. 22번 공유 개발 환경에는 변경된 management/backend jar, agent jar, UI 정적 파일만 반영한다. + +## DB 변경 + +신규 테이블/칼럼은 없다. 기존 import VM task source context JSON에 `startTargetVm` 키만 추가로 저장한다. diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtAblestackN2KConvertInstanceCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtAblestackN2KConvertInstanceCommandWrapper.java index b4663d1a61f5..65962ccbe49f 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtAblestackN2KConvertInstanceCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtAblestackN2KConvertInstanceCommandWrapper.java @@ -150,7 +150,7 @@ public Answer execute(AblestackN2KConvertInstanceCommand cmd, LibvirtComputingRe script.add("--target-map-json", cmd.getTargetMapJson()); } if (StringUtils.equals(targetProvider, CLOUD_TARGET_PROVIDER)) { - script.add("--start"); + script.add(cmd.isStartTargetVm() ? "--start" : "--apply"); script.add("--cloud-cred-file", cloudCredentialFile.toString()); addIfNotBlank(script, "--cloud-zone-id", cmd.getCloudZoneId()); addIfNotBlank(script, "--cloud-service-offering-id", cmd.getCloudServiceOfferingId()); diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtAblestackN2KConvertInstanceCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtAblestackN2KConvertInstanceCommandWrapperTest.java index a812a66b219c..29934d6fd850 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtAblestackN2KConvertInstanceCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtAblestackN2KConvertInstanceCommandWrapperTest.java @@ -122,6 +122,7 @@ public void executeBuildsAblestackN2KRunCommandWithoutPassingPlainCredentialsAsA Mockito.verify(script).add("--cleanup-source-points"); Mockito.verify(script).add("--target-map-json", "{\"scsi0:0\":\"rbd:rbd/rhel-disk0\"}"); Mockito.verify(script).add("--start"); + Mockito.verify(script, Mockito.never()).add("--apply"); Mockito.verify(script).add(Mockito.eq("--cloud-cred-file"), Mockito.anyString()); Mockito.verify(script).add("--cloud-zone-id", "zone-uuid"); Mockito.verify(script).add("--cloud-service-offering-id", "service-offering-uuid"); @@ -131,6 +132,25 @@ public void executeBuildsAblestackN2KRunCommandWithoutPassingPlainCredentialsAsA } } + @Test + public void executeUsesApplyWithoutStartWhenCloudTargetVmShouldRemainStopped() throws IOException { + String workdir = temporaryFolder.newFolder("rhel-stopped").getAbsolutePath(); + AblestackN2KConvertInstanceCommand cmd = validCommand(workdir); + cmd.setStartTargetVm(false); + + try (MockedConstruction