Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
}
Expand Down
53 changes: 53 additions & 0 deletions docs/ablestack_n2k_cloud_cutover_start_policy_design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<!--
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.
-->

# 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` 키만 추가로 저장한다.
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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<Script> ignored = Mockito.mockConstruction(Script.class, (mock, context) -> {
Mockito.when(mock.execute(Mockito.any(OutputInterpreter.class))).thenReturn("");
Mockito.when(mock.getExitValue()).thenReturn(0);
})) {
Answer answer = wrapper.execute(cmd, libvirtComputingResource);

Assert.assertTrue(answer.getResult());
Script script = ignored.constructed().get(0);
Mockito.verify(script).add("--apply");
Mockito.verify(script, Mockito.never()).add("--start");
}
}

private AblestackN2KConvertInstanceCommand validCommand() {
return validCommand("/work/rhel");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4776,8 +4776,10 @@ private void startNutanixToKvmConversionWithAblestackN2K(DataCenter zone, Cluste
Map<String, String> sourceContext = new LinkedHashMap<>();
sourceContext.put("sourceApi", cmd.getSourceApi());
sourceContext.put("resolvedSourceApi", inventory.getSourceApi());
boolean startTargetVm = getAblestackN2KStartTargetVm(cmd, sourceContext);
sourceContext.put("insecure", Boolean.toString(cmd.isInsecure()));
sourceContext.put("retentionSeconds", Long.toString(retentionSeconds));
sourceContext.put("startTargetVm", Boolean.toString(startTargetVm));
importVmTasksManager.updateImportVMTaskN2KContext(importVMTask, destinationCluster.getId(), serviceOffering.getId(),
targetStoragePool.getId(), cmd.getHost(), cmd.getSourceApi(), gson.toJson(sourceNutanixInstance),
serviceOfferingDetails, buildNicSelectionMap(nicNetworkMap, nicIpAddressMap), targetStoragePlan.getTargetProfile(),
Expand All @@ -4787,7 +4789,7 @@ serviceOfferingDetails, buildNicSelectionMap(nicNetworkMap, nicIpAddressMap), ta
startNutanixInstanceToKVMUsingAblestackN2K(sourceVMName, convertHost, targetStorageLocation, cmd.getHost(),
cmd.getUsername(), cmd.getPassword(), cmd.getSplitMode(), cmd.getSourceApi(), cmd.isInsecure(), workdir,
nfsHost, targetStoragePlan, persistedImportTask, zone, owner, serviceOffering, targetStoragePool, nicNetworkMap,
hostName, displayName, retentionSeconds);
hostName, displayName, retentionSeconds, false, startTargetVm);
if (StringUtils.equalsIgnoreCase(cmd.getSplitMode(), "full")) {
AblestackN2KStatusAnswer status = refreshImportVMTaskWithAblestackN2KStatus(persistedImportTask, convertHost);
importVmTasksManager.updateImportVMTaskV2KStep(persistedImportTask, ImportVmTask.V2KStep.Phase2_Completed);
Expand Down Expand Up @@ -4830,7 +4832,9 @@ protected void continueAblestackN2KVmImport(ImportUnmanagedInstanceForAblestackN
String targetVmName = StringUtils.defaultIfBlank(task.getTargetVMName(), task.getDisplayName());
Map<String, String> sourceContext = getTaskSourceContextMap(task);
long retentionSeconds = getAblestackN2KRetentionSeconds(cmd, sourceContext);
boolean startTargetVm = getAblestackN2KStartTargetVm(cmd, sourceContext);
sourceContext.put("retentionSeconds", Long.toString(retentionSeconds));
sourceContext.put("startTargetVm", Boolean.toString(startTargetVm));

ImportVmTaskSourceCredential storedCredential = importVmTasksManager.getImportVMTaskSourceCredential(task);
String prismEndpoint = StringUtils.defaultIfBlank(cmd.getHost(), task.getSourceEndpoint());
Expand Down Expand Up @@ -4860,7 +4864,7 @@ protected void continueAblestackN2KVmImport(ImportUnmanagedInstanceForAblestackN
startNutanixInstanceToKVMUsingAblestackN2K(task.getSourceVMName(), convertHost, targetStorageLocation,
prismEndpoint, username, password, "phase2", "v3", cmd.isInsecure(), task.getWorkdir(), nfsHost,
targetStoragePlan, task, zone, owner, serviceOffering, targetStoragePool, nicNetworkMap, targetVmName, targetVmName,
retentionSeconds);
retentionSeconds, false, startTargetVm);
triggerAblestackN2KPhase2MonitoringInBackground(task.getUuid());
} catch (RuntimeException e) {
logger.error(String.format("Error while executing ablestack-n2k phase2 workflow for task %s", task.getUuid()), e);
Expand Down Expand Up @@ -4974,6 +4978,7 @@ private void reenterAblestackN2KVmImport(ImportUnmanagedInstanceForAblestackN2KC
String sourceApi = StringUtils.defaultIfBlank(cmd.getSourceApi(), StringUtils.defaultIfBlank(sourceContext.get("sourceApi"), "v3"));
boolean insecure = cmd.isInsecure();
long retentionSeconds = getAblestackN2KRetentionSeconds(cmd, sourceContext);
boolean startTargetVm = getAblestackN2KStartTargetVm(cmd, sourceContext);

try {
if (!resume) {
Expand All @@ -4992,6 +4997,7 @@ private void reenterAblestackN2KVmImport(ImportUnmanagedInstanceForAblestackN2KC
}
sourceContext.put("insecure", Boolean.toString(insecure));
sourceContext.put("retentionSeconds", Long.toString(retentionSeconds));
sourceContext.put("startTargetVm", Boolean.toString(startTargetVm));
importVmTasksManager.updateImportVMTaskN2KContext(task, task.getClusterId(), task.getServiceOfferingId(),
targetStoragePool.getId(), prismEndpoint, sourceApi, gson.toJson(sourceInstance),
getTaskServiceOfferingDetails(task), getTaskNicSelectionMap(task), targetStoragePlan.getTargetProfile(),
Expand All @@ -5006,7 +5012,7 @@ private void reenterAblestackN2KVmImport(ImportUnmanagedInstanceForAblestackN2KC
startNutanixInstanceToKVMUsingAblestackN2K(task.getSourceVMName(), convertHost, targetStorageLocation,
prismEndpoint, username, password, splitMode, sourceApi, insecure, workdir, nfsHost,
targetStoragePlan, task, zone, owner, serviceOffering, targetStoragePool, nicNetworkMap, targetVmName, targetVmName,
retentionSeconds, resume);
retentionSeconds, resume, startTargetVm);
if (StringUtils.equalsIgnoreCase(splitMode, "phase2")) {
triggerAblestackN2KPhase2MonitoringInBackground(task.getUuid());
}
Expand All @@ -5028,7 +5034,7 @@ private void startNutanixInstanceToKVMUsingAblestackN2K(String sourceVM, HostVO
Map<String, Long> nicNetworkMap, String hostName, String displayName) {
startNutanixInstanceToKVMUsingAblestackN2K(sourceVM, convertHost, targetStorageLocation, prismEndpoint, username, password,
splitMode, sourceApi, insecure, workdir, nfsHost, targetStoragePlan, importTask, zone, owner,
serviceOffering, targetStoragePool, nicNetworkMap, hostName, displayName, ABLESTACK_N2K_DEFAULT_RETENTION_SECONDS, false);
serviceOffering, targetStoragePool, nicNetworkMap, hostName, displayName, ABLESTACK_N2K_DEFAULT_RETENTION_SECONDS, false, true);
}

private void startNutanixInstanceToKVMUsingAblestackN2K(String sourceVM, HostVO convertHost, DataStoreTO targetStorageLocation,
Expand All @@ -5041,7 +5047,7 @@ private void startNutanixInstanceToKVMUsingAblestackN2K(String sourceVM, HostVO
long retentionSeconds) {
startNutanixInstanceToKVMUsingAblestackN2K(sourceVM, convertHost, targetStorageLocation, prismEndpoint, username, password,
splitMode, sourceApi, insecure, workdir, nfsHost, targetStoragePlan, importTask, zone, owner,
serviceOffering, targetStoragePool, nicNetworkMap, hostName, displayName, retentionSeconds, false);
serviceOffering, targetStoragePool, nicNetworkMap, hostName, displayName, retentionSeconds, false, true);
}

private void startNutanixInstanceToKVMUsingAblestackN2K(String sourceVM, HostVO convertHost, DataStoreTO targetStorageLocation,
Expand All @@ -5051,7 +5057,7 @@ private void startNutanixInstanceToKVMUsingAblestackN2K(String sourceVM, HostVO
ImportVMTaskVO importTask, DataCenter zone, Account owner,
ServiceOfferingVO serviceOffering, StoragePoolVO targetStoragePool,
Map<String, Long> nicNetworkMap, String hostName, String displayName,
long retentionSeconds, boolean resume) {
long retentionSeconds, boolean resume, boolean startTargetVm) {
logger.debug("Delegating the conversion of instance {} from Nutanix to KVM to the host {} using ablestack-n2k", sourceVM, convertHost);

List<String> missingParams = new ArrayList<>();
Expand Down Expand Up @@ -5083,6 +5089,7 @@ private void startNutanixInstanceToKVMUsingAblestackN2K(String sourceVM, HostVO
AblestackN2KConvertInstanceCommand cmd = new AblestackN2KConvertInstanceCommand(sourceVM, prismEndpoint, username, password,
targetStorageLocation, splitMode, sourceApi, insecure, workdir);
cmd.setResume(resume);
cmd.setStartTargetVm(startTargetVm);
cmd.setNfsHost(nfsHost);
cmd.setRetentionSeconds(retentionSeconds);
cmd.setTargetFormat(targetStoragePlan.getTargetFormat());
Expand Down Expand Up @@ -5540,6 +5547,20 @@ protected long getAblestackN2KRetentionSeconds(ImportUnmanagedInstanceForAblesta
return ABLESTACK_N2K_DEFAULT_RETENTION_SECONDS;
}

protected boolean getAblestackN2KStartTargetVm(ImportUnmanagedInstanceForAblestackN2KCmd cmd, Map<String, String> sourceContext) {
Boolean requestedStartTargetVm = cmd != null ? cmd.getRequestedStartTargetVm() : null;
if (requestedStartTargetVm != null) {
return BooleanUtils.toBoolean(requestedStartTargetVm);
}

String storedStartTargetVm = sourceContext != null ? StringUtils.trimToNull(sourceContext.get("startTargetVm")) : null;
if (StringUtils.isNotBlank(storedStartTargetVm)) {
return BooleanUtils.toBoolean(storedStartTargetVm);
}

return true;
}

protected String getAblestackTaskResumeSplitMode(ImportVMTaskVO task) {
String phase = StringUtils.lowerCase(StringUtils.defaultString(task != null ? task.getCurrentPhase() : null));
String step = StringUtils.defaultString(task != null ? StringUtils.defaultIfBlank(task.getMigrationStep(), task.getV2kStep()) : null);
Expand Down
8 changes: 7 additions & 1 deletion ui/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand All @@ -5574,5 +5575,10 @@
"message.confirm.retry.import.vm.task.from.start": "Keep the existing task record and start a new import task from the beginning with the same selections.",
"message.confirm.cancel.import.vm.task": "Cancel the running import task and try to cleanup temporary migration resources on the conversion host.",
"message.confirm.delete.import.vm.task": "Delete the import task from the list. Stored credentials and temporary work directories are also cleaned up when selected.",
"message.confirm.clear.import.vm.task.credentials": "Delete the encrypted source credential stored for this task. You may need to enter credentials again for Phase2 or resume."
"message.confirm.clear.import.vm.task.credentials": "Delete the encrypted source credential stored for this task. You may need to enter credentials again for Phase2 or resume.",
"label.n2k.start.target.vm": "Start target VM after cutover",
"label.n2k.keep.target.vm.stopped": "Keep target VM stopped",
"label.n2k.target.vm.power.policy": "Target VM power policy",
"label.keep.current.setting": "Keep current setting",
"message.n2k.start.target.vm": "When enabled, Phase2 starts the imported Cloud VM after cutover. Disable it to leave the target VM stopped after import."
}
Loading
Loading