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