From 304d85fdef189719855b239b290f678fc5d65ea6 Mon Sep 17 00:00:00 2001 From: dhslove <44049806+dhslove@users.noreply.github.com> Date: Tue, 26 May 2026 11:03:20 +0900 Subject: [PATCH 1/3] Show N2K MAC preservation in import UI --- ui/public/locales/en.json | 1 + ui/public/locales/ko_KR.json | 1 + ui/src/views/tools/ImportUnmanagedInstance.vue | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index f4e7c9fae951..41b8d5ad5d96 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -5564,6 +5564,7 @@ "message.skip.tls.verify.nutanix": "Allow connections to Nutanix Prism endpoints that use self-signed certificates.", "message.nutanix.prism.endpoint.tooltip": "Enter the Nutanix Prism Central or Prism Element endpoint, for example https://10.10.132.100:9440.", "message.select.ablestack.n2k.primary.storage.migration": "Select the ABLESTACK primary storage target for N2K migration. If no pool is selected, Cloud chooses a compatible default.", + "message.ablestack.n2k.preserve.source.mac": "During Phase2 cutover, the source VM is shut down and the first source NIC MAC address is preserved on the Cloud VM default NIC.", "message.n2k.snapshot.retention.days": "Nutanix recovery points used for phase split migration are retained for this period. Default is 14 days.", "message.select.kvm.host.ablestack.import": "Select the ABLESTACK host that will run the import worker.", "message.select.primary.storage.ablestack.import": "Select the target primary storage for the imported VM disks.", diff --git a/ui/public/locales/ko_KR.json b/ui/public/locales/ko_KR.json index 744f7841cc87..d3b19f84e603 100644 --- a/ui/public/locales/ko_KR.json +++ b/ui/public/locales/ko_KR.json @@ -5564,6 +5564,7 @@ "message.skip.tls.verify.nutanix": "자체 서명 인증서를 사용하는 Nutanix Prism 엔드포인트 접속을 허용합니다.", "message.nutanix.prism.endpoint.tooltip": "Nutanix Prism Central 또는 Prism Element 엔드포인트를 입력합니다. 예: https://10.10.132.100:9440", "message.select.ablestack.n2k.primary.storage.migration": "N2K 마이그레이션에 사용할 ABLESTACK 기본 스토리지 대상을 선택합니다. 풀을 선택하지 않으면 Cloud가 호환 가능한 기본값을 선택합니다.", + "message.ablestack.n2k.preserve.source.mac": "Phase2 cutover에서 원본 VM을 종료한 뒤 첫 번째 NIC의 MAC 주소를 Cloud VM 기본 NIC에 유지해 배포합니다.", "message.n2k.snapshot.retention.days": "Phase 분리 이관에 사용하는 Nutanix Recovery Point를 지정한 기간 동안 보존합니다. 기본값은 14일입니다.", "message.select.kvm.host.ablestack.import": "Import 작업자를 실행할 ABLESTACK 호스트를 선택합니다.", "message.select.primary.storage.ablestack.import": "가져온 VM 디스크를 배치할 대상 기본 스토리지를 선택합니다.", diff --git a/ui/src/views/tools/ImportUnmanagedInstance.vue b/ui/src/views/tools/ImportUnmanagedInstance.vue index 4dc184f9c951..0c2cd161f9c3 100644 --- a/ui/src/views/tools/ImportUnmanagedInstance.vue +++ b/ui/src/views/tools/ImportUnmanagedInstance.vue @@ -474,6 +474,12 @@ {{ $t('message.ip.address.changes.effect.after.vm.restart') }} + From 134812cdacc9c015b18a546925a59669d4f665de Mon Sep 17 00:00:00 2001 From: dhslove <44049806+dhslove@users.noreply.github.com> Date: Tue, 26 May 2026 12:26:45 +0900 Subject: [PATCH 2/3] Add N2K target start policy for Cloud imports --- ...rtUnmanagedInstanceForAblestackN2KCmd.java | 13 +++++++ .../AblestackN2KConvertInstanceCommand.java | 9 +++++ ...k_n2k_cloud_cutover_start_policy_design.md | 34 +++++++++++++++++++ ...stackN2KConvertInstanceCommandWrapper.java | 2 +- ...kN2KConvertInstanceCommandWrapperTest.java | 20 +++++++++++ .../vm/UnmanagedVMsManagerImpl.java | 33 ++++++++++++++---- ui/public/locales/en.json | 7 +++- ui/public/locales/ko_KR.json | 7 +++- .../views/tools/ImportUnmanagedInstance.vue | 11 ++++++ ui/src/views/tools/ImportVmTasks.vue | 12 +++++++ ui/src/views/tools/ManageInstances.vue | 3 ++ 11 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 docs/ablestack_n2k_cloud_cutover_start_policy_design.md 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..c054adba6fc0 --- /dev/null +++ b/docs/ablestack_n2k_cloud_cutover_start_policy_design.md @@ -0,0 +1,34 @@ +# 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