diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9c8ca10f85e9..742faf57e4bb 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -54,5 +54,7 @@ jobs:
- name: Install Non-OSS
uses: ./.github/actions/install-nonoss
- name: Noredist Build
+ env:
+ DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
run: |
mvn -B -P developer,systemvm -Dsimulator -Dnoredist clean install -T$(nproc)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7f6645f4af4d..d46df2f395f5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -54,6 +54,8 @@ jobs:
git status
ipmitool -V
- name: Build with Maven
+ env:
+ DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
run: |
mvn -B -P developer,systemvm -Dsimulator clean install -DskipTests=true -T$(nproc)
- name: Archive artifacts
@@ -307,6 +309,8 @@ jobs:
mkdir -p ~/.m2/repository
tar -xzf /tmp/artifacts/m2-cloudstack.tar.gz -C ~/.m2/repository
- name: Setup Simulator Prerequisites
+ env:
+ DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
run: |
sudo python3 -m pip install --upgrade netaddr mysql-connector-python
python3 -m pip install --user --upgrade tools/marvin/dist/[mM]arvin-*.tar.gz
@@ -319,6 +323,8 @@ jobs:
find . | grep "src/main/java" | sed 's/\/java\//\/java /g' | awk '{print "--sourcefiles", $1, "\\"}' | sort | uniq >> jacoco-report.sh
echo "--xml jacoco-coverage.xml" >> jacoco-report.sh
- name: Start CloudStack Management Server with Simulator
+ env:
+ DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
run: |
export MAVEN_OPTS="-Xmx4096m -XX:MaxMetaspaceSize=800m -Djava.security.egd=file:/dev/urandom -javaagent:jacoco/lib/jacocoagent.jar=address=*,port=36320,output=tcpserver --add-opens=java.base/java.lang=ALL-UNNAMED --add-exports=java.base/sun.security.x509=ALL-UNNAMED --add-opens=java.base/jdk.internal.reflect=ALL-UNNAMED"
echo -e "\nStarting simulator"
@@ -329,6 +335,8 @@ jobs:
echo -e "\nStarting Advanced Zone DataCenter deployment"
python3 tools/marvin/marvin/deployDataCenter.py -i setup/dev/advdualzone.cfg 2>&1 || true
- name: Run Integration Tests with Simulator
+ env:
+ DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
run: |
mkdir -p integration-test-results/smoke/misc
mkdir -p integration-test-results/component
diff --git a/.github/workflows/docker-cloudstack-simulator.yml b/.github/workflows/docker-cloudstack-simulator.yml
index 64b17fca8cca..361f2659c7f1 100644
--- a/.github/workflows/docker-cloudstack-simulator.yml
+++ b/.github/workflows/docker-cloudstack-simulator.yml
@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Login to Docker Registry
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
+ uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ secrets.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USER }}
diff --git a/.github/workflows/main-sonar-check.yml b/.github/workflows/main-sonar-check.yml
index f48409fbc0c3..1c9f1273b976 100644
--- a/.github/workflows/main-sonar-check.yml
+++ b/.github/workflows/main-sonar-check.yml
@@ -49,9 +49,12 @@ jobs:
- name: Install Non-OSS
uses: ./.github/actions/install-nonoss
- name: Run Build and Tests with Coverage
+ env:
+ DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
run: mvn -B -T$(nproc) -P developer,systemvm,quality -Dsimulator -Dnoredist clean install
- name: Upload to SonarQube
env:
+ DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: mvn -B -P quality org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=apache_cloudstack -Dsonar.branch.name=${{ github.ref_name }}
diff --git a/.github/workflows/rat.yml b/.github/workflows/rat.yml
index 5c72d64d6ed9..f4659885fbb3 100644
--- a/.github/workflows/rat.yml
+++ b/.github/workflows/rat.yml
@@ -40,6 +40,8 @@ jobs:
- name: Install Non-OSS
uses: ./.github/actions/install-nonoss
- name: RAT licence checks
+ env:
+ DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
run: |
mvn -P developer,systemvm -Dsimulator -Dnoredist -pl . org.apache.rat:apache-rat-plugin:0.18:check
- name: Rat Report
diff --git a/.github/workflows/sonar-check.yml b/.github/workflows/sonar-check.yml
index 7f2a1bdb2935..3fe8c53fecea 100644
--- a/.github/workflows/sonar-check.yml
+++ b/.github/workflows/sonar-check.yml
@@ -47,10 +47,13 @@ jobs:
- name: Install Non-OSS
uses: ./.github/actions/install-nonoss
- name: Run Build and Tests with Coverage
+ env:
+ DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
run: mvn -B -T$(nproc) -P developer,systemvm,quality -Dsimulator -Dnoredist clean install
- name: Upload to SonarQube
if: github.repository == 'apache/cloudstack' && github.event.pull_request.head.repo.full_name == github.repository
env:
+ DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
PR_ID: ${{ github.event.pull_request.number }}
diff --git a/.gitignore b/.gitignore
index abaef83e4555..1c96edf51e2b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -99,6 +99,9 @@ tools/marvin/marvin/cloudstackAPI/*
unittest
venv
waf-*
+waf3-*
+
+**/.mvn/.develocity*
# this ignores _all files starting with '.'. Don't do that!
#.*
diff --git a/.mvn/develocity.xml b/.mvn/develocity.xml
new file mode 100644
index 000000000000..bde8355a5e2f
--- /dev/null
+++ b/.mvn/develocity.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+ https://develocity.apache.org
+ false
+
+
+ #{isFalse(env['GITHUB_ACTIONS'])}
+
+ #{{'0.0.0.0'}}
+
+
+
+
+
+
+
+
+
+ false
+
+
+ false
+ #{isTrue(env['GITHUB_ACTIONS'])}
+
+
+
diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml
new file mode 100644
index 000000000000..8118c8f9f463
--- /dev/null
+++ b/.mvn/extensions.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+ com.gradle
+ develocity-maven-extension
+ 2.6.0
+
+
+ com.gradle
+ common-custom-user-data-maven-extension
+ 2.4.0
+
+
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 391681e88c31..3c573ec8da27 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -216,3 +216,10 @@ repos:
types: [yaml]
files: \.ya?ml$
exclude: ^.*k8s-.*\.ya?ml$|^.github/workflows/.*\.lock\.ya?ml$
+ - repo: https://github.com/python-jsonschema/check-jsonschema
+ rev: 12e63946db2c5cfcc9030fac21c3883ba88c6ed8 # frozen: 0.38.0
+ hooks:
+ - id: check-dependabot
+ name: validate dependabot config
+ description: ensures the dependabot config file is valid
+ files: ^\.github/dependabot\.ya?ml$
diff --git a/PendingReleaseNotes b/PendingReleaseNotes
index 44923495b23f..bc6068fa3310 100644
--- a/PendingReleaseNotes
+++ b/PendingReleaseNotes
@@ -55,6 +55,25 @@ example.ver.1 > example.ver.2:
continue to snapshot live.
4.23.0.0 > 24.0.0:
+ * VMware-to-KVM import using VDDK can import powered-off VMware VMs directly
+ into Ceph RBD primary storage when forceconverttopool is enabled and the
+ selected KVM conversion host supports qemu RBD access and in-place virt-v2v
+ finalization. Hosts without in-place finalization support can still use the
+ staged VDDK flow, converting to temporary qcow2 storage before copying the
+ finalized disks into RBD.
+
+ * Linstor primary storage can now be the destination for VMware-to-KVM
+ imports in all three modes. Staged imports (virt-v2v to a temporary NFS
+ location) copy the converted disks into pre-created DRBD block devices
+ through the Linstor storage adaptor; the import host must be a LINSTOR
+ satellite connected to the destination pool. Direct VDDK imports with
+ forceconverttopool write each powered-off VMware disk over nbdkit/VDDK
+ straight into the DRBD device and finalize it with virt-v2v in place.
+ VMware CBT warm migrations replicate the initial full copy and all
+ incremental changed-block cycles directly into Linstor volumes and
+ finalize in place at cutover; the conversion host requires VDDK,
+ in-place virt-v2v support and access to the Linstor pool.
+
* KVM/Ceph: RBD volumes can now be encrypted at rest using native librbd
LUKS2 (), for both data disks
and root disks. Encryption is transparent to the guest and reuses the
diff --git a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java
index e4775188d0ca..f27f7d89e5bc 100644
--- a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java
+++ b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java
@@ -848,6 +848,19 @@ public Property getWorkers() {
*/
public static final Property VDDK_TRANSPORTS = new Property<>("vddk.transports", null, String.class);
+ /**
+ * NBD-transport compression used between the VDDK and the VMware server for the
+ * agent-driven nbdkit full-disk copies (cold direct import and CBT initial full
+ * sync). Passed to the nbdkit vddk plugin as compression=<value>.
+ * Valid values: none, zlib, fastlz,
+ * skipz (nbdkit ≥ 1.24, vSphere ≥ 6.5). fastlz
+ * compresses both zero regions and real data at low CPU cost; skipz
+ * only elides zero blocks. Does not affect the staged virt-v2v conversion path.
+ * Data type: String.
+ * Default value: fastlz
+ */
+ public static final Property VDDK_NBD_COMPRESSION = new Property<>("vddk.nbd.compression", "fastlz", String.class);
+
/**
* vCenter TLS certificate thumbprint used by virt-v2v VDDK mode, passed as -io vddk-thumbprint=<value>.
* If unset, the KVM host computes it at runtime from the vCenter endpoint.
diff --git a/api/src/main/java/com/cloud/agent/api/to/RemoteInstanceTO.java b/api/src/main/java/com/cloud/agent/api/to/RemoteInstanceTO.java
index 7daeb9649177..8aec16d4b101 100644
--- a/api/src/main/java/com/cloud/agent/api/to/RemoteInstanceTO.java
+++ b/api/src/main/java/com/cloud/agent/api/to/RemoteInstanceTO.java
@@ -38,6 +38,7 @@ public class RemoteInstanceTO implements Serializable {
private String datacenterName;
private String clusterName;
private String hostName;
+ private String vmwareMoref;
public RemoteInstanceTO() {
}
@@ -65,6 +66,11 @@ public RemoteInstanceTO(String instanceName, String instancePath, String vcenter
this.hostName = hostName;
}
+ public RemoteInstanceTO(String instanceName, String instancePath, String vcenterHost, String vcenterUsername, String vcenterPassword, String datacenterName, String clusterName, String hostName, String vmwareMoref) {
+ this(instanceName, instancePath, vcenterHost, vcenterUsername, vcenterPassword, datacenterName, clusterName, hostName);
+ this.vmwareMoref = vmwareMoref;
+ }
+
public Hypervisor.HypervisorType getHypervisorType() {
return this.hypervisorType;
}
@@ -100,4 +106,12 @@ public String getClusterName() {
public String getHostName() {
return hostName;
}
+
+ public String getVmwareMoref() {
+ return vmwareMoref;
+ }
+
+ public void setVmwareMoref(String vmwareMoref) {
+ this.vmwareMoref = vmwareMoref;
+ }
}
diff --git a/api/src/main/java/com/cloud/agent/api/to/VmwareCbtChangedBlockRangeTO.java b/api/src/main/java/com/cloud/agent/api/to/VmwareCbtChangedBlockRangeTO.java
new file mode 100644
index 000000000000..ee8fcc59a290
--- /dev/null
+++ b/api/src/main/java/com/cloud/agent/api/to/VmwareCbtChangedBlockRangeTO.java
@@ -0,0 +1,47 @@
+// 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.
+package com.cloud.agent.api.to;
+
+import java.io.Serializable;
+
+public class VmwareCbtChangedBlockRangeTO implements Serializable {
+
+ private String diskId;
+ private long startOffset;
+ private long length;
+
+ public VmwareCbtChangedBlockRangeTO() {
+ }
+
+ public VmwareCbtChangedBlockRangeTO(String diskId, long startOffset, long length) {
+ this.diskId = diskId;
+ this.startOffset = startOffset;
+ this.length = length;
+ }
+
+ public String getDiskId() {
+ return diskId;
+ }
+
+ public long getStartOffset() {
+ return startOffset;
+ }
+
+ public long getLength() {
+ return length;
+ }
+}
diff --git a/api/src/main/java/com/cloud/agent/api/to/VmwareCbtDiskSyncResultTO.java b/api/src/main/java/com/cloud/agent/api/to/VmwareCbtDiskSyncResultTO.java
new file mode 100644
index 000000000000..c22ea0bb24c0
--- /dev/null
+++ b/api/src/main/java/com/cloud/agent/api/to/VmwareCbtDiskSyncResultTO.java
@@ -0,0 +1,78 @@
+// 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.
+package com.cloud.agent.api.to;
+
+import java.io.Serializable;
+
+public class VmwareCbtDiskSyncResultTO implements Serializable {
+
+ private String diskId;
+ private String targetPath;
+ private String changeId;
+ private String snapshotMor;
+ private long changedBytes;
+ private long durationSeconds;
+ private boolean result;
+ private String details;
+
+ public VmwareCbtDiskSyncResultTO() {
+ }
+
+ public VmwareCbtDiskSyncResultTO(String diskId, String targetPath, String changeId, String snapshotMor,
+ long changedBytes, long durationSeconds, boolean result, String details) {
+ this.diskId = diskId;
+ this.targetPath = targetPath;
+ this.changeId = changeId;
+ this.snapshotMor = snapshotMor;
+ this.changedBytes = changedBytes;
+ this.durationSeconds = durationSeconds;
+ this.result = result;
+ this.details = details;
+ }
+
+ public String getDiskId() {
+ return diskId;
+ }
+
+ public String getTargetPath() {
+ return targetPath;
+ }
+
+ public String getChangeId() {
+ return changeId;
+ }
+
+ public String getSnapshotMor() {
+ return snapshotMor;
+ }
+
+ public long getChangedBytes() {
+ return changedBytes;
+ }
+
+ public long getDurationSeconds() {
+ return durationSeconds;
+ }
+
+ public boolean getResult() {
+ return result;
+ }
+
+ public String getDetails() {
+ return details;
+ }
+}
diff --git a/api/src/main/java/com/cloud/agent/api/to/VmwareCbtDiskTO.java b/api/src/main/java/com/cloud/agent/api/to/VmwareCbtDiskTO.java
new file mode 100644
index 000000000000..e6dcf1bfbca6
--- /dev/null
+++ b/api/src/main/java/com/cloud/agent/api/to/VmwareCbtDiskTO.java
@@ -0,0 +1,85 @@
+// 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.
+package com.cloud.agent.api.to;
+
+import java.io.Serializable;
+
+public class VmwareCbtDiskTO implements Serializable {
+
+ private String diskId;
+ private Integer diskDeviceKey;
+ private String sourceDiskPath;
+ private String datastoreName;
+ private String targetPath;
+ private String targetFormat;
+ private String changeId;
+ private String snapshotMor;
+ private long capacityBytes;
+
+ public VmwareCbtDiskTO() {
+ }
+
+ public VmwareCbtDiskTO(String diskId, Integer diskDeviceKey, String sourceDiskPath, String datastoreName,
+ String targetPath, String targetFormat, String changeId, String snapshotMor,
+ long capacityBytes) {
+ this.diskId = diskId;
+ this.diskDeviceKey = diskDeviceKey;
+ this.sourceDiskPath = sourceDiskPath;
+ this.datastoreName = datastoreName;
+ this.targetPath = targetPath;
+ this.targetFormat = targetFormat;
+ this.changeId = changeId;
+ this.snapshotMor = snapshotMor;
+ this.capacityBytes = capacityBytes;
+ }
+
+ public String getDiskId() {
+ return diskId;
+ }
+
+ public Integer getDiskDeviceKey() {
+ return diskDeviceKey;
+ }
+
+ public String getSourceDiskPath() {
+ return sourceDiskPath;
+ }
+
+ public String getDatastoreName() {
+ return datastoreName;
+ }
+
+ public String getTargetPath() {
+ return targetPath;
+ }
+
+ public String getTargetFormat() {
+ return targetFormat;
+ }
+
+ public String getChangeId() {
+ return changeId;
+ }
+
+ public String getSnapshotMor() {
+ return snapshotMor;
+ }
+
+ public long getCapacityBytes() {
+ return capacityBytes;
+ }
+}
diff --git a/api/src/main/java/com/cloud/agent/api/to/VmwareVddkSourceDiskTO.java b/api/src/main/java/com/cloud/agent/api/to/VmwareVddkSourceDiskTO.java
new file mode 100644
index 000000000000..b980c611abc9
--- /dev/null
+++ b/api/src/main/java/com/cloud/agent/api/to/VmwareVddkSourceDiskTO.java
@@ -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.
+package com.cloud.agent.api.to;
+
+import java.io.Serializable;
+
+public class VmwareVddkSourceDiskTO implements Serializable {
+
+ private String diskId;
+ private String sourceDiskPath;
+ private long capacityBytes;
+ private Integer position;
+
+ public VmwareVddkSourceDiskTO() {
+ }
+
+ public VmwareVddkSourceDiskTO(String diskId, String sourceDiskPath, long capacityBytes, Integer position) {
+ this.diskId = diskId;
+ this.sourceDiskPath = sourceDiskPath;
+ this.capacityBytes = capacityBytes;
+ this.position = position;
+ }
+
+ public String getDiskId() {
+ return diskId;
+ }
+
+ public String getSourceDiskPath() {
+ return sourceDiskPath;
+ }
+
+ public long getCapacityBytes() {
+ return capacityBytes;
+ }
+
+ public Integer getPosition() {
+ return position;
+ }
+}
diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java
index f7d13343d469..7f45c8acd3f7 100644
--- a/api/src/main/java/com/cloud/event/EventTypes.java
+++ b/api/src/main/java/com/cloud/event/EventTypes.java
@@ -49,6 +49,8 @@
import org.apache.cloudstack.storage.sharedfs.SharedFS;
import org.apache.cloudstack.usage.Usage;
import org.apache.cloudstack.schedule.ResourceSchedule;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup;
+import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule;
import com.cloud.dc.DataCenter;
import com.cloud.dc.DataCenterGuestIpv6Prefix;
@@ -127,6 +129,9 @@ public class EventTypes {
public static final String EVENT_VM_RESTORE = "VM.RESTORE";
public static final String EVENT_VM_EXPUNGE = "VM.EXPUNGE";
public static final String EVENT_VM_IMPORT = "VM.IMPORT";
+ public static final String EVENT_VMWARE_CBT_MIGRATION_START = "VMWARE.CBT.MIGRATION.START";
+ public static final String EVENT_VMWARE_CBT_MIGRATION_SYNC = "VMWARE.CBT.MIGRATION.SYNC";
+ public static final String EVENT_VMWARE_CBT_MIGRATION_CUTOVER = "VMWARE.CBT.MIGRATION.CUTOVER";
public static final String EVENT_VM_UNMANAGE = "VM.UNMANAGE";
public static final String EVENT_VM_RECOVER = "VM.RECOVER";
@@ -899,6 +904,21 @@ public class EventTypes {
public static final String EVENT_DNS_RECORD_DELETE = "DNS.RECORD.DELETE";
public static final String EVENT_DNS_NAME_COLLISION = "DNS.NAME.COLLISION";
+ // Instance Boot Group
+ public static final String EVENT_INSTANCE_BOOT_GROUP_CREATE = "INSTANCE.BOOT.GROUP.CREATE";
+ public static final String EVENT_INSTANCE_BOOT_GROUP_DELETE = "INSTANCE.BOOT.GROUP.DELETE";
+ public static final String EVENT_INSTANCE_BOOT_GROUP_UPDATE = "INSTANCE.BOOT.GROUP.UPDATE";
+ public static final String EVENT_INSTANCE_BOOT_GROUP_START = "INSTANCE.BOOT.GROUP.START";
+ public static final String EVENT_INSTANCE_BOOT_GROUP_STOP = "INSTANCE.BOOT.GROUP.STOP";
+ public static final String EVENT_INSTANCE_BOOT_GROUP_REBOOT = "INSTANCE.BOOT.GROUP.REBOOT";
+ public static final String EVENT_INSTANCE_BOOT_GROUP_MEMBER_ADD = "INSTANCE.BOOT.GROUP.MEMBER.ADD";
+ public static final String EVENT_INSTANCE_BOOT_GROUP_MEMBER_REMOVE = "INSTANCE.BOOT.GROUP.MEMBER.REMOVE";
+ public static final String EVENT_INSTANCE_BOOT_GROUP_MEMBER_REORDER = "INSTANCE.BOOT.GROUP.MEMBER.REORDER";
+ public static final String EVENT_INSTANCE_BOOT_GROUP_READINESS_RULE_CREATE = "INSTANCE.BOOT.GROUP.READINESS.RULE.CREATE";
+ public static final String EVENT_INSTANCE_BOOT_GROUP_READINESS_RULE_UPDATE = "INSTANCE.BOOT.GROUP.READINESS.RULE.UPDATE";
+ public static final String EVENT_INSTANCE_BOOT_GROUP_READINESS_RULE_DELETE = "INSTANCE.BOOT.GROUP.READINESS.RULE.DELETE";
+
+
static {
// TODO: need a way to force author adding event types to declare the entity details as well, with out braking
@@ -1468,6 +1488,19 @@ public class EventTypes {
entityEventDetails.put(EVENT_DNS_RECORD_CREATE, DnsRecord.class);
entityEventDetails.put(EVENT_DNS_RECORD_DELETE, DnsRecord.class);
+ // Instance Boot Group
+ entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_CREATE, InstanceBootGroup.class);
+ entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_DELETE, InstanceBootGroup.class);
+ entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_UPDATE, InstanceBootGroup.class);
+ entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_START, InstanceBootGroup.class);
+ entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_STOP, InstanceBootGroup.class);
+ entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_REBOOT, InstanceBootGroup.class);
+ entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_MEMBER_ADD, InstanceBootGroup.class);
+ entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_MEMBER_REMOVE, InstanceBootGroup.class);
+ entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_MEMBER_REORDER, InstanceBootGroup.class);
+ entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_READINESS_RULE_CREATE, InstanceBootGroupReadinessRule.class);
+ entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_READINESS_RULE_UPDATE, InstanceBootGroupReadinessRule.class);
+ entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_READINESS_RULE_DELETE, InstanceBootGroupReadinessRule.class);
}
public static boolean isNetworkEvent(String eventType) {
diff --git a/api/src/main/java/com/cloud/host/Host.java b/api/src/main/java/com/cloud/host/Host.java
index 79bec7a5871a..cca250c2a90f 100644
--- a/api/src/main/java/com/cloud/host/Host.java
+++ b/api/src/main/java/com/cloud/host/Host.java
@@ -62,8 +62,18 @@ public static String[] toStrings(Host.Type... types) {
String HOST_VDDK_SUPPORT = "host.vddk.support";
String HOST_VDDK_LIB_DIR = "vddk.lib.dir";
String HOST_VDDK_VERSION = "host.vddk.version";
+ String HOST_VDDK_BLOCKCOPY_SUPPORT = "host.vddk.blockcopy.support";
+ String HOST_VDDK_BLOCKCOPY_INPLACE_FINALIZATION_SUPPORT = "host.vddk.blockcopy.inplace.finalization.support";
+ String HOST_VDDK_BLOCKCOPY_RBD_SUPPORT = "host.vddk.blockcopy.rbd.support";
+ String HOST_QEMU_IMG_VERSION = "host.qemu.img.version";
+ String HOST_QEMU_NBD_VERSION = "host.qemu.nbd.version";
+ String HOST_QEMU_IO_VERSION = "host.qemu.io.version";
String HOST_OVFTOOL_VERSION = "host.ovftool.version";
String HOST_VIRTV2V_VERSION = "host.virtv2v.version";
+ String HOST_VIRTV2V_INPLACE_VERSION = "host.virtv2v.inplace.version";
+ String HOST_VDDK_RBD_DIRECT_IMPORT_SUPPORT = "host.vddk.rbd.direct.import.support";
+ String HOST_VIRTV2V_INPLACE_SUPPORT = "host.virtv2v.inplace.support";
+ String HOST_QEMU_RBD_SUPPORT = "host.qemu.rbd.support";
String HOST_SSH_PORT = "host.ssh.port";
String HOST_CDROM_MAX_COUNT = "host.cdrom.max.count";
String GUEST_OS_CATEGORY_ID = "guest.os.category.id";
diff --git a/api/src/main/java/com/cloud/network/netris/NetrisService.java b/api/src/main/java/com/cloud/network/netris/NetrisService.java
index 110e9f07105a..1e7c7d66399a 100644
--- a/api/src/main/java/com/cloud/network/netris/NetrisService.java
+++ b/api/src/main/java/com/cloud/network/netris/NetrisService.java
@@ -21,6 +21,8 @@
import com.cloud.network.SDNProviderNetworkRule;
import com.cloud.network.vpc.StaticRoute;
import com.cloud.network.vpc.Vpc;
+import com.cloud.offering.NetworkOffering;
+import com.cloud.utils.net.NetUtils;
import java.util.List;
@@ -81,18 +83,20 @@ public interface NetrisService {
/**
* Creates a virtual network (vNet) resource.
*
- * @param zoneId the ID of the zone
- * @param accountId the ID of the account
- * @param domainId the ID of the domain
- * @param vpcName the name of the VPC
- * @param vpcId the ID of the VPC
- * @param networkName the name of the network
- * @param networkId the ID of the network
- * @param cidr the CIDR of the network
- * @param globalRouting true if global routing is enabled
+ * @param zoneId the ID of the zone
+ * @param accountId the ID of the account
+ * @param domainId the ID of the domain
+ * @param vpcName the name of the VPC
+ * @param vpcId the ID of the VPC
+ * @param networkName the name of the network
+ * @param networkId the ID of the network
+ * @param cidr the CIDR of the network
+ * @param networkMode the network mode
+ * @param internetProtocol the internet protocol
+ * @param isL2 true if it is an L2 network
* @return true if the operation is successful, false otherwise
*/
- boolean createVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, Boolean globalRouting);
+ boolean createVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, NetworkOffering.NetworkMode networkMode, NetUtils.InternetProtocol internetProtocol, boolean isL2);
/**
* Updates an existing vNet resource.
@@ -105,9 +109,10 @@ public interface NetrisService {
* @param networkName the new name of the network
* @param networkId the ID of the network
* @param prevNetworkName the previous name of the network
+ * @param isL2 true if it is an L2 network
* @return true if the operation is successful, false otherwise
*/
- boolean updateVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String prevNetworkName);
+ boolean updateVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String prevNetworkName, boolean isL2);
/**
* Deletes an existing vNet resource.
@@ -120,9 +125,10 @@ public interface NetrisService {
* @param networkName the name of the network
* @param networkId the ID of the network
* @param cidr the CIDR of the network
+ * @param isL2 true if it is an L2 network
* @return true if the operation is successful, false otherwise
*/
- boolean deleteVnetResource(long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr);
+ boolean deleteVnetResource(long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, boolean isL2);
/**
* Creates a source NAT rule for a VPC or network.
diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java b/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java
index 2aa97b65a3d5..e12a709e6f5c 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java
@@ -22,6 +22,7 @@
import java.util.Map;
import org.apache.cloudstack.region.PortableIp;
+import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.EnumUtils;
import org.apache.commons.lang3.StringUtils;
@@ -91,7 +92,9 @@ public enum ApiCommandResourceType {
Extension(org.apache.cloudstack.extension.Extension.class),
ExtensionCustomAction(org.apache.cloudstack.extension.ExtensionCustomAction.class),
KmsKey(org.apache.cloudstack.kms.KMSKey.class),
- HsmProfile(org.apache.cloudstack.kms.HSMProfile.class);
+ HsmProfile(org.apache.cloudstack.kms.HSMProfile.class),
+ InstanceBootGroup(org.apache.cloudstack.vm.bootgroup.InstanceBootGroup.class),
+ InstanceBootGroupReadinessRule(InstanceBootGroupReadinessRule.class);
private final Class> clazz;
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 f74c46161180..7485029f22cb 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -93,6 +93,23 @@ public class ApiConstants {
public static final String CAPACITY = "capacity";
public static final String CATEGORY = "category";
public static final String CAN_REVERT = "canrevert";
+ public static final String BOOT_GROUP_ID = "bootgroupid";
+ public static final String BOOT_ORDER = "order";
+ public static final String MEMBER_TYPE = "membertype";
+ public static final String MEMBER_ID = "memberid";
+ public static final String MEMBER_NAME = "membername";
+ public static final String MEMBER_STATE = "memberstate";
+ public static final String CHILDREN = "children";
+ public static final String RULE_TYPE = "ruletype";
+ public static final String INHERITED = "inherited";
+ public static final String READINESS_MODE = "readinessmode";
+ public static final String READINESS_STATUS = "readinessstatus";
+ public static final String READINESS_MESSAGE = "readinessmessage";
+ public static final String READINESS_ATTEMPT_TIMEOUT_SECONDS = "readinessattempttimeoutseconds";
+ public static final String READINESS_MAX_RETRY_ATTEMPTS = "readinessmaxretryattempts";
+ public static final String READINESS_REBOOT_ON_RETRY = "readinessrebootonretry";
+ public static final String READINESS_INITIAL_DELAY_SECONDS = "readinessinitialdelayseconds";
+ public static final String IGNORE_INSTANCE_STATE = "ignoreinstancestate";
public static final String CA_CERTIFICATES = "cacertificates";
public static final String CERTIFICATE = "certificate";
public static final String CERTIFICATE_CHAIN = "certchain";
@@ -322,6 +339,7 @@ public class ApiConstants {
public static final String INTERNAL_DNS2 = "internaldns2";
public static final String INTERNET_PROTOCOL = "internetprotocol";
public static final String INTERVAL_TYPE = "intervaltype";
+ public static final String INSTANCE_GROUP_ID = "instancegroupid";
public static final String INSTANCE_LEASE_DURATION = "leaseduration";
public static final String INSTANCE_LEASE_ENABLED = "instanceleaseenabled";
public static final String INSTANCE_LEASE_EXPIRY_ACTION = "leaseexpiryaction";
@@ -653,6 +671,29 @@ public class ApiConstants {
public static final String USER_SECURITY_GROUP_LIST = "usersecuritygrouplist";
public static final String USER_SECRET_KEY = "usersecretkey";
public static final String USE_VDDK = "usevddk";
+ public static final String VMWARE_MIGRATION_MODE = "vmwaremigrationmode";
+ public static final String VMWARE_CBT_MIGRATION_ID = "vmwarecbtmigrationid";
+ public static final String SOURCE_HOST = "sourcehost";
+ public static final String SOURCE_CLUSTER = "sourcecluster";
+ public static final String SOURCE_VM_NAME = "sourcevmname";
+ public static final String SOURCE_DISK_ID = "sourcediskid";
+ public static final String SOURCE_DISK_DEVICE_KEY = "sourcediskdevicekey";
+ public static final String SOURCE_DISK_PATH = "sourcediskpath";
+ public static final String TARGET_PATH = "targetpath";
+ public static final String TARGET_FORMAT = "targetformat";
+ public static final String CHANGE_ID = "changeid";
+ public static final String SNAPSHOT_MOR = "snapshotmor";
+ public static final String CURRENT_STEP = "currentstep";
+ public static final String LAST_ERROR = "lasterror";
+ public static final String COMPLETED_CYCLES = "completedcycles";
+ public static final String QUIET_CYCLES = "quietcycles";
+ public static final String LAST_CHANGED_BYTES = "lastchangedbytes";
+ public static final String LAST_DIRTY_RATE = "lastdirtyrate";
+ public static final String TOTAL_CHANGED_BYTES = "totalchangedbytes";
+ public static final String CYCLE = "cycle";
+ public static final String CYCLE_NUMBER = "cyclenumber";
+ public static final String CHANGED_BYTES = "changedbytes";
+ public static final String DIRTY_RATE = "dirtyrate";
public static final String USE_VIRTUAL_NETWORK = "usevirtualnetwork";
public static final String USE_VIRTUAL_ROUTER_IP_RESOLVER = "userouteripresolver";
public static final String UPDATE_IN_SEQUENCE = "updateinsequence";
@@ -1342,6 +1383,7 @@ public class ApiConstants {
public static final String HAS_RULES = "hasrules";
public static final String NSX_DETAIL_KEY = "forNsx";
public static final String NETRIS_DETAIL_KEY = "forNetris";
+ public static final String NETRIS_NAT_PUBLIC_IP_ID = "netrisnatpublicipid";
public static final String NETRIS_TAG = "netristag";
public static final String NETRIS_VXLAN_ID = "netrisvxlanid";
public static final String NETRIS_URL = "netrisurl";
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/NetworkOfferingBaseCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/NetworkOfferingBaseCmd.java
index 9b42be137314..4b4d0dc94825 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/NetworkOfferingBaseCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/NetworkOfferingBaseCmd.java
@@ -52,6 +52,7 @@
import static com.cloud.network.Network.Service.StaticNat;
import static com.cloud.network.Network.Service.UserData;
+import static com.cloud.network.Network.Service.Vpn;
import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNsxWithoutLb;
import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisNatted;
import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisRouted;
@@ -290,6 +291,10 @@ public List getSupportedServices() {
} else {
services.add(Firewall.getName());
}
+
+ if (isNetrisNatted(getProvider(), getNetworkMode())) {
+ services.add(Vpn.getName());
+ }
return services;
}
}
@@ -395,6 +400,9 @@ else if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMo
serviceProviderMap.remove(Lb.getName());
}
}
+ if (isNetrisNatted(getProvider(), getNetworkMode())) {
+ serviceProviderMap.put(Vpn.getName(), List.of(routerProvider));
+ }
}
public Map getServiceCapabilities(Network.Service service) {
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CancelVmwareCbtMigrationCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CancelVmwareCbtMigrationCmd.java
new file mode 100644
index 000000000000..fa4fc4f7bb6f
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CancelVmwareCbtMigrationCmd.java
@@ -0,0 +1,73 @@
+// 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.
+package org.apache.cloudstack.api.command.admin.vm;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ResponseObject;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.VmwareCbtMigrationResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.VmwareCbtMigrationManager;
+
+import com.cloud.exception.ConcurrentOperationException;
+import com.cloud.exception.InsufficientCapacityException;
+import com.cloud.exception.NetworkRuleConflictException;
+import com.cloud.exception.ResourceAllocationException;
+import com.cloud.exception.ResourceUnavailableException;
+import com.cloud.user.Account;
+
+@APICommand(name = "cancelVmwareCbtMigration",
+ description = "Cancel a VMware CBT migration session",
+ responseObject = VmwareCbtMigrationResponse.class,
+ responseView = ResponseObject.ResponseView.Full,
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin},
+ since = "4.24.0")
+public class CancelVmwareCbtMigrationCmd extends BaseCmd {
+
+ @Inject
+ public VmwareCbtMigrationManager vmwareCbtMigrationManager;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = VmwareCbtMigrationResponse.class,
+ required = true, description = "the VMware CBT migration ID")
+ private Long id;
+
+ public Long getId() {
+ return id;
+ }
+
+ @Override
+ public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException,
+ ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
+ VmwareCbtMigrationResponse response = vmwareCbtMigrationManager.cancelVmwareCbtMigration(this);
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ Account account = CallContext.current().getCallingAccount();
+ return account == null ? Account.ACCOUNT_ID_SYSTEM : account.getId();
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CheckVmwareCbtMigrationPrerequisitesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CheckVmwareCbtMigrationPrerequisitesCmd.java
new file mode 100644
index 000000000000..cbbadc090ce5
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CheckVmwareCbtMigrationPrerequisitesCmd.java
@@ -0,0 +1,204 @@
+// 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.
+package org.apache.cloudstack.api.command.admin.vm;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ResponseObject;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.ClusterResponse;
+import org.apache.cloudstack.api.response.HostResponse;
+import org.apache.cloudstack.api.response.ServiceOfferingResponse;
+import org.apache.cloudstack.api.response.StoragePoolResponse;
+import org.apache.cloudstack.api.response.VmwareCbtMigrationPreflightResponse;
+import org.apache.cloudstack.api.response.VmwareDatacenterResponse;
+import org.apache.cloudstack.api.response.ZoneResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.VmwareCbtMigrationManager;
+import org.apache.commons.collections.MapUtils;
+
+import com.cloud.exception.ConcurrentOperationException;
+import com.cloud.exception.InsufficientCapacityException;
+import com.cloud.exception.NetworkRuleConflictException;
+import com.cloud.exception.ResourceAllocationException;
+import com.cloud.exception.ResourceUnavailableException;
+import com.cloud.user.Account;
+
+@APICommand(name = "checkVmwareCbtMigrationPrerequisites",
+ description = "Check whether a VMware VM and KVM destination are ready for VMware CBT based migration",
+ responseObject = VmwareCbtMigrationPreflightResponse.class,
+ responseView = ResponseObject.ResponseView.Full,
+ requestHasSensitiveInfo = true,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin},
+ since = "4.24.0")
+public class CheckVmwareCbtMigrationPrerequisitesCmd extends BaseCmd {
+
+ @Inject
+ public VmwareCbtMigrationManager vmwareCbtMigrationManager;
+
+ @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class,
+ required = true, description = "the destination zone ID")
+ private Long zoneId;
+
+ @Parameter(name = ApiConstants.CLUSTER_ID, type = CommandType.UUID, entityType = ClusterResponse.class,
+ required = true, description = "the destination KVM cluster ID")
+ private Long clusterId;
+
+ @Parameter(name = ApiConstants.SOURCE_VM_NAME, type = CommandType.STRING,
+ required = true, description = "the source VMware VM name")
+ private String sourceVmName;
+
+ @Parameter(name = ApiConstants.EXISTING_VCENTER_ID, type = CommandType.UUID, entityType = VmwareDatacenterResponse.class,
+ description = "UUID of a linked existing vCenter")
+ private Long existingVcenterId;
+
+ @Parameter(name = ApiConstants.VCENTER, type = CommandType.STRING,
+ description = "the source vCenter IP address or FQDN")
+ private String vcenter;
+
+ @Parameter(name = ApiConstants.DATACENTER_NAME, type = CommandType.STRING,
+ description = "the source VMware datacenter name")
+ private String datacenterName;
+
+ @Parameter(name = ApiConstants.CLUSTER_NAME, type = CommandType.STRING,
+ description = "the source VMware cluster name")
+ private String sourceCluster;
+
+ @Parameter(name = ApiConstants.HOST_IP, type = CommandType.STRING,
+ description = "the source VMware ESXi host IP address or FQDN")
+ private String sourceHost;
+
+ @Parameter(name = ApiConstants.USERNAME, type = CommandType.STRING,
+ description = "the username for the source vCenter")
+ private String username;
+
+ @Parameter(name = ApiConstants.PASSWORD, type = CommandType.STRING,
+ description = "the password for the source vCenter")
+ private String password;
+
+ @Parameter(name = ApiConstants.CONVERT_INSTANCE_HOST_ID, type = CommandType.UUID, entityType = HostResponse.class,
+ description = "optional KVM host to perform CBT block replication")
+ private Long convertInstanceHostId;
+
+ @Parameter(name = ApiConstants.CONVERT_INSTANCE_STORAGE_POOL_ID, type = CommandType.UUID, entityType = StoragePoolResponse.class,
+ description = "optional primary storage pool for converted disks")
+ private Long storagePoolId;
+
+ @Parameter(name = ApiConstants.SERVICE_OFFERING_ID, type = CommandType.UUID, entityType = ServiceOfferingResponse.class,
+ description = "optional service offering to validate against the source VM CPU and memory requirements")
+ private Long serviceOfferingId;
+
+ @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP,
+ description = "optional service offering custom parameters and migration details")
+ private Map details;
+
+ public Long getZoneId() {
+ return zoneId;
+ }
+
+ public Long getClusterId() {
+ return clusterId;
+ }
+
+ public String getSourceVmName() {
+ return sourceVmName;
+ }
+
+ public Long getExistingVcenterId() {
+ return existingVcenterId;
+ }
+
+ public String getVcenter() {
+ return vcenter;
+ }
+
+ public String getDatacenterName() {
+ return datacenterName;
+ }
+
+ public String getSourceCluster() {
+ return sourceCluster;
+ }
+
+ public String getSourceHost() {
+ return sourceHost;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public Long getConvertInstanceHostId() {
+ return convertInstanceHostId;
+ }
+
+ public Long getStoragePoolId() {
+ return storagePoolId;
+ }
+
+ public Long getServiceOfferingId() {
+ return serviceOfferingId;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Map getDetails() {
+ Map params = new HashMap<>();
+ if (MapUtils.isEmpty(details)) {
+ return params;
+ }
+
+ for (Object value : details.values()) {
+ if (!(value instanceof Map)) {
+ continue;
+ }
+ Map detailMap = (Map)value;
+ for (Map.Entry entry : detailMap.entrySet()) {
+ if (entry.getKey() != null && entry.getValue() != null) {
+ params.put(entry.getKey().toString(), entry.getValue().toString());
+ }
+ }
+ }
+ return params;
+ }
+
+ @Override
+ public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException,
+ ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
+ VmwareCbtMigrationPreflightResponse response = vmwareCbtMigrationManager.checkVmwareCbtMigrationPrerequisites(this);
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ Account account = CallContext.current().getCallingAccount();
+ return account == null ? Account.ACCOUNT_ID_SYSTEM : account.getId();
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CutoverVmwareCbtMigrationCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CutoverVmwareCbtMigrationCmd.java
new file mode 100644
index 000000000000..61aedf31f4a2
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CutoverVmwareCbtMigrationCmd.java
@@ -0,0 +1,110 @@
+// 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.
+package org.apache.cloudstack.api.command.admin.vm;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseAsyncCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ResponseObject;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.VmwareCbtMigrationResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.VmwareCbtMigrationManager;
+
+import com.cloud.event.EventTypes;
+import com.cloud.exception.ConcurrentOperationException;
+import com.cloud.exception.InsufficientCapacityException;
+import com.cloud.exception.NetworkRuleConflictException;
+import com.cloud.exception.ResourceAllocationException;
+import com.cloud.exception.ResourceUnavailableException;
+import com.cloud.user.Account;
+
+@APICommand(name = "cutoverVmwareCbtMigration",
+ description = "Perform final cutover for a VMware CBT migration",
+ responseObject = VmwareCbtMigrationResponse.class,
+ responseView = ResponseObject.ResponseView.Full,
+ requestHasSensitiveInfo = true,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin},
+ since = "4.24.0")
+public class CutoverVmwareCbtMigrationCmd extends BaseAsyncCmd {
+
+ @Inject
+ public VmwareCbtMigrationManager vmwareCbtMigrationManager;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = VmwareCbtMigrationResponse.class,
+ required = true, description = "the VMware CBT migration ID")
+ private Long id;
+
+ @Parameter(name = ApiConstants.USERNAME, type = CommandType.STRING,
+ description = "optional username override for the source vCenter. Stored external vCenter credentials are used when available")
+ private String username;
+
+ @Parameter(name = ApiConstants.PASSWORD, type = CommandType.STRING,
+ description = "optional password override for the source vCenter. Stored external vCenter credentials are used when available")
+ private String password;
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ @Override
+ public String getEventType() {
+ return EventTypes.EVENT_VMWARE_CBT_MIGRATION_CUTOVER;
+ }
+
+ @Override
+ public String getEventDescription() {
+ return String.format("Cutting over VMware CBT migration: %s", id);
+ }
+
+ @Override
+ public String getSyncObjType() {
+ return BaseAsyncCmd.migrationSyncObject;
+ }
+
+ @Override
+ public Long getSyncObjId() {
+ return id;
+ }
+
+ @Override
+ public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException,
+ ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
+ VmwareCbtMigrationResponse response = vmwareCbtMigrationManager.cutoverVmwareCbtMigration(this);
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ Account account = CallContext.current().getCallingAccount();
+ return account == null ? Account.ACCOUNT_ID_SYSTEM : account.getId();
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeleteVmwareCbtMigrationCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeleteVmwareCbtMigrationCmd.java
new file mode 100644
index 000000000000..67163814b23a
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeleteVmwareCbtMigrationCmd.java
@@ -0,0 +1,81 @@
+// 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.
+package org.apache.cloudstack.api.command.admin.vm;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.SuccessResponse;
+import org.apache.cloudstack.api.response.VmwareCbtMigrationResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.VmwareCbtMigrationManager;
+
+import com.cloud.exception.ConcurrentOperationException;
+import com.cloud.exception.InsufficientCapacityException;
+import com.cloud.exception.NetworkRuleConflictException;
+import com.cloud.exception.ResourceAllocationException;
+import com.cloud.exception.ResourceUnavailableException;
+import com.cloud.user.Account;
+
+@APICommand(name = "deleteVmwareCbtMigration",
+ description = "Deletes a failed, cancelled or completed VMware CBT migration record. Failed and cancelled migrations can optionally clean up replicated target disks; completed migrations are record-only deletes.",
+ responseObject = SuccessResponse.class,
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin},
+ since = "4.24.0")
+public class DeleteVmwareCbtMigrationCmd extends BaseCmd {
+
+ @Inject
+ public VmwareCbtMigrationManager vmwareCbtMigrationManager;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = VmwareCbtMigrationResponse.class,
+ required = true, description = "the VMware CBT migration ID")
+ private Long id;
+
+ @Parameter(name = ApiConstants.CLEANUP, type = CommandType.BOOLEAN,
+ description = "If true, clean up replicated target disks before removing a failed or cancelled migration record. True by default. Ignored for completed migrations.")
+ private Boolean cleanup;
+
+ public Long getId() {
+ return id;
+ }
+
+ public boolean getCleanup() {
+ return cleanup == null || cleanup;
+ }
+
+ @Override
+ public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException,
+ ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
+ boolean result = vmwareCbtMigrationManager.deleteVmwareCbtMigration(this);
+ SuccessResponse response = new SuccessResponse(getCommandName());
+ response.setSuccess(result);
+ setResponseObject(response);
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ Account account = CallContext.current().getCallingAccount();
+ return account == null ? Account.ACCOUNT_ID_SYSTEM : account.getId();
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceCmd.java
index 3284dbafe7ca..b42692ce4cf7 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceCmd.java
@@ -115,7 +115,7 @@ public class ImportUnmanagedInstanceCmd extends BaseAsyncCmd {
@Parameter(name = ApiConstants.TEMPLATE_ID,
type = CommandType.UUID,
entityType = TemplateResponse.class,
- description = "The ID of the Template for the Instance")
+ description = "The ID of the Template for the Instance. Required when the instance to import is an unmanaged KVM Instance.")
private Long templateId;
@Parameter(name = ApiConstants.SERVICE_OFFERING_ID,
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java
index db7dcc3fb44f..1d9b9504750e 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java
@@ -57,6 +57,24 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd {
@Inject
public VmImportService vmImportService;
+ public enum VmwareMigrationMode {
+ OVF,
+ VDDK,
+ CBT;
+
+ public static VmwareMigrationMode fromValue(String value, boolean useVddkFallback) {
+ if (StringUtils.isBlank(value)) {
+ return useVddkFallback ? VDDK : OVF;
+ }
+ for (VmwareMigrationMode mode : values()) {
+ if (mode.name().equalsIgnoreCase(value)) {
+ return mode;
+ }
+ }
+ throw new IllegalArgumentException(String.format("Unsupported VMware migration mode: %s", value));
+ }
+ }
+
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@@ -92,7 +110,7 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd {
@Parameter(name = ApiConstants.DISK_PATH,
type = CommandType.STRING,
- description = "path of the disk image")
+ description = "path of the disk image. It is the file name on file based storage pools (NFS, Local, SharedMountPoint), and the image name on RBD storage pools")
private String diskPath;
@Parameter(name = ApiConstants.IMPORT_SOURCE,
@@ -153,7 +171,8 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd {
private Long importInstanceHostId;
@Parameter(name = ApiConstants.CONVERT_INSTANCE_STORAGE_POOL_ID, type = CommandType.UUID, entityType = StoragePoolResponse.class,
- description = "(only for importing VMs from VMware to KVM) optional - the temporary storage pool to perform the virt-v2v migration from VMware to KVM.")
+ description = "(only for importing VMs from VMware to KVM) optional - the temporary storage pool to perform the virt-v2v migration from VMware to KVM. " +
+ "When " + ApiConstants.USE_VDDK + " and " + ApiConstants.FORCE_CONVERT_TO_POOL + " are true, this can be an RBD primary storage pool for direct RBD import.")
private Long convertStoragePoolId;
@Parameter(name = ApiConstants.FORCE_MS_TO_IMPORT_VM_FILES, type = CommandType.BOOLEAN,
@@ -183,9 +202,18 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd {
type = CommandType.BOOLEAN,
since = "4.22.1",
description = "(only for importing VMs from VMware to KVM) optional - if true, uses VDDK on the KVM conversion host for converting the VM. " +
- "This parameter is mutually exclusive with " + ApiConstants.FORCE_MS_TO_IMPORT_VM_FILES + ".")
+ "This parameter is mutually exclusive with " + ApiConstants.FORCE_MS_TO_IMPORT_VM_FILES + ". " +
+ "With " + ApiConstants.FORCE_CONVERT_TO_POOL + "=true and an RBD conversion pool, the source VMware VM must be powered off and " +
+ "the conversion host must support qemu RBD access and in-place virt-v2v finalization.")
private Boolean useVddk;
+ @Parameter(name = ApiConstants.VMWARE_MIGRATION_MODE,
+ type = CommandType.STRING,
+ since = "4.22.1",
+ description = "(only for importing VMs from VMware to KVM) optional - migration mode to use. Valid values are OVF, VDDK, and CBT. " +
+ "When omitted, CloudStack preserves the existing usevddk behavior.")
+ private String vmwareMigrationMode;
+
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
@@ -267,6 +295,10 @@ public boolean getUseVddk() {
return BooleanUtils.toBooleanDefaultIfNull(useVddk, true);
}
+ public String getVmwareMigrationMode() {
+ return vmwareMigrationMode;
+ }
+
public String getTmpPath() {
return tmpPath;
}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListVmwareCbtMigrationsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListVmwareCbtMigrationsCmd.java
new file mode 100644
index 000000000000..3ea5f53970cf
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListVmwareCbtMigrationsCmd.java
@@ -0,0 +1,109 @@
+// 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.
+package org.apache.cloudstack.api.command.admin.vm;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseListCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ResponseObject;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.AccountResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.api.response.VmwareCbtMigrationResponse;
+import org.apache.cloudstack.api.response.ZoneResponse;
+import org.apache.cloudstack.vm.VmwareCbtMigrationManager;
+
+import com.cloud.exception.ConcurrentOperationException;
+import com.cloud.exception.InsufficientCapacityException;
+import com.cloud.exception.NetworkRuleConflictException;
+import com.cloud.exception.ResourceAllocationException;
+import com.cloud.exception.ResourceUnavailableException;
+
+@APICommand(name = "listVmwareCbtMigrations",
+ description = "List VMware CBT based migration sessions",
+ responseObject = VmwareCbtMigrationResponse.class,
+ responseView = ResponseObject.ResponseView.Full,
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin},
+ since = "4.24.0")
+public class ListVmwareCbtMigrationsCmd extends BaseListCmd {
+
+ @Inject
+ public VmwareCbtMigrationManager vmwareCbtMigrationManager;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = VmwareCbtMigrationResponse.class,
+ description = "the VMware CBT migration ID")
+ private Long id;
+
+ @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class,
+ description = "the destination zone ID")
+ private Long zoneId;
+
+ @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class,
+ description = "the account ID")
+ private Long accountId;
+
+ @Parameter(name = ApiConstants.VCENTER, type = CommandType.STRING,
+ description = "the source VMware vCenter")
+ private String vcenter;
+
+ @Parameter(name = ApiConstants.SOURCE_VM_NAME, type = CommandType.STRING,
+ description = "the source VMware VM name")
+ private String sourceVmName;
+
+ @Parameter(name = ApiConstants.STATE, type = CommandType.STRING,
+ description = "the migration state")
+ private String state;
+
+ public Long getId() {
+ return id;
+ }
+
+ public Long getZoneId() {
+ return zoneId;
+ }
+
+ public Long getAccountId() {
+ return accountId;
+ }
+
+ public String getVcenter() {
+ return vcenter;
+ }
+
+ public String getSourceVmName() {
+ return sourceVmName;
+ }
+
+ public String getState() {
+ return state;
+ }
+
+ @Override
+ public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException,
+ ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
+ ListResponse response = vmwareCbtMigrationManager.listVmwareCbtMigrations(this);
+ response.setResponseName(getCommandName());
+ response.setObjectName("vmwarecbtmigration");
+ setResponseObject(response);
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/StartVmwareCbtMigrationCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/StartVmwareCbtMigrationCmd.java
new file mode 100644
index 000000000000..0746e40c34ca
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/StartVmwareCbtMigrationCmd.java
@@ -0,0 +1,361 @@
+// 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.
+package org.apache.cloudstack.api.command.admin.vm;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseAsyncCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ResponseObject;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.ClusterResponse;
+import org.apache.cloudstack.api.response.DomainResponse;
+import org.apache.cloudstack.api.response.GuestOSResponse;
+import org.apache.cloudstack.api.response.HostResponse;
+import org.apache.cloudstack.api.response.ProjectResponse;
+import org.apache.cloudstack.api.response.ServiceOfferingResponse;
+import org.apache.cloudstack.api.response.StoragePoolResponse;
+import org.apache.cloudstack.api.response.TemplateResponse;
+import org.apache.cloudstack.api.response.VmwareCbtMigrationResponse;
+import org.apache.cloudstack.api.response.VmwareDatacenterResponse;
+import org.apache.cloudstack.api.response.ZoneResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.VmwareCbtMigrationManager;
+import org.apache.commons.collections.MapUtils;
+import org.apache.commons.lang.BooleanUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import com.cloud.event.EventTypes;
+import com.cloud.exception.ConcurrentOperationException;
+import com.cloud.exception.InsufficientCapacityException;
+import com.cloud.exception.InvalidParameterValueException;
+import com.cloud.exception.NetworkRuleConflictException;
+import com.cloud.exception.ResourceAllocationException;
+import com.cloud.exception.ResourceUnavailableException;
+import com.cloud.network.Network;
+import com.cloud.offering.DiskOffering;
+import com.cloud.user.Account;
+import com.cloud.utils.net.NetUtils;
+import com.cloud.vm.VmDetailConstants;
+
+@APICommand(name = "startVmwareCbtMigration",
+ description = "Start tracking a VMware CBT based migration session",
+ responseObject = VmwareCbtMigrationResponse.class,
+ responseView = ResponseObject.ResponseView.Full,
+ requestHasSensitiveInfo = true,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin},
+ since = "4.24.0")
+public class StartVmwareCbtMigrationCmd extends BaseAsyncCmd {
+
+ @Inject
+ public VmwareCbtMigrationManager vmwareCbtMigrationManager;
+
+ @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class,
+ required = true, description = "the destination zone ID")
+ private Long zoneId;
+
+ @Parameter(name = ApiConstants.CLUSTER_ID, type = CommandType.UUID, entityType = ClusterResponse.class,
+ required = true, description = "the destination KVM cluster ID")
+ private Long clusterId;
+
+ @Parameter(name = ApiConstants.DISPLAY_NAME, type = CommandType.STRING,
+ description = "the display name of the migrated VM")
+ private String displayName;
+
+ @Parameter(name = ApiConstants.HOST_NAME, type = CommandType.STRING,
+ description = "the host name of the migrated VM")
+ private String hostName;
+
+ @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING,
+ description = "optional account for the migrated VM. Must be used with domainid")
+ private String accountName;
+
+ @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class,
+ description = "import the migrated VM to the specified domain")
+ private Long domainId;
+
+ @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class,
+ description = "import the migrated VM for the project")
+ private Long projectId;
+
+ @Parameter(name = ApiConstants.TEMPLATE_ID, type = CommandType.UUID, entityType = TemplateResponse.class,
+ description = "the template ID for the migrated VM")
+ private Long templateId;
+
+ @Parameter(name = ApiConstants.SERVICE_OFFERING_ID, type = CommandType.UUID, entityType = ServiceOfferingResponse.class,
+ required = true, description = "the service offering for the migrated VM")
+ private Long serviceOfferingId;
+
+ @Parameter(name = ApiConstants.OS_ID, type = CommandType.UUID, entityType = GuestOSResponse.class,
+ description = "optional guest OS ID for the migrated VM")
+ private Long guestOsId;
+
+ @Parameter(name = ApiConstants.SOURCE_VM_NAME, type = CommandType.STRING,
+ required = true, description = "the source VMware VM name")
+ private String sourceVmName;
+
+ @Parameter(name = ApiConstants.EXISTING_VCENTER_ID, type = CommandType.UUID, entityType = VmwareDatacenterResponse.class,
+ description = "UUID of a linked existing vCenter")
+ private Long existingVcenterId;
+
+ @Parameter(name = ApiConstants.VCENTER, type = CommandType.STRING,
+ description = "the source vCenter IP address or FQDN")
+ private String vcenter;
+
+ @Parameter(name = ApiConstants.DATACENTER_NAME, type = CommandType.STRING,
+ description = "the source VMware datacenter name")
+ private String datacenterName;
+
+ @Parameter(name = ApiConstants.CLUSTER_NAME, type = CommandType.STRING,
+ description = "the source VMware cluster name")
+ private String sourceCluster;
+
+ @Parameter(name = ApiConstants.HOST_IP, type = CommandType.STRING,
+ description = "the source VMware ESXi host IP address or FQDN")
+ private String sourceHost;
+
+ @Parameter(name = ApiConstants.USERNAME, type = CommandType.STRING,
+ description = "the username for the source vCenter")
+ private String username;
+
+ @Parameter(name = ApiConstants.PASSWORD, type = CommandType.STRING,
+ description = "the password for the source vCenter")
+ private String password;
+
+ @Parameter(name = ApiConstants.CONVERT_INSTANCE_HOST_ID, type = CommandType.UUID, entityType = HostResponse.class,
+ description = "optional KVM host to perform virt-v2v conversion and CBT block replication")
+ private Long convertInstanceHostId;
+
+ @Parameter(name = ApiConstants.CONVERT_INSTANCE_STORAGE_POOL_ID, type = CommandType.UUID, entityType = StoragePoolResponse.class,
+ description = "optional primary storage pool for converted disks")
+ private Long storagePoolId;
+
+ @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP,
+ description = "optional migration details, including VDDK settings such as vddk.lib.dir, vddk.transports and vddk.thumbprint")
+ private Map details;
+
+ @Parameter(name = ApiConstants.NIC_NETWORK_LIST, type = CommandType.MAP,
+ description = "VM NIC to network id mapping using keys NIC and network")
+ private Map nicNetworkList;
+
+ @Parameter(name = ApiConstants.NIC_IP_ADDRESS_LIST, type = CommandType.MAP,
+ description = "VM NIC to ip address mapping using keys NIC, ip4Address")
+ private Map nicIpAddressList;
+
+ @Parameter(name = ApiConstants.DATADISK_OFFERING_LIST, type = CommandType.MAP,
+ description = "datadisk to disk-offering mapping using keys disk and diskOffering")
+ private Map dataDiskToDiskOfferingList;
+
+ @Parameter(name = ApiConstants.FORCED, type = CommandType.BOOLEAN,
+ description = "import despite duplicate NIC MAC addresses; CloudStack generates a new MAC address when a duplicate exists")
+ private Boolean forced;
+
+ public Long getZoneId() {
+ return zoneId;
+ }
+
+ public Long getClusterId() {
+ return clusterId;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public String getHostName() {
+ return hostName;
+ }
+
+ public String getAccountName() {
+ return accountName;
+ }
+
+ public Long getDomainId() {
+ return domainId;
+ }
+
+ public Long getProjectId() {
+ return projectId;
+ }
+
+ public Long getTemplateId() {
+ return templateId;
+ }
+
+ public Long getServiceOfferingId() {
+ return serviceOfferingId;
+ }
+
+ public Long getGuestOsId() {
+ return guestOsId;
+ }
+
+ public String getSourceVmName() {
+ return sourceVmName;
+ }
+
+ public Long getExistingVcenterId() {
+ return existingVcenterId;
+ }
+
+ public String getVcenter() {
+ return vcenter;
+ }
+
+ public String getDatacenterName() {
+ return datacenterName;
+ }
+
+ public String getSourceCluster() {
+ return sourceCluster;
+ }
+
+ public String getSourceHost() {
+ return sourceHost;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public Long getConvertInstanceHostId() {
+ return convertInstanceHostId;
+ }
+
+ public Long getStoragePoolId() {
+ return storagePoolId;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Map getDetails() {
+ Map params = new HashMap<>();
+ if (MapUtils.isEmpty(details)) {
+ return params;
+ }
+
+ for (Object value : details.values()) {
+ if (!(value instanceof Map)) {
+ continue;
+ }
+ Map detailMap = (Map)value;
+ for (Map.Entry entry : detailMap.entrySet()) {
+ if (entry.getKey() != null && entry.getValue() != null) {
+ params.put(entry.getKey().toString(), entry.getValue().toString());
+ }
+ }
+ }
+ return params;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Map getNicNetworkList() {
+ Map nicNetworkMap = new HashMap<>();
+ if (MapUtils.isNotEmpty(nicNetworkList)) {
+ for (Map entry : (Collection>) nicNetworkList.values()) {
+ String nic = entry.get(VmDetailConstants.NIC);
+ String networkUuid = entry.get(VmDetailConstants.NETWORK);
+ if (StringUtils.isAnyEmpty(nic, networkUuid) || _entityMgr.findByUuid(Network.class, networkUuid) == null) {
+ throw new InvalidParameterValueException(String.format("Network ID: %s for NIC ID: %s is invalid", networkUuid, nic));
+ }
+ nicNetworkMap.put(nic, _entityMgr.findByUuid(Network.class, networkUuid).getId());
+ }
+ }
+ return nicNetworkMap;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Map getNicIpAddressList() {
+ Map nicIpAddressMap = new HashMap<>();
+ if (MapUtils.isNotEmpty(nicIpAddressList)) {
+ for (Map entry : (Collection>) nicIpAddressList.values()) {
+ String nic = entry.get(VmDetailConstants.NIC);
+ String ipAddress = StringUtils.defaultIfEmpty(entry.get(VmDetailConstants.IP4_ADDRESS), null);
+ if (StringUtils.isEmpty(nic)) {
+ throw new InvalidParameterValueException(String.format("NIC ID: '%s' is invalid for IP address mapping", nic));
+ }
+ if (StringUtils.isEmpty(ipAddress)) {
+ throw new InvalidParameterValueException(String.format("Empty address for NIC ID: %s is invalid", nic));
+ }
+ if (StringUtils.isNotEmpty(ipAddress) && !ipAddress.equals("auto") && !NetUtils.isValidIp4(ipAddress)) {
+ throw new InvalidParameterValueException(String.format("IP address '%s' for NIC ID: %s is invalid", ipAddress, nic));
+ }
+ nicIpAddressMap.put(nic, new Network.IpAddresses(ipAddress, null));
+ }
+ }
+ return nicIpAddressMap;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Map getDataDiskToDiskOfferingList() {
+ Map dataDiskToDiskOfferingMap = new HashMap<>();
+ if (MapUtils.isNotEmpty(dataDiskToDiskOfferingList)) {
+ for (Map entry : (Collection>) dataDiskToDiskOfferingList.values()) {
+ String disk = entry.get(VmDetailConstants.DISK);
+ String offeringUuid = entry.get(VmDetailConstants.DISK_OFFERING);
+ if (StringUtils.isAnyEmpty(disk, offeringUuid) || _entityMgr.findByUuid(DiskOffering.class, offeringUuid) == null) {
+ throw new InvalidParameterValueException(String.format("Disk offering ID: %s for disk ID: %s is invalid", offeringUuid, disk));
+ }
+ dataDiskToDiskOfferingMap.put(disk, _entityMgr.findByUuid(DiskOffering.class, offeringUuid).getId());
+ }
+ }
+ return dataDiskToDiskOfferingMap;
+ }
+
+ public boolean isForced() {
+ return BooleanUtils.isTrue(forced);
+ }
+
+ @Override
+ public String getEventType() {
+ return EventTypes.EVENT_VMWARE_CBT_MIGRATION_START;
+ }
+
+ @Override
+ public String getEventDescription() {
+ return String.format("Starting VMware CBT migration for source VM: %s", sourceVmName);
+ }
+
+ @Override
+ public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException,
+ ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
+ VmwareCbtMigrationResponse response = vmwareCbtMigrationManager.startVmwareCbtMigration(this);
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ Long accountId = _accountService.finalizeAccountId(accountName, domainId, projectId, true);
+ if (accountId != null) {
+ return accountId;
+ }
+ Account account = CallContext.current().getCallingAccount();
+ return account == null ? Account.ACCOUNT_ID_SYSTEM : account.getId();
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/SyncVmwareCbtMigrationCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/SyncVmwareCbtMigrationCmd.java
new file mode 100644
index 000000000000..7589360564a5
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/SyncVmwareCbtMigrationCmd.java
@@ -0,0 +1,110 @@
+// 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.
+package org.apache.cloudstack.api.command.admin.vm;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseAsyncCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ResponseObject;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.VmwareCbtMigrationResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.VmwareCbtMigrationManager;
+
+import com.cloud.event.EventTypes;
+import com.cloud.exception.ConcurrentOperationException;
+import com.cloud.exception.InsufficientCapacityException;
+import com.cloud.exception.NetworkRuleConflictException;
+import com.cloud.exception.ResourceAllocationException;
+import com.cloud.exception.ResourceUnavailableException;
+import com.cloud.user.Account;
+
+@APICommand(name = "syncVmwareCbtMigration",
+ description = "Run a VMware CBT delta synchronization cycle",
+ responseObject = VmwareCbtMigrationResponse.class,
+ responseView = ResponseObject.ResponseView.Full,
+ requestHasSensitiveInfo = true,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin},
+ since = "4.24.0")
+public class SyncVmwareCbtMigrationCmd extends BaseAsyncCmd {
+
+ @Inject
+ public VmwareCbtMigrationManager vmwareCbtMigrationManager;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = VmwareCbtMigrationResponse.class,
+ required = true, description = "the VMware CBT migration ID")
+ private Long id;
+
+ @Parameter(name = ApiConstants.USERNAME, type = CommandType.STRING,
+ description = "optional username override for the source vCenter. Stored external vCenter credentials are used when available")
+ private String username;
+
+ @Parameter(name = ApiConstants.PASSWORD, type = CommandType.STRING,
+ description = "optional password override for the source vCenter. Stored external vCenter credentials are used when available")
+ private String password;
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ @Override
+ public String getEventType() {
+ return EventTypes.EVENT_VMWARE_CBT_MIGRATION_SYNC;
+ }
+
+ @Override
+ public String getEventDescription() {
+ return String.format("Synchronizing VMware CBT migration: %s", id);
+ }
+
+ @Override
+ public String getSyncObjType() {
+ return BaseAsyncCmd.migrationSyncObject;
+ }
+
+ @Override
+ public Long getSyncObjId() {
+ return id;
+ }
+
+ @Override
+ public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException,
+ ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
+ VmwareCbtMigrationResponse response = vmwareCbtMigrationManager.syncVmwareCbtMigration(this);
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ Account account = CallContext.current().getCallingAccount();
+ return account == null ? Account.ACCOUNT_ID_SYSTEM : account.getId();
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/ImportVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/ImportVolumeCmd.java
index 50f4b9c1fbe5..db5961ac7f8b 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/ImportVolumeCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/ImportVolumeCmd.java
@@ -63,7 +63,7 @@ public class ImportVolumeCmd extends BaseAsyncCmd {
@Parameter(name = ApiConstants.PATH,
type = BaseCmd.CommandType.STRING,
required = true,
- description = "the path of the volume")
+ description = "the path of the volume. It is the file name on file based storage pools (NFS, Local, SharedMountPoint), and the image name on RBD storage pools")
private String path;
@Parameter(name = ApiConstants.NAME,
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java
index 2b934a60da7a..a31cf1a435d2 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java
@@ -24,6 +24,7 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -60,7 +61,9 @@
import static com.cloud.network.Network.Service.NetworkACL;
import static com.cloud.network.Network.Service.UserData;
import static com.cloud.network.Network.Service.Gateway;
+import static com.cloud.network.Network.Service.Vpn;
+import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetris;
import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisNatted;
import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisRouted;
import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNsxWithoutLb;
@@ -192,7 +195,7 @@ public List getSupportedServices() {
NetworkACL.getName(),
UserData.getName()
));
- if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode())) {
+ if (isNetrisNatted(getProvider(), getNetworkMode())) {
supportedServices.addAll(Arrays.asList(
StaticNat.getName(),
SourceNat.getName(),
@@ -201,9 +204,13 @@ public List getSupportedServices() {
if (NetworkOffering.NetworkMode.ROUTED.name().equalsIgnoreCase(getNetworkMode())) {
supportedServices.add(Gateway.getName());
}
+
if (getNsxSupportsLbService() || isNetrisNatted(getProvider(), getNetworkMode())) {
supportedServices.add(Lb.getName());
}
+ if (isNetrisNatted(getProvider(), getNetworkMode())) {
+ supportedServices.add(Vpn.getName());
+ }
}
return supportedServices;
}
@@ -213,7 +220,7 @@ public String getProvider() {
}
public String getNetworkMode() {
- return networkMode;
+ return isNetris(getProvider()) && Objects.isNull(networkMode) ? NetworkOffering.NetworkMode.NATTED.name() : networkMode;
}
public boolean getNsxSupportsLbService() {
@@ -249,7 +256,7 @@ public Map> getServiceProviders() {
private void getServiceProviderMapForExternalProvider(Map> serviceProviderMap, String provider) {
List unsupportedServices = new ArrayList<>(List.of("Vpn", "BaremetalPxeService", "SecurityGroup", "Connectivity", "Firewall"));
- if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode())) {
+ if (isNetrisNatted(getProvider(), getNetworkMode())) {
unsupportedServices.add("Gateway");
}
List routerSupported = List.of("Dhcp", "Dns", "UserData");
@@ -259,7 +266,7 @@ private void getServiceProviderMapForExternalProvider(Map>
continue;
if (routerSupported.contains(service))
serviceProviderMap.put(service, List.of(VirtualRouterProvider.Type.VPCVirtualRouter.name()));
- else if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode()) ||
+ else if (isNetrisNatted(getProvider(), getNetworkMode()) ||
Stream.of(NetworkACL.getName(), Gateway.getName()).anyMatch(s -> s.equalsIgnoreCase(service))) {
serviceProviderMap.put(service, List.of(provider));
}
@@ -269,7 +276,20 @@ else if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMo
}
}
+ @SuppressWarnings("unchecked")
public Map> getServiceCapabilityList() {
+ // For Netris NATTED, Gateway service is excluded so the redundant-router capability
+ // must reference SourceNat (not Gateway).
+ if (serviceCapabilityList != null
+ && isNetrisNatted(getProvider(), getNetworkMode())) {
+ for (Object capEntry : serviceCapabilityList.values()) {
+ Map cap = (Map) capEntry;
+ if (Network.Capability.RedundantRouter.getName().equalsIgnoreCase(cap.get("capabilitytype"))
+ && Gateway.getName().equalsIgnoreCase(cap.get("service"))) {
+ cap.put("service", SourceNat.getName());
+ }
+ }
+ }
return serviceCapabilityList;
}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/AddMemberToInstanceBootGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/AddMemberToInstanceBootGroupCmd.java
new file mode 100644
index 000000000000..901ca91ff83c
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/AddMemberToInstanceBootGroupCmd.java
@@ -0,0 +1,107 @@
+// 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.
+
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.user.UserCmd;
+import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse;
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.api.response.InstanceGroupResponse;
+import org.apache.cloudstack.api.response.UserVmResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService;
+
+@APICommand(name = "addMemberToInstanceBootGroup",
+ description = "Adds an Instance or instance group to an instance boot group. Exactly one of virtualmachineid or instancegroupid must be specified.",
+ since = "24.0",
+ responseObject = InstanceBootGroupMemberResponse.class,
+ entityType = {InstanceBootGroupMember.class},
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class AddMemberToInstanceBootGroupCmd extends BaseCmd implements UserCmd {
+
+ @Inject
+ InstanceBootGroupService instanceBootGroupService;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, required = true, description = "The ID of the instance boot group")
+ private Long id;
+
+ @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, description = "The ID of the Instance to add (exclusive with instancegroupid)")
+ private Long virtualMachineId;
+
+ @Parameter(name = ApiConstants.INSTANCE_GROUP_ID, type = CommandType.UUID, entityType = InstanceGroupResponse.class, description = "The ID of the Instance Group to add (exclusive with virtualmachineid)")
+ private Long instanceGroupId;
+
+ @Parameter(name = ApiConstants.BOOT_ORDER, type = CommandType.INTEGER, required = true,
+ description = "The boot order value for this member (0 or greater; non-contiguous values are allowed). "
+ + "Any existing member already at or past this value is shifted one slot later to make room")
+ private int order;
+
+ public Long getId() {
+ return id;
+ }
+
+ public Long getVirtualMachineId() {
+ return virtualMachineId;
+ }
+
+ public Long getInstanceGroupId() {
+ return instanceGroupId;
+ }
+
+ public int getOrder() {
+ return order;
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return CallContext.current().getCallingAccount().getId();
+ }
+
+ @Override
+ public Long getApiResourceId() {
+ return id;
+ }
+
+ @Override
+ public ApiCommandResourceType getApiResourceType() {
+ return ApiCommandResourceType.InstanceBootGroup;
+ }
+
+ @Override
+ public void execute() {
+ InstanceBootGroupMember result = instanceBootGroupService.addMemberToInstanceBootGroup(this);
+ if (result == null) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to add member to instance boot group");
+ }
+ InstanceBootGroupMemberResponse response = instanceBootGroupService.createInstanceBootGroupMemberResponse(result);
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupCmd.java
new file mode 100644
index 000000000000..29c0f8a344f1
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupCmd.java
@@ -0,0 +1,142 @@
+// 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.
+
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.user.UserCmd;
+import org.apache.cloudstack.api.response.DomainResponse;
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.api.response.ProjectResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService;
+
+@APICommand(name = "createInstanceBootGroup",
+ description = "Creates an Instance Boot Group",
+ since = "24.0",
+ responseObject = InstanceBootGroupResponse.class,
+ entityType = {InstanceBootGroup.class},
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class CreateInstanceBootGroupCmd extends BaseCmd implements UserCmd {
+
+ @Inject
+ InstanceBootGroupService instanceBootGroupService;
+
+ @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "The name of the Instance Boot Group")
+ private String name;
+
+ @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "The description of the Instance Boot Group")
+ private String description;
+
+ @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "The account of the Instance Boot Group. Must be used with domainid")
+ private String accountName;
+
+ @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "The domain ID of the account owning the Instance Boot Group")
+ private Long domainId;
+
+ @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "The project of the Instance Boot Group")
+ private Long projectId;
+
+ @Parameter(name = ApiConstants.READINESS_ATTEMPT_TIMEOUT_SECONDS, type = CommandType.LONG,
+ description = "Per-boot-group override of the global timeout (seconds) for each readiness retry attempt")
+ private Long readinessAttemptTimeoutSeconds;
+
+ @Parameter(name = ApiConstants.READINESS_MAX_RETRY_ATTEMPTS, type = CommandType.LONG,
+ description = "Per-boot-group override of the global maximum number of readiness retry attempts")
+ private Long readinessMaxRetryAttempts;
+
+ @Parameter(name = ApiConstants.READINESS_REBOOT_ON_RETRY, type = CommandType.BOOLEAN,
+ description = "Per-boot-group override of whether an instance is rebooted between readiness retry attempts")
+ private Boolean readinessRebootOnRetry;
+
+ @Parameter(name = ApiConstants.READINESS_INITIAL_DELAY_SECONDS, type = CommandType.LONG,
+ description = "Per-boot-group override of the global delay (seconds) after starting or rebooting an instance before its first readiness check of that attempt")
+ private Long readinessInitialDelaySeconds;
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public String getAccountName() {
+ return accountName;
+ }
+
+ public Long getDomainId() {
+ return domainId;
+ }
+
+ public Long getProjectId() {
+ return projectId;
+ }
+
+ public Long getReadinessAttemptTimeoutSeconds() {
+ return readinessAttemptTimeoutSeconds;
+ }
+
+ public Long getReadinessMaxRetryAttempts() {
+ return readinessMaxRetryAttempts;
+ }
+
+ public Boolean getReadinessRebootOnRetry() {
+ return readinessRebootOnRetry;
+ }
+
+ public Long getReadinessInitialDelaySeconds() {
+ return readinessInitialDelaySeconds;
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ Long accountId = _accountService.finalizeAccountId(accountName, domainId, projectId, true);
+ if (accountId == null) {
+ return CallContext.current().getCallingAccount().getId();
+ }
+ return accountId;
+ }
+
+ @Override
+ public ApiCommandResourceType getApiResourceType() {
+ return ApiCommandResourceType.InstanceBootGroup;
+ }
+
+ @Override
+ public void execute() {
+ InstanceBootGroup result = instanceBootGroupService.createInstanceBootGroup(this);
+ if (result == null) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create instance boot group");
+ }
+ InstanceBootGroupResponse response = instanceBootGroupService.createInstanceBootGroupResponse(result.getId());
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupReadinessRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupReadinessRuleCmd.java
new file mode 100644
index 000000000000..43c5dccc5126
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupReadinessRuleCmd.java
@@ -0,0 +1,132 @@
+// 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.
+
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import java.util.Collection;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.user.UserCmd;
+import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse;
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.api.response.InstanceGroupResponse;
+import org.apache.cloudstack.api.response.UserVmResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService;
+
+@APICommand(name = "createInstanceBootGroupReadinessRule",
+ description = "Creates a readiness rule for an Instance or Instance Group that is a member (directly, or via its instance group) of an Instance Boot Group",
+ since = "24.0",
+ responseObject = InstanceBootGroupReadinessRuleResponse.class,
+ entityType = {InstanceBootGroupReadinessRule.class},
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class CreateInstanceBootGroupReadinessRuleCmd extends BaseCmd implements UserCmd {
+
+ @Inject
+ InstanceBootGroupService instanceBootGroupService;
+
+ @Parameter(name = ApiConstants.BOOT_GROUP_ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, required = true,
+ description = "The ID of the Instance Boot Group this rule belongs to")
+ private Long bootGroupId;
+
+ @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class,
+ description = "The ID of the Instance this rule applies to (exclusive with instancegroupid)")
+ private Long virtualMachineId;
+
+ @Parameter(name = ApiConstants.INSTANCE_GROUP_ID, type = CommandType.UUID, entityType = InstanceGroupResponse.class,
+ description = "The ID of the Instance Group this rule applies to (exclusive with virtualmachineid)")
+ private Long instanceGroupId;
+
+ @Parameter(name = ApiConstants.RULE_TYPE, type = CommandType.STRING, required = true,
+ description = "The readiness rule type: GuestAgentLiveness, Ping, PortCheck or MemberQuorum")
+ private String ruleType;
+
+ @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "The name of the readiness rule; auto-generated if not provided")
+ private String name;
+
+ @Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, description = "Whether the rule is enabled; defaults to true")
+ private Boolean enabled;
+
+ @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, description = "Rule-type-specific configuration, e.g. port/protocol, script, threshold_type/threshold_value")
+ private Map details;
+
+ public Long getBootGroupId() {
+ return bootGroupId;
+ }
+
+ public Long getVirtualMachineId() {
+ return virtualMachineId;
+ }
+
+ public Long getInstanceGroupId() {
+ return instanceGroupId;
+ }
+
+ public String getRuleType() {
+ return ruleType;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public boolean isEnabled() {
+ return enabled == null || enabled;
+ }
+
+ public Map getDetails() {
+ if (this.details == null || this.details.isEmpty()) {
+ return null;
+ }
+ Collection paramsCollection = this.details.values();
+ return (Map) (paramsCollection.toArray())[0];
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return CallContext.current().getCallingAccount().getId();
+ }
+
+ @Override
+ public ApiCommandResourceType getApiResourceType() {
+ return ApiCommandResourceType.InstanceBootGroupReadinessRule;
+ }
+
+ @Override
+ public void execute() {
+ InstanceBootGroupReadinessRule result = instanceBootGroupService.createInstanceBootGroupReadinessRule(this);
+ if (result == null) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create instance boot group readiness rule");
+ }
+ InstanceBootGroupReadinessRuleResponse response = instanceBootGroupService.createInstanceBootGroupReadinessRuleResponse(result);
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupCmd.java
new file mode 100644
index 000000000000..8c6c5d7fa3e4
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupCmd.java
@@ -0,0 +1,81 @@
+// 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.
+
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.user.UserCmd;
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.api.response.SuccessResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService;
+
+@APICommand(name = "deleteInstanceBootGroup",
+ description = "Deletes an Instance Boot Group",
+ since = "24.0",
+ responseObject = SuccessResponse.class,
+ entityType = {InstanceBootGroup.class},
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class DeleteInstanceBootGroupCmd extends BaseCmd implements UserCmd {
+
+ @Inject
+ InstanceBootGroupService instanceBootGroupService;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, required = true, description = "The ID of the Instance Boot Group")
+ private Long id;
+
+ public Long getId() {
+ return id;
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return CallContext.current().getCallingAccount().getId();
+ }
+
+ @Override
+ public Long getApiResourceId() {
+ return id;
+ }
+
+ @Override
+ public ApiCommandResourceType getApiResourceType() {
+ return ApiCommandResourceType.InstanceBootGroup;
+ }
+
+ @Override
+ public void execute() {
+ boolean result = instanceBootGroupService.deleteInstanceBootGroup(this);
+ if (!result) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete instance boot group");
+ }
+ SuccessResponse response = new SuccessResponse(getCommandName());
+ setResponseObject(response);
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupReadinessRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupReadinessRuleCmd.java
new file mode 100644
index 000000000000..1a8a1bc79e42
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupReadinessRuleCmd.java
@@ -0,0 +1,82 @@
+// 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.
+
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.user.UserCmd;
+import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse;
+import org.apache.cloudstack.api.response.SuccessResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService;
+
+@APICommand(name = "deleteInstanceBootGroupReadinessRule",
+ description = "Deletes an Instance Boot Group readiness rule",
+ since = "24.0",
+ responseObject = SuccessResponse.class,
+ entityType = {InstanceBootGroupReadinessRule.class},
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class DeleteInstanceBootGroupReadinessRuleCmd extends BaseCmd implements UserCmd {
+
+ @Inject
+ InstanceBootGroupService instanceBootGroupService;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupReadinessRuleResponse.class, required = true,
+ description = "The ID of the Instance Boot Group readiness rule")
+ private Long id;
+
+ public Long getId() {
+ return id;
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return CallContext.current().getCallingAccount().getId();
+ }
+
+ @Override
+ public Long getApiResourceId() {
+ return id;
+ }
+
+ @Override
+ public ApiCommandResourceType getApiResourceType() {
+ return ApiCommandResourceType.InstanceBootGroupReadinessRule;
+ }
+
+ @Override
+ public void execute() {
+ boolean result = instanceBootGroupService.deleteInstanceBootGroupReadinessRule(this);
+ if (!result) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete instance boot group readiness rule");
+ }
+ SuccessResponse response = new SuccessResponse(getCommandName());
+ setResponseObject(response);
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupMembersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupMembersCmd.java
new file mode 100644
index 000000000000..e8af5ddf4feb
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupMembersCmd.java
@@ -0,0 +1,96 @@
+// 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.
+
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import java.util.List;
+
+import javax.inject.Inject;
+
+import org.apache.commons.lang3.BooleanUtils;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseListCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.command.user.UserCmd;
+import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse;
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService;
+
+@APICommand(name = "listInstanceBootGroupMembers",
+ description = "Lists members of an Instance Boot Group, sorted by boot order",
+ since = "24.0",
+ responseObject = InstanceBootGroupMemberResponse.class,
+ entityType = {InstanceBootGroupMember.class},
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class ListInstanceBootGroupMembersCmd extends BaseListCmd implements UserCmd {
+
+ @Inject
+ InstanceBootGroupService instanceBootGroupService;
+
+ @Parameter(name = ApiConstants.BOOT_GROUP_ID, type = CommandType.UUID, required = true, entityType = InstanceBootGroupResponse.class, description = "The ID of the Instance Boot Group")
+ private Long bootGroupId;
+
+ @Parameter(name = ApiConstants.MEMBER_TYPE, type = CommandType.STRING, description = "Filter by member type: VirtualMachine or InstanceGroup")
+ private String memberType;
+
+ @Parameter(name = ApiConstants.DETAILS,
+ type = CommandType.LIST,
+ collectionType = CommandType.STRING,
+ description = "Comma separated list of additional details requested, value can be a list of [all, readiness, children]. "
+ + "Readiness fields are computed from cached check results (not a live re-check) and are omitted unless requested, since computing them is not free. "
+ + "Children returns the member Instances of InstanceGroup-type members (omitted for VirtualMachine-type members); combine with readiness to also include per-child readiness")
+ private List viewDetails;
+
+ @Parameter(name = ApiConstants.IGNORE_INSTANCE_STATE, type = CommandType.BOOLEAN,
+ description = "If true, readiness status/message reflect the last cached rule check regardless of the member's current instance state. "
+ + "If false (default), an Instance that isn't Running is always reported NotReady, even if its rules were last cached Ready")
+ private Boolean ignoreInstanceState;
+
+ public Long getBootGroupId() {
+ return bootGroupId;
+ }
+
+ public String getMemberType() {
+ return memberType;
+ }
+
+ public boolean isIgnoreInstanceState() {
+ return BooleanUtils.toBoolean(ignoreInstanceState);
+ }
+
+ public boolean isReadinessDetailRequested() {
+ return viewDetails != null && (viewDetails.contains("readiness") || viewDetails.contains("all"));
+ }
+
+ public boolean isChildrenDetailRequested() {
+ return viewDetails != null && (viewDetails.contains("children") || viewDetails.contains("all"));
+ }
+
+ @Override
+ public void execute() {
+ ListResponse response = instanceBootGroupService.listInstanceBootGroupMembers(this);
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupReadinessRulesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupReadinessRulesCmd.java
new file mode 100644
index 000000000000..34db56662efe
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupReadinessRulesCmd.java
@@ -0,0 +1,91 @@
+// 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.
+
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseListCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.command.user.UserCmd;
+import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse;
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.api.response.InstanceGroupResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.api.response.UserVmResponse;
+import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService;
+
+@APICommand(name = "listInstanceBootGroupReadinessRules",
+ description = "Lists readiness rules for an Instance Boot Group",
+ since = "24.0",
+ responseObject = InstanceBootGroupReadinessRuleResponse.class,
+ entityType = {InstanceBootGroupReadinessRule.class},
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class ListInstanceBootGroupReadinessRulesCmd extends BaseListCmd implements UserCmd {
+
+ @Inject
+ InstanceBootGroupService instanceBootGroupService;
+
+ @Parameter(name = ApiConstants.BOOT_GROUP_ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, required = true,
+ description = "The ID of the Instance Boot Group; listing is always scoped to one boot group")
+ private Long bootGroupId;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupReadinessRuleResponse.class, description = "List by readiness rule ID")
+ private Long id;
+
+ @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, description = "List rules for the Instance")
+ private Long virtualMachineId;
+
+ @Parameter(name = ApiConstants.INSTANCE_GROUP_ID, type = CommandType.UUID, entityType = InstanceGroupResponse.class, description = "List rules for the Instance Group")
+ private Long instanceGroupId;
+
+ @Parameter(name = ApiConstants.RULE_TYPE, type = CommandType.STRING, description = "Filter by readiness rule type")
+ private String ruleType;
+
+ public Long getBootGroupId() {
+ return bootGroupId;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public Long getVirtualMachineId() {
+ return virtualMachineId;
+ }
+
+ public Long getInstanceGroupId() {
+ return instanceGroupId;
+ }
+
+ public String getRuleType() {
+ return ruleType;
+ }
+
+ @Override
+ public void execute() {
+ ListResponse response = instanceBootGroupService.listInstanceBootGroupReadinessRules(this);
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupsCmd.java
new file mode 100644
index 000000000000..80bc29482b85
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupsCmd.java
@@ -0,0 +1,83 @@
+// 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.
+
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseListProjectAndAccountResourcesCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.command.user.UserCmd;
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.api.response.InstanceGroupResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.api.response.UserVmResponse;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService;
+
+@APICommand(name = "listInstanceBootGroups",
+ description = "Lists Instance Boot Groups",
+ since = "24.0",
+ responseObject = InstanceBootGroupResponse.class,
+ entityType = {InstanceBootGroup.class},
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class ListInstanceBootGroupsCmd extends BaseListProjectAndAccountResourcesCmd implements UserCmd {
+
+ @Inject
+ InstanceBootGroupService instanceBootGroupService;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, description = "List Instance Boot Group by ID")
+ private Long id;
+
+ @Parameter(name = ApiConstants.KEYWORD, type = CommandType.STRING, description = "List Instance Boot Groups by name keyword")
+ private String keyword;
+
+ @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, description = "List Instance Boot Groups that contain the Instance")
+ private Long virtualMachineId;
+
+ @Parameter(name = ApiConstants.INSTANCE_GROUP_ID, type = CommandType.UUID, entityType = InstanceGroupResponse.class, description = "List Instance Boot Groups that contain the Instance Group")
+ private Long instanceGroupId;
+
+ public Long getId() {
+ return id;
+ }
+
+ @Override
+ public String getKeyword() {
+ return keyword;
+ }
+
+ public Long getVirtualMachineId() {
+ return virtualMachineId;
+ }
+
+ public Long getInstanceGroupId() {
+ return instanceGroupId;
+ }
+
+ @Override
+ public void execute() {
+ ListResponse response = instanceBootGroupService.listInstanceBootGroups(this);
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/RebootInstanceBootGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/RebootInstanceBootGroupCmd.java
new file mode 100644
index 000000000000..624a736bbf8e
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/RebootInstanceBootGroupCmd.java
@@ -0,0 +1,107 @@
+// 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.
+
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseAsyncCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.user.UserCmd;
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService;
+
+import com.cloud.event.EventTypes;
+import com.cloud.utils.exception.CloudRuntimeException;
+
+@APICommand(name = "rebootInstanceBootGroup",
+ description = "Reboots all VMs in an Instance Boot Group: stops in reverse order then starts in forward order",
+ since = "24.0",
+ responseObject = InstanceBootGroupResponse.class,
+ entityType = {InstanceBootGroup.class},
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class RebootInstanceBootGroupCmd extends BaseAsyncCmd implements UserCmd {
+
+ @Inject
+ InstanceBootGroupService instanceBootGroupService;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, required = true, description = "The ID of the Instance Boot Group")
+ private Long id;
+
+ @Parameter(name = ApiConstants.FORCED, type = CommandType.BOOLEAN, required = false,
+ description = "Whether to force stop every Instance in the Instance Boot Group during the stop phase of the reboot (It is force-stopped and then started)")
+ private Boolean forced;
+
+ public Long getId() {
+ return id;
+ }
+
+ public boolean isForced() {
+ return forced != null && forced;
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return CallContext.current().getCallingAccount().getId();
+ }
+
+ @Override
+ public String getEventType() {
+ return EventTypes.EVENT_INSTANCE_BOOT_GROUP_REBOOT;
+ }
+
+ @Override
+ public String getEventDescription() {
+ return "Rebooting Instance Boot Group with ID: " + getResourceUuid(ApiConstants.ID);
+ }
+
+ @Override
+ public Long getApiResourceId() {
+ return id;
+ }
+
+ @Override
+ public ApiCommandResourceType getApiResourceType() {
+ return ApiCommandResourceType.InstanceBootGroup;
+ }
+
+ @Override
+ public void execute() {
+ InstanceBootGroup result;
+ try {
+ result = instanceBootGroupService.rebootInstanceBootGroup(this);
+ } catch (CloudRuntimeException e) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
+ }
+ if (result == null) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to reboot instance boot group");
+ }
+ InstanceBootGroupResponse response = instanceBootGroupService.createInstanceBootGroupResponse(result.getId());
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/RemoveInstanceBootGroupMemberCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/RemoveInstanceBootGroupMemberCmd.java
new file mode 100644
index 000000000000..847041742511
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/RemoveInstanceBootGroupMemberCmd.java
@@ -0,0 +1,81 @@
+// 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.
+
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.user.UserCmd;
+import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse;
+import org.apache.cloudstack.api.response.SuccessResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService;
+
+@APICommand(name = "removeInstanceBootGroupMember",
+ description = "Removes a member from an Instance Boot Group",
+ since = "24.0",
+ responseObject = SuccessResponse.class,
+ entityType = {InstanceBootGroupMember.class},
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class RemoveInstanceBootGroupMemberCmd extends BaseCmd implements UserCmd {
+
+ @Inject
+ InstanceBootGroupService instanceBootGroupService;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupMemberResponse.class, required = true, description = "The ID of the Instance Boot Group member to remove")
+ private Long id;
+
+ public Long getId() {
+ return id;
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return CallContext.current().getCallingAccount().getId();
+ }
+
+ @Override
+ public Long getApiResourceId() {
+ return instanceBootGroupService.getInstanceBootGroupIdForMember(getId());
+ }
+
+ @Override
+ public ApiCommandResourceType getApiResourceType() {
+ return ApiCommandResourceType.InstanceBootGroup;
+ }
+
+ @Override
+ public void execute() {
+ boolean result = instanceBootGroupService.removeInstanceBootGroupMember(this);
+ if (!result) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to remove member from instance boot group");
+ }
+ SuccessResponse response = new SuccessResponse(getCommandName());
+ setResponseObject(response);
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/StartInstanceBootGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/StartInstanceBootGroupCmd.java
new file mode 100644
index 000000000000..9351e4d16d5e
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/StartInstanceBootGroupCmd.java
@@ -0,0 +1,99 @@
+// 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.
+
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseAsyncCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.user.UserCmd;
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService;
+
+import com.cloud.event.EventTypes;
+import com.cloud.utils.exception.CloudRuntimeException;
+
+@APICommand(name = "startInstanceBootGroup",
+ description = "Starts all Instances in an Instance Boot Group in order (lowest boot order first). Instances within the same order tier start concurrently",
+ since = "24.0",
+ responseObject = InstanceBootGroupResponse.class,
+ entityType = {InstanceBootGroup.class},
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class StartInstanceBootGroupCmd extends BaseAsyncCmd implements UserCmd {
+
+ @Inject
+ InstanceBootGroupService instanceBootGroupService;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, required = true, description = "The ID of the Instance Boot Group")
+ private Long id;
+
+ public Long getId() {
+ return id;
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return CallContext.current().getCallingAccount().getId();
+ }
+
+ @Override
+ public String getEventType() {
+ return EventTypes.EVENT_INSTANCE_BOOT_GROUP_START;
+ }
+
+ @Override
+ public String getEventDescription() {
+ return "Starting Instance Boot Group with ID: " + getResourceUuid(ApiConstants.ID);
+ }
+
+ @Override
+ public Long getApiResourceId() {
+ return id;
+ }
+
+ @Override
+ public ApiCommandResourceType getApiResourceType() {
+ return ApiCommandResourceType.InstanceBootGroup;
+ }
+
+ @Override
+ public void execute() {
+ InstanceBootGroup result;
+ try {
+ result = instanceBootGroupService.startInstanceBootGroup(this);
+ } catch (CloudRuntimeException e) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
+ }
+ if (result == null) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to start instance boot group");
+ }
+ InstanceBootGroupResponse response = instanceBootGroupService.createInstanceBootGroupResponse(result.getId());
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/StopInstanceBootGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/StopInstanceBootGroupCmd.java
new file mode 100644
index 000000000000..a86a81e0b038
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/StopInstanceBootGroupCmd.java
@@ -0,0 +1,101 @@
+// 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.
+
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseAsyncCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.user.UserCmd;
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService;
+
+import com.cloud.event.EventTypes;
+
+@APICommand(name = "stopInstanceBootGroup",
+ description = "Stops all Instances in an Instance Boot Group in reverse order (highest boot order first). Continues through all tiers even if some Instances fail to stop",
+ since = "24.0",
+ responseObject = InstanceBootGroupResponse.class,
+ entityType = {InstanceBootGroup.class},
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class StopInstanceBootGroupCmd extends BaseAsyncCmd implements UserCmd {
+
+ @Inject
+ InstanceBootGroupService instanceBootGroupService;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, required = true, description = "The ID of the Instance Boot Group")
+ private Long id;
+
+ @Parameter(name = ApiConstants.FORCED, type = CommandType.BOOLEAN, required = false,
+ description = "Force stop every Instance in the Instance Boot Group (marked as Stopped even when the stop command fails to be sent to the backend, otherwise a force poweroff is attempted)")
+ private Boolean forced;
+
+ public Long getId() {
+ return id;
+ }
+
+ public boolean isForced() {
+ return forced != null && forced;
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return CallContext.current().getCallingAccount().getId();
+ }
+
+ @Override
+ public String getEventType() {
+ return EventTypes.EVENT_INSTANCE_BOOT_GROUP_STOP;
+ }
+
+ @Override
+ public String getEventDescription() {
+ return "Stopping Instance Boot Group with ID: " + getResourceUuid(ApiConstants.ID);
+ }
+
+ @Override
+ public Long getApiResourceId() {
+ return id;
+ }
+
+ @Override
+ public ApiCommandResourceType getApiResourceType() {
+ return ApiCommandResourceType.InstanceBootGroup;
+ }
+
+ @Override
+ public void execute() {
+ InstanceBootGroup result = instanceBootGroupService.stopInstanceBootGroup(this);
+ if (result == null) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to stop instance boot group");
+ }
+ InstanceBootGroupResponse response = instanceBootGroupService.createInstanceBootGroupResponse(result.getId());
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupCmd.java
new file mode 100644
index 000000000000..c700053dec0e
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupCmd.java
@@ -0,0 +1,127 @@
+// 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.
+
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.user.UserCmd;
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService;
+
+@APICommand(name = "updateInstanceBootGroup",
+ description = "Updates an Instance Boot Group",
+ since = "24.0",
+ responseObject = InstanceBootGroupResponse.class,
+ entityType = {InstanceBootGroup.class},
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class UpdateInstanceBootGroupCmd extends BaseCmd implements UserCmd {
+
+ @Inject
+ InstanceBootGroupService instanceBootGroupService;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, required = true, description = "The ID of the Instance Boot Group")
+ private Long id;
+
+ @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "New name for the Instance Boot Group")
+ private String name;
+
+ @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "New description for the Instance Boot Group")
+ private String description;
+
+ @Parameter(name = ApiConstants.READINESS_ATTEMPT_TIMEOUT_SECONDS, type = CommandType.LONG,
+ description = "Per-boot-group override of the global timeout (seconds) for each readiness retry attempt. Pass -1 to clear the override and fall back to the global setting")
+ private Long readinessAttemptTimeoutSeconds;
+
+ @Parameter(name = ApiConstants.READINESS_MAX_RETRY_ATTEMPTS, type = CommandType.LONG,
+ description = "Per-boot-group override of the global maximum number of readiness retry attempts. Pass -1 to clear the override and fall back to the global setting")
+ private Long readinessMaxRetryAttempts;
+
+ @Parameter(name = ApiConstants.READINESS_REBOOT_ON_RETRY, type = CommandType.BOOLEAN,
+ description = "Per-boot-group override of whether an instance is rebooted between readiness retry attempts")
+ private Boolean readinessRebootOnRetry;
+
+ @Parameter(name = ApiConstants.READINESS_INITIAL_DELAY_SECONDS, type = CommandType.LONG,
+ description = "Per-boot-group override of the global delay (seconds) after starting or rebooting an instance before its first readiness check of that attempt. Pass -1 to clear the override and fall back to the global setting")
+ private Long readinessInitialDelaySeconds;
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public Long getReadinessAttemptTimeoutSeconds() {
+ return readinessAttemptTimeoutSeconds;
+ }
+
+ public Long getReadinessMaxRetryAttempts() {
+ return readinessMaxRetryAttempts;
+ }
+
+ public Boolean getReadinessRebootOnRetry() {
+ return readinessRebootOnRetry;
+ }
+
+ public Long getReadinessInitialDelaySeconds() {
+ return readinessInitialDelaySeconds;
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return CallContext.current().getCallingAccount().getId();
+ }
+
+ @Override
+ public Long getApiResourceId() {
+ return id;
+ }
+
+ @Override
+ public ApiCommandResourceType getApiResourceType() {
+ return ApiCommandResourceType.InstanceBootGroup;
+ }
+
+ @Override
+ public void execute() {
+ InstanceBootGroup result = instanceBootGroupService.updateInstanceBootGroup(this);
+ if (result == null) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update instance boot group");
+ }
+ InstanceBootGroupResponse response = instanceBootGroupService.createInstanceBootGroupResponse(result.getId());
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupMemberCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupMemberCmd.java
new file mode 100644
index 000000000000..8c2316a104b7
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupMemberCmd.java
@@ -0,0 +1,88 @@
+// 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.
+
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.user.UserCmd;
+import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService;
+
+@APICommand(name = "updateInstanceBootGroupMember",
+ description = "Updates the boot order of a member in an instance boot group",
+ since = "24.0",
+ responseObject = InstanceBootGroupMemberResponse.class,
+ entityType = {InstanceBootGroupMember.class},
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class UpdateInstanceBootGroupMemberCmd extends BaseCmd implements UserCmd {
+
+ @Inject
+ InstanceBootGroupService instanceBootGroupService;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupMemberResponse.class, required = true, description = "The ID of the boot group member entry")
+ private Long id;
+
+ @Parameter(name = ApiConstants.BOOT_ORDER, type = CommandType.INTEGER, required = true, description = "The new boot order value (0 or greater)")
+ private int order;
+
+ public Long getId() {
+ return id;
+ }
+
+ public int getOrder() {
+ return order;
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return CallContext.current().getCallingAccount().getId();
+ }
+
+ @Override
+ public Long getApiResourceId() {
+ return instanceBootGroupService.getInstanceBootGroupIdForMember(getId());
+ }
+
+ @Override
+ public ApiCommandResourceType getApiResourceType() {
+ return ApiCommandResourceType.InstanceBootGroup;
+ }
+
+ @Override
+ public void execute() {
+ InstanceBootGroupMember result = instanceBootGroupService.updateInstanceBootGroupMember(this);
+ if (result == null) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update instance boot group member");
+ }
+ InstanceBootGroupMemberResponse response = instanceBootGroupService.createInstanceBootGroupMemberResponse(result);
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupReadinessRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupReadinessRuleCmd.java
new file mode 100644
index 000000000000..ea399f085658
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupReadinessRuleCmd.java
@@ -0,0 +1,110 @@
+// 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.
+
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import java.util.Collection;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.user.UserCmd;
+import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService;
+
+@APICommand(name = "updateInstanceBootGroupReadinessRule",
+ description = "Updates an Instance Boot Group readiness rule. The rule type, Instance Boot Group and item are immutable after creation.",
+ since = "24.0",
+ responseObject = InstanceBootGroupReadinessRuleResponse.class,
+ entityType = {InstanceBootGroupReadinessRule.class},
+ requestHasSensitiveInfo = false,
+ responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class UpdateInstanceBootGroupReadinessRuleCmd extends BaseCmd implements UserCmd {
+
+ @Inject
+ InstanceBootGroupService instanceBootGroupService;
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupReadinessRuleResponse.class, required = true,
+ description = "The ID of the readiness rule")
+ private Long id;
+
+ @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "New name for the readiness rule")
+ private String name;
+
+ @Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, description = "Whether the rule is enabled")
+ private Boolean enabled;
+
+ @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, description = "Rule-type-specific configuration")
+ private Map details;
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Boolean getEnabled() {
+ return enabled;
+ }
+
+ public Map getDetails() {
+ if (this.details == null || this.details.isEmpty()) {
+ return null;
+ }
+ Collection paramsCollection = this.details.values();
+ return (Map) (paramsCollection.toArray())[0];
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return CallContext.current().getCallingAccount().getId();
+ }
+
+ @Override
+ public Long getApiResourceId() {
+ return id;
+ }
+
+ @Override
+ public ApiCommandResourceType getApiResourceType() {
+ return ApiCommandResourceType.InstanceBootGroupReadinessRule;
+ }
+
+ @Override
+ public void execute() {
+ InstanceBootGroupReadinessRule result = instanceBootGroupService.updateInstanceBootGroupReadinessRule(this);
+ if (result == null) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update instance boot group readiness rule");
+ }
+ InstanceBootGroupReadinessRuleResponse response = instanceBootGroupService.createInstanceBootGroupReadinessRuleResponse(result);
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java
index 48205c50e077..618d14d90a68 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java
@@ -65,7 +65,7 @@ public class UpdateVMCmd extends BaseCustomIdCmd implements SecurityGroupAction,
@Parameter(name = ApiConstants.DISPLAY_NAME, type = CommandType.STRING, description = "User generated name")
private String displayName;
- @Parameter(name = ApiConstants.GROUP, type = CommandType.STRING, description = "Group of the Instance")
+ @Parameter(name = ApiConstants.GROUP, type = CommandType.STRING, description = "Group of the Instance. To remove an Instance from its current Group, please pass an empty string as parameter")
private String group;
@Parameter(name = ApiConstants.HA_ENABLE, type = CommandType.BOOLEAN, description = "True if high-availability is enabled for the Instance, false otherwise")
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/utils/OfferingUtils.java b/api/src/main/java/org/apache/cloudstack/api/command/utils/OfferingUtils.java
index 433a37c07cde..3885c30f0af4 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/utils/OfferingUtils.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/utils/OfferingUtils.java
@@ -23,8 +23,12 @@ public class OfferingUtils {
private OfferingUtils() {
}
+ public static boolean isNetris(String provider) {
+ return "Netris".equalsIgnoreCase(provider);
+ }
+
public static boolean isNetrisNatted(String provider, String networkMode) {
- return "Netris".equalsIgnoreCase(provider) &&
+ return isNetris(provider) &&
NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(networkMode);
}
@@ -33,6 +37,6 @@ public static boolean isNsxWithoutLb(String provider, boolean nsxSupportsLbServi
}
public static boolean isNetrisRouted(String provider, String networkMode) {
- return "Netris".equalsIgnoreCase(provider) && NetworkOffering.NetworkMode.ROUTED.name().equalsIgnoreCase(networkMode);
+ return isNetris(provider) && NetworkOffering.NetworkMode.ROUTED.name().equalsIgnoreCase(networkMode);
}
}
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberChildResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberChildResponse.java
new file mode 100644
index 000000000000..d1164209d003
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberChildResponse.java
@@ -0,0 +1,57 @@
+// 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.
+
+package org.apache.cloudstack.api.response;
+
+import com.google.gson.annotations.SerializedName;
+
+import org.apache.cloudstack.api.ApiConstants;
+
+import com.cloud.serializer.Param;
+
+/**
+ * Extends {@link UserVmResponse} rather than duplicating id/name/state fields, since this is set
+ * up from a plain {@code UserVmVO} (basic details only, via {@code UserVmDao}) with just a few
+ * boot-group-specific fields of its own — never populated from the join-based VM response builder.
+ */
+@SuppressWarnings("unused")
+public class InstanceBootGroupMemberChildResponse extends UserVmResponse {
+
+ @SerializedName(ApiConstants.READINESS_MODE)
+ @Param(description = "None, ChildDependent or RuleBased, computed from whether readiness rules are attached")
+ private String readinessMode;
+
+ @SerializedName(ApiConstants.READINESS_STATUS)
+ @Param(description = "The last cached readiness evaluation")
+ private String readinessStatus;
+
+ @SerializedName(ApiConstants.READINESS_MESSAGE)
+ @Param(description = "Why readinessstatus is what it is: the failing rule(s)' cached message")
+ private String readinessMessage;
+
+ public void setReadinessMode(String readinessMode) {
+ this.readinessMode = readinessMode;
+ }
+
+ public void setReadinessStatus(String readinessStatus) {
+ this.readinessStatus = readinessStatus;
+ }
+
+ public void setReadinessMessage(String readinessMessage) {
+ this.readinessMessage = readinessMessage;
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberResponse.java
new file mode 100644
index 000000000000..4d91714216ce
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberResponse.java
@@ -0,0 +1,131 @@
+// 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.
+
+package org.apache.cloudstack.api.response;
+
+import java.util.Date;
+import java.util.List;
+
+import com.google.gson.annotations.SerializedName;
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+import org.apache.cloudstack.api.EntityReference;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember;
+
+import com.cloud.serializer.Param;
+
+@SuppressWarnings("unused")
+@EntityReference(value = InstanceBootGroupMember.class)
+public class InstanceBootGroupMemberResponse extends BaseResponse {
+
+ @SerializedName(ApiConstants.ID)
+ @Param(description = "The UUID of this member entry")
+ private String id;
+
+ @SerializedName(ApiConstants.BOOT_GROUP_ID)
+ @Param(description = "The ID of the boot group this member belongs to")
+ private String bootGroupId;
+
+ @SerializedName(ApiConstants.MEMBER_TYPE)
+ @Param(description = "The type of the member: VirtualMachine or InstanceGroup")
+ private String memberType;
+
+ @SerializedName(ApiConstants.MEMBER_ID)
+ @Param(description = "The ID of the VM or InstanceGroup")
+ private String memberId;
+
+ @SerializedName(ApiConstants.MEMBER_NAME)
+ @Param(description = "The name of the VM or InstanceGroup")
+ private String memberName;
+
+ @SerializedName(ApiConstants.MEMBER_STATE)
+ @Param(description = "The state of the Instance")
+ private String memberState;
+
+ @SerializedName(ApiConstants.BOOT_ORDER)
+ @Param(description = "The boot order value for this member")
+ private int order;
+
+ @SerializedName(ApiConstants.CREATED)
+ @Param(description = "The date the member was added to the boot group")
+ private Date created;
+
+ @SerializedName(ApiConstants.READINESS_MODE)
+ @Param(description = "None, CHILD_DEPENDENT or RULE_BASED, computed from whether readiness rules are attached")
+ private String readinessMode;
+
+ @SerializedName(ApiConstants.READINESS_STATUS)
+ @Param(description = "The last cached readiness evaluation")
+ private String readinessStatus;
+
+ @SerializedName(ApiConstants.READINESS_MESSAGE)
+ @Param(description = "Why readinessstatus is what it is: the failing rule(s)' cached message, and/or a count of not-ready member VMs for InstanceGroup members")
+ private String readinessMessage;
+
+ @SerializedName(ApiConstants.CHILDREN)
+ @Param(description = "For InstanceGroup members, the VMs within the group (only present when requested via details=children)", responseObject = InstanceBootGroupMemberChildResponse.class)
+ private List children;
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public void setBootGroupId(String bootGroupId) {
+ this.bootGroupId = bootGroupId;
+ }
+
+ public void setMemberType(String memberType) {
+ this.memberType = memberType;
+ }
+
+ public void setMemberId(String memberId) {
+ this.memberId = memberId;
+ }
+
+ public void setMemberName(String memberName) {
+ this.memberName = memberName;
+ }
+
+ public void setMemberState(String memberState) {
+ this.memberState = memberState;
+ }
+
+ public void setOrder(int order) {
+ this.order = order;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public void setReadinessMode(String readinessMode) {
+ this.readinessMode = readinessMode;
+ }
+
+ public void setReadinessStatus(String readinessStatus) {
+ this.readinessStatus = readinessStatus;
+ }
+
+ public void setReadinessMessage(String readinessMessage) {
+ this.readinessMessage = readinessMessage;
+ }
+
+ public void setChildren(List children) {
+ this.children = children;
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupReadinessRuleResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupReadinessRuleResponse.java
new file mode 100644
index 000000000000..26228c8e8784
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupReadinessRuleResponse.java
@@ -0,0 +1,148 @@
+// 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.
+
+package org.apache.cloudstack.api.response;
+
+import java.util.Date;
+import java.util.Map;
+
+import com.google.gson.annotations.SerializedName;
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+import org.apache.cloudstack.api.EntityReference;
+import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule;
+
+import com.cloud.serializer.Param;
+
+@SuppressWarnings("unused")
+@EntityReference(value = InstanceBootGroupReadinessRule.class)
+public class InstanceBootGroupReadinessRuleResponse extends BaseResponse {
+
+ @SerializedName(ApiConstants.ID)
+ @Param(description = "The ID of the readiness rule")
+ private String id;
+
+ @SerializedName(ApiConstants.NAME)
+ @Param(description = "The name of the readiness rule")
+ private String name;
+
+ @SerializedName(ApiConstants.BOOT_GROUP_ID)
+ @Param(description = "The ID of the boot group this rule belongs to")
+ private String bootGroupId;
+
+ @SerializedName(ApiConstants.MEMBER_TYPE)
+ @Param(description = "The item type this rule applies to: VirtualMachine or InstanceGroup")
+ private String itemType;
+
+ @SerializedName(ApiConstants.MEMBER_ID)
+ @Param(description = "The ID of the item (VM or instance group) this rule applies to")
+ private String itemId;
+
+ @SerializedName(ApiConstants.MEMBER_NAME)
+ @Param(description = "The name of the item (VM or instance group) this rule applies to")
+ private String itemName;
+
+ @SerializedName(ApiConstants.RULE_TYPE)
+ @Param(description = "The readiness rule type")
+ private String ruleType;
+
+ @SerializedName(ApiConstants.ENABLED)
+ @Param(description = "Whether the rule is enabled")
+ private boolean enabled;
+
+ @SerializedName(ApiConstants.INHERITED)
+ @Param(description = "True if this rule is not attached to the queried item directly, but inherited from its owning InstanceGroup "
+ + "(only possible when listing by virtualmachineid; the rule's itemtype/itemid still refer to the InstanceGroup it's actually attached to)")
+ private boolean inherited;
+
+ @SerializedName(ApiConstants.DETAILS)
+ @Param(description = "Rule-type-specific configuration")
+ private Map details;
+
+ @SerializedName(ApiConstants.CREATED)
+ @Param(description = "The date the rule was created")
+ private Date created;
+
+ @SerializedName(ApiConstants.READINESS_STATUS)
+ @Param(description = "The last cached evaluation status of this rule: READY, NOT_READY, ERROR or UNKNOWN")
+ private String status;
+
+ @SerializedName("statusmessage")
+ @Param(description = "The message from the last evaluation of this rule")
+ private String statusMessage;
+
+ @SerializedName("checkedon")
+ @Param(description = "When this rule was last evaluated")
+ private Date checkedOn;
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setBootGroupId(String bootGroupId) {
+ this.bootGroupId = bootGroupId;
+ }
+
+ public void setItemType(String itemType) {
+ this.itemType = itemType;
+ }
+
+ public void setItemId(String itemId) {
+ this.itemId = itemId;
+ }
+
+ public void setItemName(String itemName) {
+ this.itemName = itemName;
+ }
+
+ public void setRuleType(String ruleType) {
+ this.ruleType = ruleType;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public void setInherited(boolean inherited) {
+ this.inherited = inherited;
+ }
+
+ public void setDetails(Map details) {
+ this.details = details;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public void setStatusMessage(String statusMessage) {
+ this.statusMessage = statusMessage;
+ }
+
+ public void setCheckedOn(Date checkedOn) {
+ this.checkedOn = checkedOn;
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupResponse.java
new file mode 100644
index 000000000000..127eeda17744
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupResponse.java
@@ -0,0 +1,160 @@
+// 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.
+
+package org.apache.cloudstack.api.response;
+
+import java.util.Date;
+
+import com.google.gson.annotations.SerializedName;
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+import org.apache.cloudstack.api.EntityReference;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup;
+
+import com.cloud.serializer.Param;
+
+@SuppressWarnings("unused")
+@EntityReference(value = InstanceBootGroup.class)
+public class InstanceBootGroupResponse extends BaseResponse implements ControlledViewEntityResponse {
+
+ @SerializedName(ApiConstants.ID)
+ @Param(description = "The ID of the instance boot group")
+ private String id;
+
+ @SerializedName(ApiConstants.NAME)
+ @Param(description = "The name of the instance boot group")
+ private String name;
+
+ @SerializedName(ApiConstants.DESCRIPTION)
+ @Param(description = "The description of the instance boot group")
+ private String description;
+
+ @SerializedName(ApiConstants.CREATED)
+ @Param(description = "The date the instance boot group was created")
+ private Date created;
+
+ @SerializedName(ApiConstants.ACCOUNT)
+ @Param(description = "The account owning the instance boot group")
+ private String accountName;
+
+ @SerializedName(ApiConstants.ACCOUNT_ID)
+ @Param(description = "The account ID owning the instance boot group")
+ private String accountId;
+
+ @SerializedName(ApiConstants.DOMAIN_ID)
+ @Param(description = "The domain ID of the instance boot group")
+ private String domainId;
+
+ @SerializedName(ApiConstants.DOMAIN)
+ @Param(description = "The domain name of the instance boot group")
+ private String domainName;
+
+ @SerializedName(ApiConstants.DOMAIN_PATH)
+ @Param(description = "The path of the domain the instance boot group belongs to")
+ private String domainPath;
+
+ @SerializedName(ApiConstants.PROJECT_ID)
+ @Param(description = "The project ID of the instance boot group")
+ private String projectId;
+
+ @SerializedName(ApiConstants.PROJECT)
+ @Param(description = "The project name of the instance boot group")
+ private String projectName;
+
+ @SerializedName(ApiConstants.READINESS_ATTEMPT_TIMEOUT_SECONDS)
+ @Param(description = "Effective timeout in seconds for each readiness retry attempt (per-boot-group override if set, else the global default)")
+ private long readinessAttemptTimeoutSeconds;
+
+ @SerializedName(ApiConstants.READINESS_MAX_RETRY_ATTEMPTS)
+ @Param(description = "Effective maximum number of readiness retry attempts (per-boot-group override if set, else the global default)")
+ private long readinessMaxRetryAttempts;
+
+ @SerializedName(ApiConstants.READINESS_REBOOT_ON_RETRY)
+ @Param(description = "Effective setting for whether an instance is rebooted between readiness retry attempts (per-boot-group override if set, else the global default)")
+ private boolean readinessRebootOnRetry;
+
+ @SerializedName(ApiConstants.READINESS_INITIAL_DELAY_SECONDS)
+ @Param(description = "Effective delay in seconds after starting or rebooting an instance before its first readiness check of that attempt (per-boot-group override if set, else the global default)")
+ private long readinessInitialDelaySeconds;
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public void setAccountId(String accountId) {
+ this.accountId = accountId;
+ }
+
+ @Override
+ public void setAccountName(String accountName) {
+ this.accountName = accountName;
+ }
+
+ @Override
+ public void setDomainId(String domainId) {
+ this.domainId = domainId;
+ }
+
+ @Override
+ public void setDomainName(String domainName) {
+ this.domainName = domainName;
+ }
+
+ @Override
+ public void setDomainPath(String domainPath) {
+ this.domainPath = domainPath;
+ }
+
+ @Override
+ public void setProjectId(String projectId) {
+ this.projectId = projectId;
+ }
+
+ @Override
+ public void setProjectName(String projectName) {
+ this.projectName = projectName;
+ }
+
+ public void setReadinessAttemptTimeoutSeconds(long readinessAttemptTimeoutSeconds) {
+ this.readinessAttemptTimeoutSeconds = readinessAttemptTimeoutSeconds;
+ }
+
+ public void setReadinessMaxRetryAttempts(long readinessMaxRetryAttempts) {
+ this.readinessMaxRetryAttempts = readinessMaxRetryAttempts;
+ }
+
+ public void setReadinessRebootOnRetry(boolean readinessRebootOnRetry) {
+ this.readinessRebootOnRetry = readinessRebootOnRetry;
+ }
+
+ public void setReadinessInitialDelaySeconds(long readinessInitialDelaySeconds) {
+ this.readinessInitialDelaySeconds = readinessInitialDelaySeconds;
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationCycleResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationCycleResponse.java
new file mode 100644
index 000000000000..2ceab991b179
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationCycleResponse.java
@@ -0,0 +1,111 @@
+// 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.
+package org.apache.cloudstack.api.response;
+
+import java.util.Date;
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+import org.apache.cloudstack.api.EntityReference;
+import org.apache.cloudstack.vm.VmwareCbtMigrationCycle;
+
+import com.cloud.serializer.Param;
+import com.google.gson.annotations.SerializedName;
+
+@EntityReference(value = VmwareCbtMigrationCycle.class)
+public class VmwareCbtMigrationCycleResponse extends BaseResponse {
+
+ @SerializedName(ApiConstants.ID)
+ @Param(description = "the ID of the VMware CBT migration cycle")
+ private String id;
+
+ @SerializedName(ApiConstants.CYCLE_NUMBER)
+ @Param(description = "the CBT delta synchronization cycle number")
+ private int cycleNumber;
+
+ @SerializedName(ApiConstants.SNAPSHOT_MOR)
+ @Param(description = "the VMware snapshot managed object reference used for this cycle")
+ private String snapshotMor;
+
+ @SerializedName(ApiConstants.CHANGED_BYTES)
+ @Param(description = "the changed bytes copied in this cycle")
+ private Long changedBytes;
+
+ @SerializedName(ApiConstants.DIRTY_RATE)
+ @Param(description = "the dirty rate in bytes per second observed in this cycle")
+ private Long dirtyRate;
+
+ @SerializedName(ApiConstants.DURATION)
+ @Param(description = "the cycle duration in milliseconds")
+ private Long duration;
+
+ @SerializedName(ApiConstants.STATE)
+ @Param(description = "the cycle state")
+ private String state;
+
+ @SerializedName(ApiConstants.DESCRIPTION)
+ @Param(description = "the cycle status or failure description")
+ private String description;
+
+ @SerializedName(ApiConstants.CREATED)
+ @Param(description = "the create date of the VMware CBT migration cycle")
+ private Date created;
+
+ @SerializedName(ApiConstants.LAST_UPDATED)
+ @Param(description = "the last updated date of the VMware CBT migration cycle")
+ private Date lastUpdated;
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public void setCycleNumber(int cycleNumber) {
+ this.cycleNumber = cycleNumber;
+ }
+
+ public void setSnapshotMor(String snapshotMor) {
+ this.snapshotMor = snapshotMor;
+ }
+
+ public void setChangedBytes(Long changedBytes) {
+ this.changedBytes = changedBytes;
+ }
+
+ public void setDirtyRate(Long dirtyRate) {
+ this.dirtyRate = dirtyRate;
+ }
+
+ public void setDuration(Long duration) {
+ this.duration = duration;
+ }
+
+ public void setState(String state) {
+ this.state = state;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public void setLastUpdated(Date lastUpdated) {
+ this.lastUpdated = lastUpdated;
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationDiskResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationDiskResponse.java
new file mode 100644
index 000000000000..9a23cf44cad1
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationDiskResponse.java
@@ -0,0 +1,117 @@
+// 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.
+package org.apache.cloudstack.api.response;
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+import org.apache.cloudstack.api.EntityReference;
+import org.apache.cloudstack.vm.VmwareCbtMigrationDisk;
+
+import com.cloud.serializer.Param;
+import com.google.gson.annotations.SerializedName;
+
+@EntityReference(value = VmwareCbtMigrationDisk.class)
+public class VmwareCbtMigrationDiskResponse extends BaseResponse {
+
+ @SerializedName(ApiConstants.ID)
+ @Param(description = "the ID of the VMware CBT migration disk")
+ private String id;
+
+ @SerializedName(ApiConstants.SOURCE_DISK_ID)
+ @Param(description = "the source VMware disk identifier")
+ private String sourceDiskId;
+
+ @SerializedName(ApiConstants.SOURCE_DISK_DEVICE_KEY)
+ @Param(description = "the source VMware disk device key")
+ private Integer sourceDiskDeviceKey;
+
+ @SerializedName(ApiConstants.SOURCE_DISK_PATH)
+ @Param(description = "the source VMware disk path")
+ private String sourceDiskPath;
+
+ @SerializedName(ApiConstants.DATASTORE_NAME)
+ @Param(description = "the source VMware datastore name")
+ private String datastoreName;
+
+ @SerializedName(ApiConstants.CAPACITY)
+ @Param(description = "the source disk capacity in bytes")
+ private Long capacityBytes;
+
+ @SerializedName(ApiConstants.TARGET_PATH)
+ @Param(description = "the planned or actual KVM target disk path")
+ private String targetPath;
+
+ @SerializedName(ApiConstants.TARGET_FORMAT)
+ @Param(description = "the KVM target disk format")
+ private String targetFormat;
+
+ @SerializedName(ApiConstants.CHANGE_ID)
+ @Param(description = "the VMware CBT change ID used for the next delta query")
+ private String changeId;
+
+ @SerializedName(ApiConstants.SNAPSHOT_MOR)
+ @Param(description = "the VMware snapshot managed object reference currently associated with this disk")
+ private String snapshotMor;
+
+ @SerializedName(ApiConstants.STATE)
+ @Param(description = "the disk replication state")
+ private String state;
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public void setSourceDiskId(String sourceDiskId) {
+ this.sourceDiskId = sourceDiskId;
+ }
+
+ public void setSourceDiskDeviceKey(Integer sourceDiskDeviceKey) {
+ this.sourceDiskDeviceKey = sourceDiskDeviceKey;
+ }
+
+ public void setSourceDiskPath(String sourceDiskPath) {
+ this.sourceDiskPath = sourceDiskPath;
+ }
+
+ public void setDatastoreName(String datastoreName) {
+ this.datastoreName = datastoreName;
+ }
+
+ public void setCapacityBytes(Long capacityBytes) {
+ this.capacityBytes = capacityBytes;
+ }
+
+ public void setTargetPath(String targetPath) {
+ this.targetPath = targetPath;
+ }
+
+ public void setTargetFormat(String targetFormat) {
+ this.targetFormat = targetFormat;
+ }
+
+ public void setChangeId(String changeId) {
+ this.changeId = changeId;
+ }
+
+ public void setSnapshotMor(String snapshotMor) {
+ this.snapshotMor = snapshotMor;
+ }
+
+ public void setState(String state) {
+ this.state = state;
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationPreflightDiskResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationPreflightDiskResponse.java
new file mode 100644
index 000000000000..e637619a9a8d
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationPreflightDiskResponse.java
@@ -0,0 +1,114 @@
+// 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.
+package org.apache.cloudstack.api.response;
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+
+import com.cloud.serializer.Param;
+import com.google.gson.annotations.SerializedName;
+
+public class VmwareCbtMigrationPreflightDiskResponse extends BaseResponse {
+
+ @SerializedName(ApiConstants.SOURCE_DISK_ID)
+ @Param(description = "the source VMware disk identifier")
+ private String sourceDiskId;
+
+ @SerializedName(ApiConstants.SOURCE_DISK_DEVICE_KEY)
+ @Param(description = "the source VMware disk device key")
+ private Integer sourceDiskDeviceKey;
+
+ @SerializedName(ApiConstants.SOURCE_DISK_PATH)
+ @Param(description = "the source VMware disk path")
+ private String sourceDiskPath;
+
+ @SerializedName(ApiConstants.DATASTORE_NAME)
+ @Param(description = "the source VMware datastore name")
+ private String datastoreName;
+
+ @SerializedName(ApiConstants.CAPACITY)
+ @Param(description = "the source disk capacity in bytes")
+ private Long capacityBytes;
+
+ @SerializedName("backingtype")
+ @Param(description = "the VMware disk backing class")
+ private String backingType;
+
+ @SerializedName("diskmode")
+ @Param(description = "the VMware disk mode")
+ private String diskMode;
+
+ @SerializedName("rdmcompatibilitymode")
+ @Param(description = "the RDM compatibility mode, when the disk is an RDM")
+ private String rdmCompatibilityMode;
+
+ @SerializedName("independentdisk")
+ @Param(description = "whether the VMware disk is configured as independent")
+ private boolean independentDisk;
+
+ @SerializedName("physicalrdm")
+ @Param(description = "whether the VMware disk is a physical-mode RDM")
+ private boolean physicalRdm;
+
+ @SerializedName("changeidpresent")
+ @Param(description = "whether a VMware CBT change ID is currently visible for this disk")
+ private boolean changeIdPresent;
+
+ public void setSourceDiskId(String sourceDiskId) {
+ this.sourceDiskId = sourceDiskId;
+ }
+
+ public void setSourceDiskDeviceKey(Integer sourceDiskDeviceKey) {
+ this.sourceDiskDeviceKey = sourceDiskDeviceKey;
+ }
+
+ public void setSourceDiskPath(String sourceDiskPath) {
+ this.sourceDiskPath = sourceDiskPath;
+ }
+
+ public void setDatastoreName(String datastoreName) {
+ this.datastoreName = datastoreName;
+ }
+
+ public void setCapacityBytes(Long capacityBytes) {
+ this.capacityBytes = capacityBytes;
+ }
+
+ public void setBackingType(String backingType) {
+ this.backingType = backingType;
+ }
+
+ public void setDiskMode(String diskMode) {
+ this.diskMode = diskMode;
+ }
+
+ public void setRdmCompatibilityMode(String rdmCompatibilityMode) {
+ this.rdmCompatibilityMode = rdmCompatibilityMode;
+ }
+
+ public void setIndependentDisk(boolean independentDisk) {
+ this.independentDisk = independentDisk;
+ }
+
+ public void setPhysicalRdm(boolean physicalRdm) {
+ this.physicalRdm = physicalRdm;
+ }
+
+ public void setChangeIdPresent(boolean changeIdPresent) {
+ this.changeIdPresent = changeIdPresent;
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationPreflightFindingResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationPreflightFindingResponse.java
new file mode 100644
index 000000000000..a4cccd06fa69
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationPreflightFindingResponse.java
@@ -0,0 +1,55 @@
+// 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.
+package org.apache.cloudstack.api.response;
+
+import org.apache.cloudstack.api.BaseResponse;
+
+import com.cloud.serializer.Param;
+import com.google.gson.annotations.SerializedName;
+
+public class VmwareCbtMigrationPreflightFindingResponse extends BaseResponse {
+
+ @SerializedName("severity")
+ @Param(description = "finding severity: PASS, WARN or FAIL")
+ private String severity;
+
+ @SerializedName("code")
+ @Param(description = "technical preflight finding code")
+ private String code;
+
+ @SerializedName("component")
+ @Param(description = "component checked by the preflight finding")
+ private String component;
+
+ @SerializedName("resource")
+ @Param(description = "resource checked by the preflight finding")
+ private String resource;
+
+ @SerializedName("message")
+ @Param(description = "human-readable preflight finding message")
+ private String message;
+
+ public VmwareCbtMigrationPreflightFindingResponse(String severity, String code, String component,
+ String resource, String message) {
+ this.severity = severity;
+ this.code = code;
+ this.component = component;
+ this.resource = resource;
+ this.message = message;
+ setObjectName("finding");
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationPreflightResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationPreflightResponse.java
new file mode 100644
index 000000000000..ec151189ee3a
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationPreflightResponse.java
@@ -0,0 +1,284 @@
+// 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.
+package org.apache.cloudstack.api.response;
+
+import java.util.List;
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+
+import com.cloud.serializer.Param;
+import com.google.gson.annotations.SerializedName;
+
+public class VmwareCbtMigrationPreflightResponse extends BaseResponse {
+
+ @SerializedName("ready")
+ @Param(description = "whether the source VM and destination are ready for VMware CBT migration start")
+ private boolean ready;
+
+ @SerializedName(ApiConstants.ZONE_ID)
+ @Param(description = "the destination zone ID")
+ private String zoneId;
+
+ @SerializedName(ApiConstants.ZONE_NAME)
+ @Param(description = "the destination zone name")
+ private String zoneName;
+
+ @SerializedName(ApiConstants.CLUSTER_ID)
+ @Param(description = "the destination KVM cluster ID")
+ private String clusterId;
+
+ @SerializedName(ApiConstants.CLUSTER_NAME)
+ @Param(description = "the destination KVM cluster name")
+ private String clusterName;
+
+ @SerializedName(ApiConstants.CONVERT_INSTANCE_HOST_ID)
+ @Param(description = "the KVM host selected for conversion and CBT replication")
+ private String convertInstanceHostId;
+
+ @SerializedName("convertinstancehostname")
+ @Param(description = "the KVM host selected for conversion and CBT replication")
+ private String convertInstanceHostName;
+
+ @SerializedName(ApiConstants.CONVERT_INSTANCE_STORAGE_POOL_ID)
+ @Param(description = "the destination storage pool ID")
+ private String storagePoolId;
+
+ @SerializedName(ApiConstants.POOL_NAME)
+ @Param(description = "the destination storage pool name")
+ private String storagePoolName;
+
+ @SerializedName("storagepooltype")
+ @Param(description = "the destination storage pool type")
+ private String storagePoolType;
+
+ @SerializedName("storagewritertype")
+ @Param(description = "the VMware CBT target storage writer type")
+ private String storageWriterType;
+
+ @SerializedName("storagewritersupported")
+ @Param(description = "whether the target storage writer is implemented")
+ private boolean storageWriterSupported;
+
+ @SerializedName("storagerequiresinplacefinalization")
+ @Param(description = "whether the target storage writer requires virt-v2v in-place finalization")
+ private boolean storageRequiresInPlaceFinalization;
+
+ @SerializedName("convertinstancehostinplacefinalizationsupported")
+ @Param(description = "whether the selected KVM host reports VMware CBT in-place finalization support")
+ private boolean convertInstanceHostInPlaceFinalizationSupported;
+
+ @SerializedName("noninplacefinalizationfallbackallowed")
+ @Param(description = "whether VMware CBT non-in-place fallback finalization is enabled by configuration")
+ private boolean nonInPlaceFinalizationFallbackAllowed;
+
+ @SerializedName("noninplacefinalizationfallbacksupported")
+ @Param(description = "whether the selected target storage can use non-in-place fallback finalization")
+ private boolean nonInPlaceFinalizationFallbackSupported;
+
+ @SerializedName(ApiConstants.VCENTER)
+ @Param(description = "the source VMware vCenter")
+ private String vcenter;
+
+ @SerializedName(ApiConstants.DATACENTER_NAME)
+ @Param(description = "the source VMware datacenter")
+ private String datacenterName;
+
+ @SerializedName(ApiConstants.SOURCE_HOST)
+ @Param(description = "the source VMware ESXi host")
+ private String sourceHost;
+
+ @SerializedName(ApiConstants.SOURCE_VM_NAME)
+ @Param(description = "the source VMware VM name")
+ private String sourceVmName;
+
+ @SerializedName("sourcevmmor")
+ @Param(description = "the source VMware VM managed object reference")
+ private String sourceVmMor;
+
+ @SerializedName("changetrackingsupported")
+ @Param(description = "whether the source VM reports CBT support")
+ private Boolean changeTrackingSupported;
+
+ @SerializedName("changetrackingenabled")
+ @Param(description = "whether CBT is currently enabled on the source VM")
+ private Boolean changeTrackingEnabled;
+
+ @SerializedName("consolidationneeded")
+ @Param(description = "whether the source VM reports disk consolidation is needed")
+ private Boolean consolidationNeeded;
+
+ @SerializedName("existingsnapshotcount")
+ @Param(description = "number of existing VMware snapshots on the source VM")
+ private Integer existingSnapshotCount;
+
+ @SerializedName("sourcecpunumber")
+ @Param(description = "source VMware VM CPU count")
+ private Integer sourceCpuNumber;
+
+ @SerializedName("sourcecpuspeed")
+ @Param(description = "source VMware VM CPU reservation in MHz used for offering validation, when available")
+ private Integer sourceCpuSpeed;
+
+ @SerializedName("sourcememory")
+ @Param(description = "source VMware VM memory in MB")
+ private Integer sourceMemory;
+
+ @SerializedName("sourceguestosid")
+ @Param(description = "source VMware VM guest OS identifier")
+ private String sourceGuestOsId;
+
+ @SerializedName("sourceguestos")
+ @Param(description = "source VMware VM guest OS name")
+ private String sourceGuestOs;
+
+ @SerializedName(ApiConstants.DISK)
+ @Param(description = "source VMware disk preflight information")
+ private List disks;
+
+ @SerializedName("finding")
+ @Param(description = "preflight findings")
+ private List findings;
+
+ public void setReady(boolean ready) {
+ this.ready = ready;
+ }
+
+ public void setZoneId(String zoneId) {
+ this.zoneId = zoneId;
+ }
+
+ public void setZoneName(String zoneName) {
+ this.zoneName = zoneName;
+ }
+
+ public void setClusterId(String clusterId) {
+ this.clusterId = clusterId;
+ }
+
+ public void setClusterName(String clusterName) {
+ this.clusterName = clusterName;
+ }
+
+ public void setConvertInstanceHostId(String convertInstanceHostId) {
+ this.convertInstanceHostId = convertInstanceHostId;
+ }
+
+ public void setConvertInstanceHostName(String convertInstanceHostName) {
+ this.convertInstanceHostName = convertInstanceHostName;
+ }
+
+ public void setStoragePoolId(String storagePoolId) {
+ this.storagePoolId = storagePoolId;
+ }
+
+ public void setStoragePoolName(String storagePoolName) {
+ this.storagePoolName = storagePoolName;
+ }
+
+ public void setStoragePoolType(String storagePoolType) {
+ this.storagePoolType = storagePoolType;
+ }
+
+ public void setStorageWriterType(String storageWriterType) {
+ this.storageWriterType = storageWriterType;
+ }
+
+ public void setStorageWriterSupported(boolean storageWriterSupported) {
+ this.storageWriterSupported = storageWriterSupported;
+ }
+
+ public void setStorageRequiresInPlaceFinalization(boolean storageRequiresInPlaceFinalization) {
+ this.storageRequiresInPlaceFinalization = storageRequiresInPlaceFinalization;
+ }
+
+ public void setConvertInstanceHostInPlaceFinalizationSupported(boolean convertInstanceHostInPlaceFinalizationSupported) {
+ this.convertInstanceHostInPlaceFinalizationSupported = convertInstanceHostInPlaceFinalizationSupported;
+ }
+
+ public void setNonInPlaceFinalizationFallbackAllowed(boolean nonInPlaceFinalizationFallbackAllowed) {
+ this.nonInPlaceFinalizationFallbackAllowed = nonInPlaceFinalizationFallbackAllowed;
+ }
+
+ public void setNonInPlaceFinalizationFallbackSupported(boolean nonInPlaceFinalizationFallbackSupported) {
+ this.nonInPlaceFinalizationFallbackSupported = nonInPlaceFinalizationFallbackSupported;
+ }
+
+ public void setVcenter(String vcenter) {
+ this.vcenter = vcenter;
+ }
+
+ public void setDatacenterName(String datacenterName) {
+ this.datacenterName = datacenterName;
+ }
+
+ public void setSourceHost(String sourceHost) {
+ this.sourceHost = sourceHost;
+ }
+
+ public void setSourceVmName(String sourceVmName) {
+ this.sourceVmName = sourceVmName;
+ }
+
+ public void setSourceVmMor(String sourceVmMor) {
+ this.sourceVmMor = sourceVmMor;
+ }
+
+ public void setChangeTrackingSupported(Boolean changeTrackingSupported) {
+ this.changeTrackingSupported = changeTrackingSupported;
+ }
+
+ public void setChangeTrackingEnabled(Boolean changeTrackingEnabled) {
+ this.changeTrackingEnabled = changeTrackingEnabled;
+ }
+
+ public void setConsolidationNeeded(Boolean consolidationNeeded) {
+ this.consolidationNeeded = consolidationNeeded;
+ }
+
+ public void setExistingSnapshotCount(Integer existingSnapshotCount) {
+ this.existingSnapshotCount = existingSnapshotCount;
+ }
+
+ public void setSourceCpuNumber(Integer sourceCpuNumber) {
+ this.sourceCpuNumber = sourceCpuNumber;
+ }
+
+ public void setSourceCpuSpeed(Integer sourceCpuSpeed) {
+ this.sourceCpuSpeed = sourceCpuSpeed;
+ }
+
+ public void setSourceMemory(Integer sourceMemory) {
+ this.sourceMemory = sourceMemory;
+ }
+
+ public void setSourceGuestOsId(String sourceGuestOsId) {
+ this.sourceGuestOsId = sourceGuestOsId;
+ }
+
+ public void setSourceGuestOs(String sourceGuestOs) {
+ this.sourceGuestOs = sourceGuestOs;
+ }
+
+ public void setDisks(List disks) {
+ this.disks = disks;
+ }
+
+ public void setFindings(List findings) {
+ this.findings = findings;
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationResponse.java
new file mode 100644
index 000000000000..37ee6906f9f3
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationResponse.java
@@ -0,0 +1,288 @@
+// 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.
+package org.apache.cloudstack.api.response;
+
+import java.util.Date;
+import java.util.List;
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+import org.apache.cloudstack.api.EntityReference;
+import org.apache.cloudstack.vm.VmwareCbtMigration;
+
+import com.cloud.serializer.Param;
+import com.google.gson.annotations.SerializedName;
+
+@EntityReference(value = VmwareCbtMigration.class)
+public class VmwareCbtMigrationResponse extends BaseResponse {
+
+ @SerializedName(ApiConstants.ID)
+ @Param(description = "the ID of the VMware CBT migration")
+ private String id;
+
+ @SerializedName(ApiConstants.ZONE_ID)
+ @Param(description = "the destination zone ID")
+ private String zoneId;
+
+ @SerializedName(ApiConstants.ZONE_NAME)
+ @Param(description = "the destination zone name")
+ private String zoneName;
+
+ @SerializedName(ApiConstants.ACCOUNT)
+ @Param(description = "the account name")
+ private String accountName;
+
+ @SerializedName(ApiConstants.ACCOUNT_ID)
+ @Param(description = "the account ID")
+ private String accountId;
+
+ @SerializedName(ApiConstants.VIRTUAL_MACHINE_ID)
+ @Param(description = "the ID of the imported VM after cutover")
+ private String virtualMachineId;
+
+ @SerializedName(ApiConstants.CLUSTER_ID)
+ @Param(description = "the destination KVM cluster ID")
+ private String clusterId;
+
+ @SerializedName(ApiConstants.CLUSTER_NAME)
+ @Param(description = "the destination KVM cluster name")
+ private String clusterName;
+
+ @SerializedName(ApiConstants.CONVERT_INSTANCE_HOST_ID)
+ @Param(description = "the KVM host selected for conversion and CBT replication")
+ private String convertInstanceHostId;
+
+ @SerializedName("convertinstancehostname")
+ @Param(description = "the KVM host name selected for conversion and CBT replication")
+ private String convertInstanceHostName;
+
+ @SerializedName(ApiConstants.CONVERT_INSTANCE_STORAGE_POOL_ID)
+ @Param(description = "the storage pool selected for converted disks")
+ private String storagePoolId;
+
+ @SerializedName(ApiConstants.POOL_NAME)
+ @Param(description = "the storage pool name selected for converted disks")
+ private String storagePoolName;
+
+ @SerializedName(ApiConstants.DISPLAY_NAME)
+ @Param(description = "the display name of the target VM")
+ private String displayName;
+
+ @SerializedName(ApiConstants.VCENTER)
+ @Param(description = "the source VMware vCenter")
+ private String vcenter;
+
+ @SerializedName(ApiConstants.EXISTING_VCENTER_ID)
+ @Param(description = "the linked existing vCenter ID, when used")
+ private String existingVcenterId;
+
+ @SerializedName(ApiConstants.DATACENTER_NAME)
+ @Param(description = "the source VMware datacenter")
+ private String datacenterName;
+
+ @SerializedName(ApiConstants.SOURCE_HOST)
+ @Param(description = "the source VMware ESXi host")
+ private String sourceHost;
+
+ @SerializedName(ApiConstants.SOURCE_CLUSTER)
+ @Param(description = "the source VMware cluster")
+ private String sourceCluster;
+
+ @SerializedName(ApiConstants.SOURCE_VM_NAME)
+ @Param(description = "the source VMware VM name")
+ private String sourceVmName;
+
+ @SerializedName(ApiConstants.STATE)
+ @Param(description = "the migration state")
+ private String state;
+
+ @SerializedName(ApiConstants.CURRENT_STEP)
+ @Param(description = "the current migration step")
+ private String currentStep;
+
+ @SerializedName("currentstepduration")
+ @Param(description = "the elapsed time spent in the current migration step")
+ private String currentStepDuration;
+
+ @SerializedName(ApiConstants.LAST_ERROR)
+ @Param(description = "the last migration error")
+ private String lastError;
+
+ @SerializedName(ApiConstants.COMPLETED_CYCLES)
+ @Param(description = "the number of completed CBT delta cycles")
+ private int completedCycles;
+
+ @SerializedName(ApiConstants.QUIET_CYCLES)
+ @Param(description = "the number of consecutive quiet CBT delta cycles")
+ private int quietCycles;
+
+ @SerializedName(ApiConstants.TOTAL_CHANGED_BYTES)
+ @Param(description = "the total changed bytes synchronized across CBT delta cycles")
+ private long totalChangedBytes;
+
+ @SerializedName(ApiConstants.LAST_CHANGED_BYTES)
+ @Param(description = "the changed bytes observed in the last CBT delta cycle")
+ private Long lastChangedBytes;
+
+ @SerializedName(ApiConstants.LAST_DIRTY_RATE)
+ @Param(description = "the dirty rate in bytes per second observed in the last CBT delta cycle")
+ private Long lastDirtyRate;
+
+ @SerializedName(ApiConstants.DISK)
+ @Param(description = "the source and target disks tracked by this VMware CBT migration")
+ private List disks;
+
+ @SerializedName(ApiConstants.CYCLE)
+ @Param(description = "the CBT delta synchronization cycles tracked by this VMware CBT migration")
+ private List cycles;
+
+ @SerializedName(ApiConstants.CREATED)
+ @Param(description = "the create date of the VMware CBT migration")
+ private Date created;
+
+ @SerializedName(ApiConstants.LAST_UPDATED)
+ @Param(description = "the last updated date of the VMware CBT migration")
+ private Date lastUpdated;
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public void setZoneId(String zoneId) {
+ this.zoneId = zoneId;
+ }
+
+ public void setZoneName(String zoneName) {
+ this.zoneName = zoneName;
+ }
+
+ public void setAccountName(String accountName) {
+ this.accountName = accountName;
+ }
+
+ public void setAccountId(String accountId) {
+ this.accountId = accountId;
+ }
+
+ public void setVirtualMachineId(String virtualMachineId) {
+ this.virtualMachineId = virtualMachineId;
+ }
+
+ public void setClusterId(String clusterId) {
+ this.clusterId = clusterId;
+ }
+
+ public void setClusterName(String clusterName) {
+ this.clusterName = clusterName;
+ }
+
+ public void setConvertInstanceHostId(String convertInstanceHostId) {
+ this.convertInstanceHostId = convertInstanceHostId;
+ }
+
+ public void setConvertInstanceHostName(String convertInstanceHostName) {
+ this.convertInstanceHostName = convertInstanceHostName;
+ }
+
+ public void setStoragePoolId(String storagePoolId) {
+ this.storagePoolId = storagePoolId;
+ }
+
+ public void setStoragePoolName(String storagePoolName) {
+ this.storagePoolName = storagePoolName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
+ public void setVcenter(String vcenter) {
+ this.vcenter = vcenter;
+ }
+
+ public void setExistingVcenterId(String existingVcenterId) {
+ this.existingVcenterId = existingVcenterId;
+ }
+
+ public void setDatacenterName(String datacenterName) {
+ this.datacenterName = datacenterName;
+ }
+
+ public void setSourceHost(String sourceHost) {
+ this.sourceHost = sourceHost;
+ }
+
+ public void setSourceCluster(String sourceCluster) {
+ this.sourceCluster = sourceCluster;
+ }
+
+ public void setSourceVmName(String sourceVmName) {
+ this.sourceVmName = sourceVmName;
+ }
+
+ public void setState(String state) {
+ this.state = state;
+ }
+
+ public void setCurrentStep(String currentStep) {
+ this.currentStep = currentStep;
+ }
+
+ public void setCurrentStepDuration(String currentStepDuration) {
+ this.currentStepDuration = currentStepDuration;
+ }
+
+ public void setLastError(String lastError) {
+ this.lastError = lastError;
+ }
+
+ public void setCompletedCycles(int completedCycles) {
+ this.completedCycles = completedCycles;
+ }
+
+ public void setQuietCycles(int quietCycles) {
+ this.quietCycles = quietCycles;
+ }
+
+ public void setTotalChangedBytes(long totalChangedBytes) {
+ this.totalChangedBytes = totalChangedBytes;
+ }
+
+ public void setLastChangedBytes(Long lastChangedBytes) {
+ this.lastChangedBytes = lastChangedBytes;
+ }
+
+ public void setLastDirtyRate(Long lastDirtyRate) {
+ this.lastDirtyRate = lastDirtyRate;
+ }
+
+ public void setDisks(List disks) {
+ this.disks = disks;
+ }
+
+ public void setCycles(List cycles) {
+ this.cycles = cycles;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public void setLastUpdated(Date lastUpdated) {
+ this.lastUpdated = lastUpdated;
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java
index 2e821dae52de..496aaf270f52 100644
--- a/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java
+++ b/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java
@@ -62,6 +62,10 @@ public class VpcOfferingResponse extends BaseResponse {
@Param(description = "Indicates if the VPC offering supports distributed router for one-hop forwarding", since = "4.4")
private Boolean supportsDistributedRouter;
+ @SerializedName(ApiConstants.REDUNDANT_ROUTER)
+ @Param(description = "Indicates if the VPC offering supports redundant routers")
+ private Boolean redundantRouter;
+
@SerializedName((ApiConstants.SUPPORTS_REGION_LEVEL_VPC))
@Param(description = "Indicated if the offering can support region level VPC", since = "4.4")
private Boolean supportsRegionLevelVpc;
@@ -213,4 +217,8 @@ public Boolean getConserveMode() {
public void setConserveMode(Boolean conserveMode) {
this.conserveMode = conserveMode;
}
+
+ public void setRedundantRouter(Boolean redundantRouter) {
+ this.redundantRouter = redundantRouter;
+ }
}
diff --git a/api/src/main/java/org/apache/cloudstack/query/QueryService.java b/api/src/main/java/org/apache/cloudstack/query/QueryService.java
index b6362e9a9c9b..4203f54aa06c 100644
--- a/api/src/main/java/org/apache/cloudstack/query/QueryService.java
+++ b/api/src/main/java/org/apache/cloudstack/query/QueryService.java
@@ -142,6 +142,12 @@ public interface QueryService {
ConfigKey ReturnVmStatsOnVmList = new ConfigKey<>("Advanced", Boolean.class, "list.vm.default.details.stats", "true",
"Determines whether VM stats should be returned when details are not explicitly specified in listVirtualMachines API request. When false, details default to [group, nics, secgrp, tmpl, servoff, diskoff, backoff, iso, volume, min, affgrp]. When true, all details are returned including 'stats'.", true, ConfigKey.Scope.Global);
+ ConfigKey BypassTemplateView = new ConfigKey<>("Advanced", Boolean.class, "template.list.bypass.view",
+ "false",
+ "If true, uses an optimized query path for listing templates and ISOs, which can improve performance on " +
+ "deployments with large numbers of templates. Automatically falls back to the standard query when " +
+ "filtering by tags, using the shared, featured, or community template filters, or when including " +
+ "removed templates.", true, ConfigKey.Scope.Global);
ListResponse searchForUsers(ResponseObject.ResponseView responseView, ListUsersCmd cmd) throws PermissionDeniedException;
diff --git a/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java b/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java
index 13f01b4944ad..709ad691971a 100644
--- a/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java
+++ b/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java
@@ -37,7 +37,8 @@ public interface VolumeImportUnmanageService extends PluggableService, Configura
Arrays.asList(Hypervisor.HypervisorType.KVM, Hypervisor.HypervisorType.VMware);
List SUPPORTED_STORAGE_POOL_TYPES_FOR_KVM = Arrays.asList(Storage.StoragePoolType.NetworkFilesystem,
- Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.RBD, Storage.StoragePoolType.SharedMountPoint);
+ Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.RBD, Storage.StoragePoolType.SharedMountPoint,
+ Storage.StoragePoolType.Linstor);
ConfigKey AllowImportVolumeWithBackingFile = new ConfigKey<>(Boolean.class,
"allow.import.volume.with.backing.file",
diff --git a/api/src/main/java/org/apache/cloudstack/vm/UnmanagedInstanceTO.java b/api/src/main/java/org/apache/cloudstack/vm/UnmanagedInstanceTO.java
index cbb7c4de698a..63ef8892d2c6 100644
--- a/api/src/main/java/org/apache/cloudstack/vm/UnmanagedInstanceTO.java
+++ b/api/src/main/java/org/apache/cloudstack/vm/UnmanagedInstanceTO.java
@@ -66,6 +66,7 @@ public enum PowerState {
private String bootType;
private String bootMode;
+ private String vmwareMoref;
public String getName() {
return name;
@@ -234,6 +235,14 @@ public void setBootMode(String bootMode) {
this.bootMode = bootMode;
}
+ public String getVmwareMoref() {
+ return vmwareMoref;
+ }
+
+ public void setVmwareMoref(String vmwareMoref) {
+ this.vmwareMoref = vmwareMoref;
+ }
+
public static class Disk {
private String diskId;
@@ -400,6 +409,16 @@ public static class Nic {
private List ipAddress;
+ // Guest network facts captured from VMware Tools while the source runs, used to preserve
+ // static configuration across the migration: IPv4 addresses in CIDR form, the default
+ // gateway serving this NIC's subnet, and the guest's DNS servers. Null when Tools is not
+ // running or the guest reports nothing for this NIC.
+ private List ipv4Cidrs;
+
+ private String ipv4Gateway;
+
+ private List dnsServers;
+
private String pciSlot;
public String getNicId() {
@@ -466,6 +485,30 @@ public void setIpAddress(List ipAddress) {
this.ipAddress = ipAddress;
}
+ public List getIpv4Cidrs() {
+ return ipv4Cidrs;
+ }
+
+ public void setIpv4Cidrs(List ipv4Cidrs) {
+ this.ipv4Cidrs = ipv4Cidrs;
+ }
+
+ public String getIpv4Gateway() {
+ return ipv4Gateway;
+ }
+
+ public void setIpv4Gateway(String ipv4Gateway) {
+ this.ipv4Gateway = ipv4Gateway;
+ }
+
+ public List getDnsServers() {
+ return dnsServers;
+ }
+
+ public void setDnsServers(List dnsServers) {
+ this.dnsServers = dnsServers;
+ }
+
public String getPciSlot() {
return pciSlot;
}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManager.java b/api/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManager.java
index e1963313eb6f..ec7128caa7fc 100644
--- a/api/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManager.java
+++ b/api/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManager.java
@@ -17,10 +17,20 @@
package org.apache.cloudstack.vm;
+import java.util.List;
+import java.util.Map;
+
+import com.cloud.dc.DataCenter;
import com.cloud.hypervisor.Hypervisor;
+import com.cloud.network.Network;
+import com.cloud.org.Cluster;
+import com.cloud.user.Account;
import com.cloud.utils.component.PluggableService;
+import com.cloud.uservm.UserVm;
+
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
+
import static com.cloud.hypervisor.Hypervisor.HypervisorType.KVM;
import static com.cloud.hypervisor.Hypervisor.HypervisorType.VMware;
@@ -75,4 +85,44 @@ public interface UnmanagedVMsManager extends VmImportService, UnmanageVMService,
static boolean isSupported(Hypervisor.HypervisorType hypervisorType) {
return hypervisorType == VMware || hypervisorType == KVM;
}
+
+ UserVm importConvertedVmwareCbtInstanceToKvm(String vcenter, String datacenterName, String username, String password,
+ String clusterName, String sourceHostName, String sourceVMName,
+ UnmanagedInstanceTO convertedInstance, DataCenter zone,
+ Cluster destinationCluster, String displayName, String hostName,
+ Account caller, Account owner, long userId, Long templateId,
+ Long serviceOfferingId, Map dataDiskOfferingMap,
+ Map nicNetworkMap,
+ Map nicIpAddressMap,
+ Long guestOsId, Map details, boolean forced,
+ Long storagePoolId);
+
+ /**
+ * Maps a VMware guest identity (config.guestFullName / config.guestId, available regardless of
+ * the source VM's power state) to a CloudStack guest OS id, so an imported VM does not inherit
+ * the import dummy template's generic OS type. Returns null when no reasonable match exists;
+ * callers then keep their previous behaviour.
+ */
+ Long resolveGuestOsIdForVmwareImport(String osName, String osId);
+
+ /**
+ * Fills in the imported VM's hardware details from the VMware source: UEFI boot (with SECURE
+ * when the source has secure boot enabled) plus the q35 machine type when the source firmware
+ * is UEFI, and a vga console for Windows guests. Only keys the caller did not provide are
+ * added, so explicit user choices always win.
+ */
+ Map applyVmwareImportHardwareDetails(Map details, String bootType, String bootMode, String osName);
+
+ /**
+ * Preserves static source IPs across the migration: for every NIC that is mapped to a
+ * CloudStack network but has no caller-provided IP, requests the guest's own IPv4 address
+ * (captured from VMware Tools while the source runs) when that address fits the target
+ * network's CIDR, is not the network's gateway and is not already in use. Caller-provided
+ * entries always win; NICs whose source address does not fit fall back to normal allocation.
+ *
+ * @param nicIdToIpv4Cidrs source NIC id to its captured IPv4 addresses in CIDR form
+ * @return the effective NIC-to-IP map to use for the import
+ */
+ Map autoFillStaticNicIpAddresses(Map nicNetworkMap,
+ Map nicIpAddressMap, Map> nicIdToIpv4Cidrs);
}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtChangedBlockInfo.java b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtChangedBlockInfo.java
new file mode 100644
index 000000000000..998bfc701dd0
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtChangedBlockInfo.java
@@ -0,0 +1,36 @@
+// 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.
+package org.apache.cloudstack.vm;
+
+public class VmwareCbtChangedBlockInfo {
+
+ private final long startOffset;
+ private final long length;
+
+ public VmwareCbtChangedBlockInfo(long startOffset, long length) {
+ this.startOffset = startOffset;
+ this.length = length;
+ }
+
+ public long getStartOffset() {
+ return startOffset;
+ }
+
+ public long getLength() {
+ return length;
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtChangedDiskInfo.java b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtChangedDiskInfo.java
new file mode 100644
index 000000000000..958cbffa3519
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtChangedDiskInfo.java
@@ -0,0 +1,46 @@
+// 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.
+package org.apache.cloudstack.vm;
+
+import java.util.Collections;
+import java.util.List;
+
+public class VmwareCbtChangedDiskInfo {
+
+ private final String sourceDiskId;
+ private final String nextChangeId;
+ private final List changedBlocks;
+
+ public VmwareCbtChangedDiskInfo(String sourceDiskId, String nextChangeId,
+ List changedBlocks) {
+ this.sourceDiskId = sourceDiskId;
+ this.nextChangeId = nextChangeId;
+ this.changedBlocks = changedBlocks == null ? Collections.emptyList() : changedBlocks;
+ }
+
+ public String getSourceDiskId() {
+ return sourceDiskId;
+ }
+
+ public String getNextChangeId() {
+ return nextChangeId;
+ }
+
+ public List getChangedBlocks() {
+ return changedBlocks;
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtDiskInfo.java b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtDiskInfo.java
new file mode 100644
index 000000000000..99ed6d5de530
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtDiskInfo.java
@@ -0,0 +1,67 @@
+// 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.
+package org.apache.cloudstack.vm;
+
+public class VmwareCbtDiskInfo {
+
+ private final String sourceDiskId;
+ private final Integer sourceDiskDeviceKey;
+ private final String label;
+ private final String sourceDiskPath;
+ private final String datastoreName;
+ private final Long capacityBytes;
+ private final String changeId;
+
+ public VmwareCbtDiskInfo(String sourceDiskId, Integer sourceDiskDeviceKey, String label, String sourceDiskPath,
+ String datastoreName, Long capacityBytes, String changeId) {
+ this.sourceDiskId = sourceDiskId;
+ this.sourceDiskDeviceKey = sourceDiskDeviceKey;
+ this.label = label;
+ this.sourceDiskPath = sourceDiskPath;
+ this.datastoreName = datastoreName;
+ this.capacityBytes = capacityBytes;
+ this.changeId = changeId;
+ }
+
+ public String getSourceDiskId() {
+ return sourceDiskId;
+ }
+
+ public Integer getSourceDiskDeviceKey() {
+ return sourceDiskDeviceKey;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public String getSourceDiskPath() {
+ return sourceDiskPath;
+ }
+
+ public String getDatastoreName() {
+ return datastoreName;
+ }
+
+ public Long getCapacityBytes() {
+ return capacityBytes;
+ }
+
+ public String getChangeId() {
+ return changeId;
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigration.java b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigration.java
new file mode 100644
index 000000000000..3f56a9c64451
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigration.java
@@ -0,0 +1,47 @@
+// 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.
+package org.apache.cloudstack.vm;
+
+import org.apache.cloudstack.api.Identity;
+import org.apache.cloudstack.api.InternalIdentity;
+
+public interface VmwareCbtMigration extends Identity, InternalIdentity {
+ enum State {
+ Created,
+ InitialSync,
+ Replicating,
+ ReadyForCutover,
+ CuttingOver,
+ ReadyForImport,
+ Completed,
+ Failed,
+ Cancelled;
+
+ public boolean isTerminal() {
+ return this == Completed || this == Failed || this == Cancelled;
+ }
+
+ public static State getValue(String state) {
+ for (State value : values()) {
+ if (value.name().equalsIgnoreCase(state)) {
+ return value;
+ }
+ }
+ throw new IllegalArgumentException("Invalid VMware CBT migration state: " + state);
+ }
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigrationCycle.java b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigrationCycle.java
new file mode 100644
index 000000000000..e1e8758358aa
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigrationCycle.java
@@ -0,0 +1,30 @@
+// 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.
+package org.apache.cloudstack.vm;
+
+import org.apache.cloudstack.api.Identity;
+import org.apache.cloudstack.api.InternalIdentity;
+
+public interface VmwareCbtMigrationCycle extends Identity, InternalIdentity {
+ enum State {
+ Created,
+ QueryingChangedAreas,
+ CopyingChangedBlocks,
+ Completed,
+ Failed
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigrationDisk.java b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigrationDisk.java
new file mode 100644
index 000000000000..41d672de69c9
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigrationDisk.java
@@ -0,0 +1,30 @@
+// 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.
+package org.apache.cloudstack.vm;
+
+import org.apache.cloudstack.api.Identity;
+import org.apache.cloudstack.api.InternalIdentity;
+
+public interface VmwareCbtMigrationDisk extends Identity, InternalIdentity {
+ enum State {
+ Created,
+ Prepared,
+ Syncing,
+ Ready,
+ Failed
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigrationManager.java b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigrationManager.java
new file mode 100644
index 000000000000..bf248782c2a0
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigrationManager.java
@@ -0,0 +1,46 @@
+// 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.
+package org.apache.cloudstack.vm;
+
+import org.apache.cloudstack.api.command.admin.vm.CancelVmwareCbtMigrationCmd;
+import org.apache.cloudstack.api.command.admin.vm.CheckVmwareCbtMigrationPrerequisitesCmd;
+import org.apache.cloudstack.api.command.admin.vm.CutoverVmwareCbtMigrationCmd;
+import org.apache.cloudstack.api.command.admin.vm.DeleteVmwareCbtMigrationCmd;
+import org.apache.cloudstack.api.command.admin.vm.ListVmwareCbtMigrationsCmd;
+import org.apache.cloudstack.api.command.admin.vm.StartVmwareCbtMigrationCmd;
+import org.apache.cloudstack.api.command.admin.vm.SyncVmwareCbtMigrationCmd;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.api.response.VmwareCbtMigrationPreflightResponse;
+import org.apache.cloudstack.api.response.VmwareCbtMigrationResponse;
+
+import com.cloud.utils.component.PluggableService;
+
+public interface VmwareCbtMigrationManager extends PluggableService {
+ VmwareCbtMigrationPreflightResponse checkVmwareCbtMigrationPrerequisites(CheckVmwareCbtMigrationPrerequisitesCmd cmd);
+
+ VmwareCbtMigrationResponse startVmwareCbtMigration(StartVmwareCbtMigrationCmd cmd);
+
+ ListResponse listVmwareCbtMigrations(ListVmwareCbtMigrationsCmd cmd);
+
+ VmwareCbtMigrationResponse syncVmwareCbtMigration(SyncVmwareCbtMigrationCmd cmd);
+
+ VmwareCbtMigrationResponse cutoverVmwareCbtMigration(CutoverVmwareCbtMigrationCmd cmd);
+
+ VmwareCbtMigrationResponse cancelVmwareCbtMigration(CancelVmwareCbtMigrationCmd cmd);
+
+ boolean deleteVmwareCbtMigration(DeleteVmwareCbtMigrationCmd cmd);
+}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigrationService.java b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigrationService.java
new file mode 100644
index 000000000000..9527ac67fd1f
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigrationService.java
@@ -0,0 +1,47 @@
+// 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.
+package org.apache.cloudstack.vm;
+
+import java.util.List;
+
+public interface VmwareCbtMigrationService {
+ VmwareCbtPreflightInfo getPreflightInfo(String vcenter, String datacenterName, String username, String password,
+ String sourceHost, String sourceVmName);
+
+ List listSourceDisks(String vcenter, String datacenterName, String username, String password,
+ String sourceHost, String sourceVmName);
+
+ List listSnapshotDisks(String vcenter, String datacenterName, String username, String password,
+ String sourceHost, String sourceVmName, String snapshotMor);
+
+ void ensureChangeTrackingEnabled(String vcenter, String datacenterName, String username, String password,
+ String sourceHost, String sourceVmName);
+
+ VmwareCbtSnapshotInfo createSnapshot(String vcenter, String datacenterName, String username, String password,
+ String sourceHost, String sourceVmName, String snapshotName,
+ String snapshotDescription, boolean quiesce);
+
+ List queryChangedDiskAreas(String vcenter, String datacenterName, String username,
+ String password, String sourceHost, String sourceVmName,
+ List disks, String snapshotMor);
+
+ UnmanagedInstanceTO.PowerState getPowerState(String vcenter, String datacenterName, String username,
+ String password, String sourceHost, String sourceVmName);
+
+ void removeSnapshot(String vcenter, String datacenterName, String username, String password, String sourceHost,
+ String sourceVmName, String snapshotMor);
+}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtPreflightDiskInfo.java b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtPreflightDiskInfo.java
new file mode 100644
index 000000000000..a574af96a083
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtPreflightDiskInfo.java
@@ -0,0 +1,59 @@
+// 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.
+package org.apache.cloudstack.vm;
+
+public class VmwareCbtPreflightDiskInfo extends VmwareCbtDiskInfo {
+
+ private final String backingType;
+ private final String diskMode;
+ private final String rdmCompatibilityMode;
+ private final boolean independentDisk;
+ private final boolean physicalRdm;
+
+ public VmwareCbtPreflightDiskInfo(String sourceDiskId, Integer sourceDiskDeviceKey, String label,
+ String sourceDiskPath, String datastoreName, Long capacityBytes,
+ String changeId, String backingType, String diskMode,
+ String rdmCompatibilityMode, boolean independentDisk,
+ boolean physicalRdm) {
+ super(sourceDiskId, sourceDiskDeviceKey, label, sourceDiskPath, datastoreName, capacityBytes, changeId);
+ this.backingType = backingType;
+ this.diskMode = diskMode;
+ this.rdmCompatibilityMode = rdmCompatibilityMode;
+ this.independentDisk = independentDisk;
+ this.physicalRdm = physicalRdm;
+ }
+
+ public String getBackingType() {
+ return backingType;
+ }
+
+ public String getDiskMode() {
+ return diskMode;
+ }
+
+ public String getRdmCompatibilityMode() {
+ return rdmCompatibilityMode;
+ }
+
+ public boolean isIndependentDisk() {
+ return independentDisk;
+ }
+
+ public boolean isPhysicalRdm() {
+ return physicalRdm;
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtPreflightInfo.java b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtPreflightInfo.java
new file mode 100644
index 000000000000..bf76922926e0
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtPreflightInfo.java
@@ -0,0 +1,138 @@
+// 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.
+package org.apache.cloudstack.vm;
+
+import java.util.Collections;
+import java.util.List;
+
+public class VmwareCbtPreflightInfo {
+
+ private final String sourceVmName;
+ private final String sourceVmMor;
+ private final Boolean changeTrackingSupported;
+ private final Boolean changeTrackingEnabled;
+ private final Boolean consolidationNeeded;
+ private final Integer existingSnapshotCount;
+ private final List disks;
+ private final List nics;
+ private final Integer cpuCores;
+ private final Integer cpuSpeed;
+ private final Integer memoryMb;
+ private final String operatingSystemId;
+ private final String operatingSystem;
+ private String bootType;
+ private String bootMode;
+
+ public VmwareCbtPreflightInfo(String sourceVmName, String sourceVmMor,
+ Boolean changeTrackingSupported, Boolean changeTrackingEnabled,
+ Boolean consolidationNeeded, Integer existingSnapshotCount,
+ List disks,
+ Integer cpuCores, Integer cpuSpeed, Integer memoryMb) {
+ this(sourceVmName, sourceVmMor, changeTrackingSupported, changeTrackingEnabled, consolidationNeeded,
+ existingSnapshotCount, disks, null, cpuCores, cpuSpeed, memoryMb, null, null);
+ }
+
+ public VmwareCbtPreflightInfo(String sourceVmName, String sourceVmMor,
+ Boolean changeTrackingSupported, Boolean changeTrackingEnabled,
+ Boolean consolidationNeeded, Integer existingSnapshotCount,
+ List disks,
+ List nics,
+ Integer cpuCores, Integer cpuSpeed, Integer memoryMb,
+ String operatingSystemId, String operatingSystem) {
+ this.sourceVmName = sourceVmName;
+ this.sourceVmMor = sourceVmMor;
+ this.changeTrackingSupported = changeTrackingSupported;
+ this.changeTrackingEnabled = changeTrackingEnabled;
+ this.consolidationNeeded = consolidationNeeded;
+ this.existingSnapshotCount = existingSnapshotCount;
+ this.disks = disks == null ? Collections.emptyList() : Collections.unmodifiableList(disks);
+ this.nics = nics == null ? Collections.emptyList() : Collections.unmodifiableList(nics);
+ this.cpuCores = cpuCores;
+ this.cpuSpeed = cpuSpeed;
+ this.memoryMb = memoryMb;
+ this.operatingSystemId = operatingSystemId;
+ this.operatingSystem = operatingSystem;
+ }
+
+ public String getSourceVmName() {
+ return sourceVmName;
+ }
+
+ public String getSourceVmMor() {
+ return sourceVmMor;
+ }
+
+ public Boolean getChangeTrackingSupported() {
+ return changeTrackingSupported;
+ }
+
+ public Boolean getChangeTrackingEnabled() {
+ return changeTrackingEnabled;
+ }
+
+ public Boolean getConsolidationNeeded() {
+ return consolidationNeeded;
+ }
+
+ public Integer getExistingSnapshotCount() {
+ return existingSnapshotCount;
+ }
+
+ public List getNics() {
+ return nics;
+ }
+
+ public List getDisks() {
+ return disks;
+ }
+
+ public Integer getCpuCores() {
+ return cpuCores;
+ }
+
+ public Integer getCpuSpeed() {
+ return cpuSpeed;
+ }
+
+ public Integer getMemoryMb() {
+ return memoryMb;
+ }
+
+ public String getOperatingSystemId() {
+ return operatingSystemId;
+ }
+
+ public String getOperatingSystem() {
+ return operatingSystem;
+ }
+
+ public String getBootType() {
+ return bootType;
+ }
+
+ public void setBootType(String bootType) {
+ this.bootType = bootType;
+ }
+
+ public String getBootMode() {
+ return bootMode;
+ }
+
+ public void setBootMode(String bootMode) {
+ this.bootMode = bootMode;
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtPreflightNicInfo.java b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtPreflightNicInfo.java
new file mode 100644
index 000000000000..d22d61235d7a
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtPreflightNicInfo.java
@@ -0,0 +1,87 @@
+// 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.
+package org.apache.cloudstack.vm;
+
+import java.util.List;
+
+/**
+ * A source VM NIC discovered during VMware CBT preflight, so NIC-to-network
+ * mappings can be validated when the migration is started instead of failing
+ * at import time, after replication has already run.
+ */
+public class VmwareCbtPreflightNicInfo {
+
+ private final String sourceNicId;
+ private final String adapterType;
+ private final String macAddress;
+ private final Integer vlan;
+
+ // Guest network facts from VMware Tools (available while the source runs): the IPv4
+ // addresses in CIDR form, the default gateway of this NIC's subnet, and the guest's DNS
+ // servers. Null when Tools is not running or reports nothing for this NIC. Used to
+ // preserve a static source IP across the migration when it fits the target network.
+ private List ipv4Cidrs;
+ private String ipv4Gateway;
+ private List dnsServers;
+
+ public VmwareCbtPreflightNicInfo(String sourceNicId, String adapterType, String macAddress, Integer vlan) {
+ this.sourceNicId = sourceNicId;
+ this.adapterType = adapterType;
+ this.macAddress = macAddress;
+ this.vlan = vlan;
+ }
+
+ public String getSourceNicId() {
+ return sourceNicId;
+ }
+
+ public String getAdapterType() {
+ return adapterType;
+ }
+
+ public String getMacAddress() {
+ return macAddress;
+ }
+
+ public Integer getVlan() {
+ return vlan;
+ }
+
+ public List getIpv4Cidrs() {
+ return ipv4Cidrs;
+ }
+
+ public void setIpv4Cidrs(List ipv4Cidrs) {
+ this.ipv4Cidrs = ipv4Cidrs;
+ }
+
+ public String getIpv4Gateway() {
+ return ipv4Gateway;
+ }
+
+ public void setIpv4Gateway(String ipv4Gateway) {
+ this.ipv4Gateway = ipv4Gateway;
+ }
+
+ public List getDnsServers() {
+ return dnsServers;
+ }
+
+ public void setDnsServers(List dnsServers) {
+ this.dnsServers = dnsServers;
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtSnapshotInfo.java b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtSnapshotInfo.java
new file mode 100644
index 000000000000..c8bfe3edbe84
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtSnapshotInfo.java
@@ -0,0 +1,46 @@
+// 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.
+package org.apache.cloudstack.vm;
+
+public class VmwareCbtSnapshotInfo {
+
+ private final String snapshotName;
+ private final String snapshotMor;
+ private final String sourceVmMor;
+
+ public VmwareCbtSnapshotInfo(String snapshotName, String snapshotMor) {
+ this(snapshotName, snapshotMor, null);
+ }
+
+ public VmwareCbtSnapshotInfo(String snapshotName, String snapshotMor, String sourceVmMor) {
+ this.snapshotName = snapshotName;
+ this.snapshotMor = snapshotMor;
+ this.sourceVmMor = sourceVmMor;
+ }
+
+ public String getSnapshotName() {
+ return snapshotName;
+ }
+
+ public String getSnapshotMor() {
+ return snapshotMor;
+ }
+
+ public String getSourceVmMor() {
+ return sourceVmMor;
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroup.java b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroup.java
new file mode 100644
index 000000000000..f22a4f4b8e98
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroup.java
@@ -0,0 +1,33 @@
+// 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.
+
+package org.apache.cloudstack.vm.bootgroup;
+
+import java.util.Date;
+
+import org.apache.cloudstack.acl.ControlledEntity;
+import org.apache.cloudstack.api.Identity;
+import org.apache.cloudstack.api.InternalIdentity;
+
+public interface InstanceBootGroup extends ControlledEntity, Identity, InternalIdentity {
+
+ String getName();
+
+ String getDescription();
+
+ Date getCreated();
+}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMember.java b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMember.java
new file mode 100644
index 000000000000..8f070634c040
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMember.java
@@ -0,0 +1,44 @@
+// 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.
+
+package org.apache.cloudstack.vm.bootgroup;
+
+import java.util.Date;
+
+import org.apache.cloudstack.api.Identity;
+import org.apache.cloudstack.api.InternalIdentity;
+
+public interface InstanceBootGroupMember extends Identity, InternalIdentity {
+
+ long getBootGroupId();
+
+ MemberType getMemberType();
+
+ long getMemberId();
+
+ int getBootOrder();
+
+ Date getCreated();
+
+ enum MemberType {
+ VirtualMachine, InstanceGroup
+ }
+
+ enum ReadinessMode {
+ None, RuleBased, ChildDependent
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupService.java b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupService.java
new file mode 100644
index 000000000000..111bf4c50840
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupService.java
@@ -0,0 +1,81 @@
+// 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.
+
+package org.apache.cloudstack.vm.bootgroup;
+
+import org.apache.cloudstack.api.command.user.bootgroup.AddMemberToInstanceBootGroupCmd;
+import org.apache.cloudstack.api.command.user.bootgroup.CreateInstanceBootGroupCmd;
+import org.apache.cloudstack.api.command.user.bootgroup.CreateInstanceBootGroupReadinessRuleCmd;
+import org.apache.cloudstack.api.command.user.bootgroup.DeleteInstanceBootGroupCmd;
+import org.apache.cloudstack.api.command.user.bootgroup.DeleteInstanceBootGroupReadinessRuleCmd;
+import org.apache.cloudstack.api.command.user.bootgroup.ListInstanceBootGroupMembersCmd;
+import org.apache.cloudstack.api.command.user.bootgroup.ListInstanceBootGroupReadinessRulesCmd;
+import org.apache.cloudstack.api.command.user.bootgroup.ListInstanceBootGroupsCmd;
+import org.apache.cloudstack.api.command.user.bootgroup.RebootInstanceBootGroupCmd;
+import org.apache.cloudstack.api.command.user.bootgroup.RemoveInstanceBootGroupMemberCmd;
+import org.apache.cloudstack.api.command.user.bootgroup.StartInstanceBootGroupCmd;
+import org.apache.cloudstack.api.command.user.bootgroup.StopInstanceBootGroupCmd;
+import org.apache.cloudstack.api.command.user.bootgroup.UpdateInstanceBootGroupCmd;
+import org.apache.cloudstack.api.command.user.bootgroup.UpdateInstanceBootGroupMemberCmd;
+import org.apache.cloudstack.api.command.user.bootgroup.UpdateInstanceBootGroupReadinessRuleCmd;
+import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse;
+import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse;
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule;
+
+public interface InstanceBootGroupService {
+
+ InstanceBootGroup createInstanceBootGroup(CreateInstanceBootGroupCmd cmd);
+
+ boolean deleteInstanceBootGroup(DeleteInstanceBootGroupCmd cmd);
+
+ InstanceBootGroup updateInstanceBootGroup(UpdateInstanceBootGroupCmd cmd);
+
+ ListResponse listInstanceBootGroups(ListInstanceBootGroupsCmd cmd);
+
+ InstanceBootGroupMember addMemberToInstanceBootGroup(AddMemberToInstanceBootGroupCmd cmd);
+
+ boolean removeInstanceBootGroupMember(RemoveInstanceBootGroupMemberCmd cmd);
+
+ InstanceBootGroupMember updateInstanceBootGroupMember(UpdateInstanceBootGroupMemberCmd cmd);
+
+ ListResponse listInstanceBootGroupMembers(ListInstanceBootGroupMembersCmd cmd);
+
+ InstanceBootGroup startInstanceBootGroup(StartInstanceBootGroupCmd cmd);
+
+ InstanceBootGroup stopInstanceBootGroup(StopInstanceBootGroupCmd cmd);
+
+ InstanceBootGroup rebootInstanceBootGroup(RebootInstanceBootGroupCmd cmd);
+
+ InstanceBootGroupResponse createInstanceBootGroupResponse(long id);
+
+ InstanceBootGroupMemberResponse createInstanceBootGroupMemberResponse(InstanceBootGroupMember member);
+
+ InstanceBootGroupReadinessRule createInstanceBootGroupReadinessRule(CreateInstanceBootGroupReadinessRuleCmd cmd);
+
+ InstanceBootGroupReadinessRule updateInstanceBootGroupReadinessRule(UpdateInstanceBootGroupReadinessRuleCmd cmd);
+
+ boolean deleteInstanceBootGroupReadinessRule(DeleteInstanceBootGroupReadinessRuleCmd cmd);
+
+ ListResponse listInstanceBootGroupReadinessRules(ListInstanceBootGroupReadinessRulesCmd cmd);
+
+ InstanceBootGroupReadinessRuleResponse createInstanceBootGroupReadinessRuleResponse(InstanceBootGroupReadinessRule rule);
+
+ Long getInstanceBootGroupIdForMember(long memberId);
+
+}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRule.java b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRule.java
new file mode 100644
index 000000000000..e1933f9c6d12
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRule.java
@@ -0,0 +1,81 @@
+// 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.
+
+package org.apache.cloudstack.vm.bootgroup.readiness;
+
+import java.util.Date;
+
+import org.apache.cloudstack.api.Identity;
+import org.apache.cloudstack.api.InternalIdentity;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember;
+
+/**
+ * A readiness rule always belongs to exactly one boot group and references one "item" within it:
+ * a direct {@code VirtualMachine}-type member, a direct {@code InstanceGroup}-type member, or a VM
+ * sitting inside one of the boot group's {@code InstanceGroup}-type members (no member row of its
+ * own, referenced directly by VM id).
+ */
+public interface InstanceBootGroupReadinessRule extends Identity, InternalIdentity {
+
+ long getBootGroupId();
+
+ InstanceBootGroupMember.MemberType getItemType();
+
+ long getItemId();
+
+ RuleType getRuleType();
+
+ String getName();
+
+ boolean isEnabled();
+
+ Date getCreated();
+
+ /**
+ * Which rule types are valid depends on {@link InstanceBootGroupMember.MemberType},
+ * hypervisor-agnostic at the DB/API layer (no qemu/KVM naming stored). Ping/PortCheck/
+ * GuestAgentLiveness apply to VirtualMachine items; MemberQuorum applies to InstanceGroup
+ * items. CustomScript is reserved for a future checker/evaluator implementation and is not
+ * currently accepted by the API for either item type.
+ */
+ enum RuleType {
+ GuestAgentLiveness(true),
+ Ping(true),
+ PortCheck(true),
+ CustomScript(false),
+ MemberQuorum(false);
+
+ private final boolean memberTargeted;
+
+ RuleType(boolean memberTargeted) {
+ this.memberTargeted = memberTargeted;
+ }
+
+ /**
+ * True for a rule type that, when attached to an InstanceGroup, is evaluated against every
+ * current member individually and inherited by each member's own readiness — unlike
+ * MemberQuorum, which only ever operates at group scope.
+ */
+ public boolean isMemberTargeted() {
+ return memberTargeted;
+ }
+ }
+
+ enum Status {
+ Ready, NotReady, Error, Unknown
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/ReadinessChecker.java b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/ReadinessChecker.java
new file mode 100644
index 000000000000..cb66ad2b1d82
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/ReadinessChecker.java
@@ -0,0 +1,93 @@
+// 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.
+
+package org.apache.cloudstack.vm.bootgroup.readiness;
+
+import java.util.Map;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * Strategy interface for evaluating one readiness rule type. Implementations are collected by
+ * {@code InstanceBootGroupReadinessRuleManagerImpl} via Spring's {@code List}
+ * autowiring and dispatched by {@link #getRuleType()} — an internal implementation detail, not
+ * API-facing, so left unprefixed.
+ */
+public interface ReadinessChecker {
+
+ /**
+ * Below this much remaining budget, a checker should not even attempt to dispatch a remote
+ * command — there isn't enough time left for a meaningful wait, and dispatching anyway would
+ * either use an unhelpfully tiny (or, worse, a zero/negative, which some transports treat as "no
+ * override, use the default") wait value.
+ */
+ long MIN_REMAINING_MS_TO_DISPATCH = 2000L;
+
+ InstanceBootGroupReadinessRule.RuleType getRuleType();
+
+ /**
+ * @param remainingMs time budget left for this VM's current attempt; bound any remote dispatch
+ * to it (e.g. via {@code Command.setWait}) and return {@code Status.Error} directly if
+ * it's already too small to be worth dispatching.
+ */
+ Result check(InstanceBootGroupReadinessRule rule, Map details, long vmId, long remainingMs);
+
+ class Result {
+ private final InstanceBootGroupReadinessRule.Status status;
+ private final String message;
+
+ public Result(InstanceBootGroupReadinessRule.Status status, String message) {
+ this.status = status;
+ this.message = message;
+ }
+
+ public InstanceBootGroupReadinessRule.Status getStatus() {
+ return status;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+ }
+
+ default Logger getLogger() {
+ return null;
+ }
+
+ default Result logAndReturn(InstanceBootGroupReadinessRule rule, Object vmOrId, Result result) {
+ Logger log = getLogger();
+ if (log == null) {
+ return result;
+ }
+ Level level = InstanceBootGroupReadinessRule.Status.Ready.equals(result.getStatus())
+ ? Level.DEBUG
+ : Level.WARN;
+
+ log.log(level, "{} evaluated for {}: status={}, message={}",
+ rule, vmOrId, result.getStatus(), result.getMessage());
+ return result;
+ }
+
+ /**
+ * Halves the remaining budget and floors at 1 second for {@code Command.setWait}, which doubles
+ * whatever value it's given and treats 0 as "use the global default" rather than "no wait".
+ */
+ default int computeWaitSeconds(long remainingMs) {
+ return (int) Math.max(1, remainingMs / 2000);
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/AddMemberToInstanceBootGroupCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/AddMemberToInstanceBootGroupCmdTest.java
new file mode 100644
index 000000000000..45880112bcad
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/AddMemberToInstanceBootGroupCmdTest.java
@@ -0,0 +1,95 @@
+// 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.
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember;
+import org.junit.Test;
+
+public class AddMemberToInstanceBootGroupCmdTest extends BaseBootGroupCmdTest {
+
+ private AddMemberToInstanceBootGroupCmd createCmd() throws Exception {
+ AddMemberToInstanceBootGroupCmd cmd = new AddMemberToInstanceBootGroupCmd();
+ setField(cmd, "instanceBootGroupService", instanceBootGroupService);
+ setField(cmd, "id", ENTITY_ID);
+ setField(cmd, "virtualMachineId", 500L);
+ setField(cmd, "order", 2);
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ AddMemberToInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getId());
+ assertEquals(Long.valueOf(500L), cmd.getVirtualMachineId());
+ assertEquals(2, cmd.getOrder());
+ }
+
+ @Test
+ public void testGetEntityOwnerId() throws Exception {
+ AddMemberToInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetApiResourceType() throws Exception {
+ AddMemberToInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(ApiCommandResourceType.InstanceBootGroup, cmd.getApiResourceType());
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getApiResourceId());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ AddMemberToInstanceBootGroupCmd cmd = createCmd();
+
+ InstanceBootGroupMember member = mock(InstanceBootGroupMember.class);
+ InstanceBootGroupMemberResponse mockResponse = new InstanceBootGroupMemberResponse();
+
+ when(instanceBootGroupService.addMemberToInstanceBootGroup(cmd)).thenReturn(member);
+ when(instanceBootGroupService.createInstanceBootGroupMemberResponse(member)).thenReturn(mockResponse);
+
+ cmd.execute();
+
+ InstanceBootGroupMemberResponse response = (InstanceBootGroupMemberResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("addmembertoinstancebootgroupresponse", response.getResponseName());
+ verify(instanceBootGroupService).addMemberToInstanceBootGroup(cmd);
+ verify(instanceBootGroupService).createInstanceBootGroupMemberResponse(member);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsNull() throws Exception {
+ AddMemberToInstanceBootGroupCmd cmd = createCmd();
+ when(instanceBootGroupService.addMemberToInstanceBootGroup(cmd)).thenReturn(null);
+ cmd.execute();
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testExecutePropagatesServiceException() throws Exception {
+ AddMemberToInstanceBootGroupCmd cmd = createCmd();
+ when(instanceBootGroupService.addMemberToInstanceBootGroup(cmd)).thenThrow(new RuntimeException("conflict"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/BaseBootGroupCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/BaseBootGroupCmdTest.java
new file mode 100644
index 000000000000..f3d62d207733
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/BaseBootGroupCmdTest.java
@@ -0,0 +1,88 @@
+// 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.
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Field;
+
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService;
+import org.junit.After;
+import org.junit.Before;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import com.cloud.user.Account;
+import com.cloud.user.AccountService;
+
+/**
+ * Shared setup for all Instance Boot Group command unit tests.
+ */
+public abstract class BaseBootGroupCmdTest {
+
+ protected static final long ACCOUNT_ID = 42L;
+ protected static final long ENTITY_ID = 100L;
+
+ protected InstanceBootGroupService instanceBootGroupService;
+ protected AccountService accountService;
+ protected Account callingAccount;
+
+ private MockedStatic callContextMock;
+
+ @Before
+ public void setUp() {
+ instanceBootGroupService = mock(InstanceBootGroupService.class);
+ accountService = mock(AccountService.class);
+
+ callingAccount = mock(Account.class);
+ when(callingAccount.getId()).thenReturn(ACCOUNT_ID);
+
+ CallContext callContext = mock(CallContext.class);
+ when(callContext.getCallingAccount()).thenReturn(callingAccount);
+
+ callContextMock = Mockito.mockStatic(CallContext.class);
+ callContextMock.when(CallContext::current).thenReturn(callContext);
+ }
+
+ @After
+ public void tearDown() {
+ callContextMock.close();
+ }
+
+ /**
+ * Sets a private/inherited field value via reflection.
+ */
+ protected void setField(Object target, String fieldName, Object value) throws Exception {
+ Field field = null;
+ Class> clazz = target.getClass();
+ while (clazz != null) {
+ try {
+ field = clazz.getDeclaredField(fieldName);
+ break;
+ } catch (NoSuchFieldException e) {
+ clazz = clazz.getSuperclass();
+ }
+ }
+ if (field == null) {
+ throw new NoSuchFieldException(fieldName + " not found in hierarchy of " + target.getClass().getName());
+ }
+ field.setAccessible(true);
+ field.set(target, value);
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupCmdTest.java
new file mode 100644
index 000000000000..c6352dcc94ef
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupCmdTest.java
@@ -0,0 +1,107 @@
+// 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.
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup;
+import org.junit.Test;
+
+public class CreateInstanceBootGroupCmdTest extends BaseBootGroupCmdTest {
+
+ private CreateInstanceBootGroupCmd createCmd() throws Exception {
+ CreateInstanceBootGroupCmd cmd = new CreateInstanceBootGroupCmd();
+ setField(cmd, "instanceBootGroupService", instanceBootGroupService);
+ setField(cmd, "_accountService", accountService);
+ setField(cmd, "name", "web-tier");
+ setField(cmd, "description", "web tier boot group");
+ setField(cmd, "readinessAttemptTimeoutSeconds", 120L);
+ setField(cmd, "readinessMaxRetryAttempts", 3L);
+ setField(cmd, "readinessRebootOnRetry", true);
+ setField(cmd, "readinessInitialDelaySeconds", 15L);
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ CreateInstanceBootGroupCmd cmd = createCmd();
+ assertEquals("web-tier", cmd.getName());
+ assertEquals("web tier boot group", cmd.getDescription());
+ assertEquals(Long.valueOf(120L), cmd.getReadinessAttemptTimeoutSeconds());
+ assertEquals(Long.valueOf(3L), cmd.getReadinessMaxRetryAttempts());
+ assertEquals(Boolean.TRUE, cmd.getReadinessRebootOnRetry());
+ assertEquals(Long.valueOf(15L), cmd.getReadinessInitialDelaySeconds());
+ }
+
+ @Test
+ public void testGetEntityOwnerIdResolvedFromAccount() throws Exception {
+ CreateInstanceBootGroupCmd cmd = createCmd();
+ setField(cmd, "accountName", "someaccount");
+ setField(cmd, "domainId", 5L);
+ when(accountService.finalizeAccountId("someaccount", 5L, null, true)).thenReturn(200L);
+
+ assertEquals(200L, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetEntityOwnerIdFallsBackToCaller() throws Exception {
+ CreateInstanceBootGroupCmd cmd = createCmd();
+ when(accountService.finalizeAccountId(null, null, null, true)).thenReturn(null);
+
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ CreateInstanceBootGroupCmd cmd = createCmd();
+
+ InstanceBootGroup group = mock(InstanceBootGroup.class);
+ when(group.getId()).thenReturn(ENTITY_ID);
+ InstanceBootGroupResponse mockResponse = new InstanceBootGroupResponse();
+
+ when(instanceBootGroupService.createInstanceBootGroup(cmd)).thenReturn(group);
+ when(instanceBootGroupService.createInstanceBootGroupResponse(ENTITY_ID)).thenReturn(mockResponse);
+
+ cmd.execute();
+
+ InstanceBootGroupResponse response = (InstanceBootGroupResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("createinstancebootgroupresponse", response.getResponseName());
+ verify(instanceBootGroupService).createInstanceBootGroup(cmd);
+ verify(instanceBootGroupService).createInstanceBootGroupResponse(ENTITY_ID);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsNull() throws Exception {
+ CreateInstanceBootGroupCmd cmd = createCmd();
+ when(instanceBootGroupService.createInstanceBootGroup(cmd)).thenReturn(null);
+ cmd.execute();
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testExecutePropagatesServiceException() throws Exception {
+ CreateInstanceBootGroupCmd cmd = createCmd();
+ when(instanceBootGroupService.createInstanceBootGroup(cmd)).thenThrow(new RuntimeException("db error"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupReadinessRuleCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupReadinessRuleCmdTest.java
new file mode 100644
index 000000000000..7efb8ec4c797
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupReadinessRuleCmdTest.java
@@ -0,0 +1,158 @@
+// 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.
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse;
+import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule;
+import org.junit.Test;
+
+public class CreateInstanceBootGroupReadinessRuleCmdTest extends BaseBootGroupCmdTest {
+
+ private CreateInstanceBootGroupReadinessRuleCmd createCmd() throws Exception {
+ CreateInstanceBootGroupReadinessRuleCmd cmd = new CreateInstanceBootGroupReadinessRuleCmd();
+ setField(cmd, "instanceBootGroupService", instanceBootGroupService);
+ setField(cmd, "bootGroupId", ENTITY_ID);
+ setField(cmd, "virtualMachineId", 500L);
+ setField(cmd, "instanceGroupId", null);
+ setField(cmd, "ruleType", "PortCheck");
+ setField(cmd, "name", "port-check-rule");
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getBootGroupId());
+ assertEquals(Long.valueOf(500L), cmd.getVirtualMachineId());
+ assertNull(cmd.getInstanceGroupId());
+ assertEquals("PortCheck", cmd.getRuleType());
+ assertEquals("port-check-rule", cmd.getName());
+ }
+
+ @Test
+ public void testGetEntityOwnerId() throws Exception {
+ CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetApiResourceType() throws Exception {
+ CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ assertEquals(ApiCommandResourceType.InstanceBootGroupReadinessRule, cmd.getApiResourceType());
+ }
+
+ @Test
+ public void testIsEnabledDefaultsToTrueWhenNull() throws Exception {
+ CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ setField(cmd, "enabled", null);
+ assertTrue(cmd.isEnabled());
+ }
+
+ @Test
+ public void testIsEnabledFalse() throws Exception {
+ CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ setField(cmd, "enabled", false);
+ assertFalse(cmd.isEnabled());
+ }
+
+ @Test
+ public void testIsEnabledTrue() throws Exception {
+ CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ setField(cmd, "enabled", true);
+ assertTrue(cmd.isEnabled());
+ }
+
+ @Test
+ public void testGetDetailsNull() throws Exception {
+ CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ setField(cmd, "details", null);
+ assertNull(cmd.getDetails());
+ }
+
+ @Test
+ public void testGetDetailsEmptyMap() throws Exception {
+ CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ setField(cmd, "details", new HashMap<>());
+ assertNull(cmd.getDetails());
+ }
+
+ @Test
+ public void testGetDetailsPopulatedMap() throws Exception {
+ CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+
+ Map innerMap = new HashMap<>();
+ innerMap.put("port", "8080");
+ innerMap.put("protocol", "tcp");
+
+ Map> outerMap = new HashMap<>();
+ outerMap.put("0", innerMap);
+
+ setField(cmd, "details", outerMap);
+
+ Map details = cmd.getDetails();
+ assertNotNull(details);
+ assertEquals("8080", details.get("port"));
+ assertEquals("tcp", details.get("protocol"));
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+
+ InstanceBootGroupReadinessRule rule = mock(InstanceBootGroupReadinessRule.class);
+ InstanceBootGroupReadinessRuleResponse mockResponse = new InstanceBootGroupReadinessRuleResponse();
+
+ when(instanceBootGroupService.createInstanceBootGroupReadinessRule(cmd)).thenReturn(rule);
+ when(instanceBootGroupService.createInstanceBootGroupReadinessRuleResponse(rule)).thenReturn(mockResponse);
+
+ cmd.execute();
+
+ InstanceBootGroupReadinessRuleResponse response = (InstanceBootGroupReadinessRuleResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("createinstancebootgroupreadinessruleresponse", response.getResponseName());
+ verify(instanceBootGroupService).createInstanceBootGroupReadinessRule(cmd);
+ verify(instanceBootGroupService).createInstanceBootGroupReadinessRuleResponse(rule);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsNull() throws Exception {
+ CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ when(instanceBootGroupService.createInstanceBootGroupReadinessRule(cmd)).thenReturn(null);
+ cmd.execute();
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testExecutePropagatesServiceException() throws Exception {
+ CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ when(instanceBootGroupService.createInstanceBootGroupReadinessRule(cmd)).thenThrow(new RuntimeException("db error"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupCmdTest.java
new file mode 100644
index 000000000000..503efc5e94b1
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupCmdTest.java
@@ -0,0 +1,83 @@
+// 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.
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.SuccessResponse;
+import org.junit.Test;
+
+public class DeleteInstanceBootGroupCmdTest extends BaseBootGroupCmdTest {
+
+ private DeleteInstanceBootGroupCmd createCmd() throws Exception {
+ DeleteInstanceBootGroupCmd cmd = new DeleteInstanceBootGroupCmd();
+ setField(cmd, "instanceBootGroupService", instanceBootGroupService);
+ setField(cmd, "id", ENTITY_ID);
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ DeleteInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getId());
+ }
+
+ @Test
+ public void testGetEntityOwnerId() throws Exception {
+ DeleteInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetApiResourceType() throws Exception {
+ DeleteInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(ApiCommandResourceType.InstanceBootGroup, cmd.getApiResourceType());
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getApiResourceId());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ DeleteInstanceBootGroupCmd cmd = createCmd();
+ when(instanceBootGroupService.deleteInstanceBootGroup(cmd)).thenReturn(true);
+
+ cmd.execute();
+
+ SuccessResponse response = (SuccessResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("deleteinstancebootgroupresponse", response.getResponseName());
+ verify(instanceBootGroupService).deleteInstanceBootGroup(cmd);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsFalse() throws Exception {
+ DeleteInstanceBootGroupCmd cmd = createCmd();
+ when(instanceBootGroupService.deleteInstanceBootGroup(cmd)).thenReturn(false);
+ cmd.execute();
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testExecutePropagatesServiceException() throws Exception {
+ DeleteInstanceBootGroupCmd cmd = createCmd();
+ when(instanceBootGroupService.deleteInstanceBootGroup(cmd)).thenThrow(new RuntimeException("db error"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupReadinessRuleCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupReadinessRuleCmdTest.java
new file mode 100644
index 000000000000..d31803703a3e
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupReadinessRuleCmdTest.java
@@ -0,0 +1,83 @@
+// 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.
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.SuccessResponse;
+import org.junit.Test;
+
+public class DeleteInstanceBootGroupReadinessRuleCmdTest extends BaseBootGroupCmdTest {
+
+ private DeleteInstanceBootGroupReadinessRuleCmd createCmd() throws Exception {
+ DeleteInstanceBootGroupReadinessRuleCmd cmd = new DeleteInstanceBootGroupReadinessRuleCmd();
+ setField(cmd, "instanceBootGroupService", instanceBootGroupService);
+ setField(cmd, "id", ENTITY_ID);
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ DeleteInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getId());
+ }
+
+ @Test
+ public void testGetEntityOwnerId() throws Exception {
+ DeleteInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetApiResourceType() throws Exception {
+ DeleteInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ assertEquals(ApiCommandResourceType.InstanceBootGroupReadinessRule, cmd.getApiResourceType());
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getApiResourceId());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ DeleteInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ when(instanceBootGroupService.deleteInstanceBootGroupReadinessRule(cmd)).thenReturn(true);
+
+ cmd.execute();
+
+ SuccessResponse response = (SuccessResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("deleteinstancebootgroupreadinessruleresponse", response.getResponseName());
+ verify(instanceBootGroupService).deleteInstanceBootGroupReadinessRule(cmd);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsFalse() throws Exception {
+ DeleteInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ when(instanceBootGroupService.deleteInstanceBootGroupReadinessRule(cmd)).thenReturn(false);
+ cmd.execute();
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testExecutePropagatesServiceException() throws Exception {
+ DeleteInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ when(instanceBootGroupService.deleteInstanceBootGroupReadinessRule(cmd)).thenThrow(new RuntimeException("db error"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupMembersCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupMembersCmdTest.java
new file mode 100644
index 000000000000..3a9f34fef4d6
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupMembersCmdTest.java
@@ -0,0 +1,143 @@
+// 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.
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.junit.Test;
+
+public class ListInstanceBootGroupMembersCmdTest extends BaseBootGroupCmdTest {
+
+ private ListInstanceBootGroupMembersCmd createCmd() throws Exception {
+ ListInstanceBootGroupMembersCmd cmd = new ListInstanceBootGroupMembersCmd();
+ setField(cmd, "instanceBootGroupService", instanceBootGroupService);
+ setField(cmd, "bootGroupId", ENTITY_ID);
+ setField(cmd, "memberType", "VirtualMachine");
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ ListInstanceBootGroupMembersCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getBootGroupId());
+ assertEquals("VirtualMachine", cmd.getMemberType());
+ }
+
+ @Test
+ public void testIsIgnoreInstanceStateNull() throws Exception {
+ ListInstanceBootGroupMembersCmd cmd = createCmd();
+ assertFalse(cmd.isIgnoreInstanceState());
+ }
+
+ @Test
+ public void testIsIgnoreInstanceStateFalse() throws Exception {
+ ListInstanceBootGroupMembersCmd cmd = createCmd();
+ setField(cmd, "ignoreInstanceState", false);
+ assertFalse(cmd.isIgnoreInstanceState());
+ }
+
+ @Test
+ public void testIsIgnoreInstanceStateTrue() throws Exception {
+ ListInstanceBootGroupMembersCmd cmd = createCmd();
+ setField(cmd, "ignoreInstanceState", true);
+ assertTrue(cmd.isIgnoreInstanceState());
+ }
+
+ @Test
+ public void testDetailFlagsNullList() throws Exception {
+ ListInstanceBootGroupMembersCmd cmd = createCmd();
+ assertFalse(cmd.isReadinessDetailRequested());
+ assertFalse(cmd.isChildrenDetailRequested());
+ }
+
+ @Test
+ public void testDetailFlagsEmptyList() throws Exception {
+ ListInstanceBootGroupMembersCmd cmd = createCmd();
+ setField(cmd, "viewDetails", Collections.emptyList());
+ assertFalse(cmd.isReadinessDetailRequested());
+ assertFalse(cmd.isChildrenDetailRequested());
+ }
+
+ @Test
+ public void testDetailFlagsListWithoutKeyword() throws Exception {
+ ListInstanceBootGroupMembersCmd cmd = createCmd();
+ setField(cmd, "viewDetails", Collections.singletonList("other"));
+ assertFalse(cmd.isReadinessDetailRequested());
+ assertFalse(cmd.isChildrenDetailRequested());
+ }
+
+ @Test
+ public void testDetailFlagsReadinessOnly() throws Exception {
+ ListInstanceBootGroupMembersCmd cmd = createCmd();
+ List details = Collections.singletonList("readiness");
+ setField(cmd, "viewDetails", details);
+ assertTrue(cmd.isReadinessDetailRequested());
+ assertFalse(cmd.isChildrenDetailRequested());
+ }
+
+ @Test
+ public void testDetailFlagsChildrenOnly() throws Exception {
+ ListInstanceBootGroupMembersCmd cmd = createCmd();
+ List details = Collections.singletonList("children");
+ setField(cmd, "viewDetails", details);
+ assertFalse(cmd.isReadinessDetailRequested());
+ assertTrue(cmd.isChildrenDetailRequested());
+ }
+
+ @Test
+ public void testDetailFlagsAll() throws Exception {
+ ListInstanceBootGroupMembersCmd cmd = createCmd();
+ List details = Collections.singletonList("all");
+ setField(cmd, "viewDetails", details);
+ assertTrue(cmd.isReadinessDetailRequested());
+ assertTrue(cmd.isChildrenDetailRequested());
+ }
+
+ @Test
+ public void testDetailFlagsBothExplicit() throws Exception {
+ ListInstanceBootGroupMembersCmd cmd = createCmd();
+ List details = Arrays.asList("readiness", "children");
+ setField(cmd, "viewDetails", details);
+ assertTrue(cmd.isReadinessDetailRequested());
+ assertTrue(cmd.isChildrenDetailRequested());
+ }
+
+ @Test
+ public void testExecute() throws Exception {
+ ListInstanceBootGroupMembersCmd cmd = createCmd();
+
+ ListResponse mockListResponse = new ListResponse<>();
+ when(instanceBootGroupService.listInstanceBootGroupMembers(cmd)).thenReturn(mockListResponse);
+
+ cmd.execute();
+
+ @SuppressWarnings("unchecked")
+ ListResponse response = (ListResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("listinstancebootgroupmembersresponse", response.getResponseName());
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupReadinessRulesCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupReadinessRulesCmdTest.java
new file mode 100644
index 000000000000..92b6f22110ba
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupReadinessRulesCmdTest.java
@@ -0,0 +1,65 @@
+// 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.
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.junit.Test;
+
+public class ListInstanceBootGroupReadinessRulesCmdTest extends BaseBootGroupCmdTest {
+
+ private ListInstanceBootGroupReadinessRulesCmd createCmd() throws Exception {
+ ListInstanceBootGroupReadinessRulesCmd cmd = new ListInstanceBootGroupReadinessRulesCmd();
+ setField(cmd, "instanceBootGroupService", instanceBootGroupService);
+ setField(cmd, "bootGroupId", ENTITY_ID);
+ setField(cmd, "id", 700L);
+ setField(cmd, "virtualMachineId", 500L);
+ setField(cmd, "instanceGroupId", 600L);
+ setField(cmd, "ruleType", "Http");
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ ListInstanceBootGroupReadinessRulesCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getBootGroupId());
+ assertEquals(Long.valueOf(700L), cmd.getId());
+ assertEquals(Long.valueOf(500L), cmd.getVirtualMachineId());
+ assertEquals(Long.valueOf(600L), cmd.getInstanceGroupId());
+ assertEquals("Http", cmd.getRuleType());
+ }
+
+ @Test
+ public void testExecute() throws Exception {
+ ListInstanceBootGroupReadinessRulesCmd cmd = createCmd();
+
+ ListResponse mockListResponse = new ListResponse<>();
+ when(instanceBootGroupService.listInstanceBootGroupReadinessRules(cmd)).thenReturn(mockListResponse);
+
+ cmd.execute();
+
+ @SuppressWarnings("unchecked")
+ ListResponse response =
+ (ListResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("listinstancebootgroupreadinessrulesresponse", response.getResponseName());
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupsCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupsCmdTest.java
new file mode 100644
index 000000000000..599cb05c3ba7
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupsCmdTest.java
@@ -0,0 +1,62 @@
+// 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.
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.junit.Test;
+
+public class ListInstanceBootGroupsCmdTest extends BaseBootGroupCmdTest {
+
+ private ListInstanceBootGroupsCmd createCmd() throws Exception {
+ ListInstanceBootGroupsCmd cmd = new ListInstanceBootGroupsCmd();
+ setField(cmd, "instanceBootGroupService", instanceBootGroupService);
+ setField(cmd, "id", ENTITY_ID);
+ setField(cmd, "keyword", "web");
+ setField(cmd, "virtualMachineId", 500L);
+ setField(cmd, "instanceGroupId", 600L);
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ ListInstanceBootGroupsCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getId());
+ assertEquals("web", cmd.getKeyword());
+ assertEquals(Long.valueOf(500L), cmd.getVirtualMachineId());
+ assertEquals(Long.valueOf(600L), cmd.getInstanceGroupId());
+ }
+
+ @Test
+ public void testExecute() throws Exception {
+ ListInstanceBootGroupsCmd cmd = createCmd();
+
+ ListResponse mockListResponse = new ListResponse<>();
+ when(instanceBootGroupService.listInstanceBootGroups(cmd)).thenReturn(mockListResponse);
+
+ cmd.execute();
+
+ @SuppressWarnings("unchecked")
+ ListResponse response = (ListResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("listinstancebootgroupsresponse", response.getResponseName());
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/RebootInstanceBootGroupCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/RebootInstanceBootGroupCmdTest.java
new file mode 100644
index 000000000000..a278d4991a9f
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/RebootInstanceBootGroupCmdTest.java
@@ -0,0 +1,118 @@
+// 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.
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup;
+import org.junit.Test;
+
+import com.cloud.event.EventTypes;
+import com.cloud.utils.exception.CloudRuntimeException;
+
+public class RebootInstanceBootGroupCmdTest extends BaseBootGroupCmdTest {
+
+ private RebootInstanceBootGroupCmd createCmd() throws Exception {
+ RebootInstanceBootGroupCmd cmd = new RebootInstanceBootGroupCmd();
+ setField(cmd, "instanceBootGroupService", instanceBootGroupService);
+ setField(cmd, "id", ENTITY_ID);
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ RebootInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getId());
+ }
+
+ @Test
+ public void testGetEntityOwnerId() throws Exception {
+ RebootInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetApiResourceType() throws Exception {
+ RebootInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(ApiCommandResourceType.InstanceBootGroup, cmd.getApiResourceType());
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getApiResourceId());
+ }
+
+ @Test
+ public void testEventType() throws Exception {
+ RebootInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(EventTypes.EVENT_INSTANCE_BOOT_GROUP_REBOOT, cmd.getEventType());
+ }
+
+ @Test
+ public void testEventDescription() throws Exception {
+ RebootInstanceBootGroupCmd cmd = createCmd();
+ when(CallContext.current().getApiResourceUuid(ApiConstants.ID)).thenReturn("group-uuid");
+
+ assertEquals("Rebooting Instance Boot Group with ID: group-uuid", cmd.getEventDescription());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ RebootInstanceBootGroupCmd cmd = createCmd();
+
+ InstanceBootGroup group = mock(InstanceBootGroup.class);
+ when(group.getId()).thenReturn(ENTITY_ID);
+ InstanceBootGroupResponse mockResponse = new InstanceBootGroupResponse();
+
+ when(instanceBootGroupService.rebootInstanceBootGroup(cmd)).thenReturn(group);
+ when(instanceBootGroupService.createInstanceBootGroupResponse(ENTITY_ID)).thenReturn(mockResponse);
+
+ cmd.execute();
+
+ InstanceBootGroupResponse response = (InstanceBootGroupResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("rebootinstancebootgroupresponse", response.getResponseName());
+ verify(instanceBootGroupService).rebootInstanceBootGroup(cmd);
+ verify(instanceBootGroupService).createInstanceBootGroupResponse(ENTITY_ID);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsNull() throws Exception {
+ RebootInstanceBootGroupCmd cmd = createCmd();
+ when(instanceBootGroupService.rebootInstanceBootGroup(cmd)).thenReturn(null);
+ cmd.execute();
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testExecutePropagatesServiceException() throws Exception {
+ RebootInstanceBootGroupCmd cmd = createCmd();
+ when(instanceBootGroupService.rebootInstanceBootGroup(cmd)).thenThrow(new RuntimeException("db error"));
+ cmd.execute();
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteWrapsCloudRuntimeException() throws Exception {
+ RebootInstanceBootGroupCmd cmd = createCmd();
+ when(instanceBootGroupService.rebootInstanceBootGroup(cmd)).thenThrow(new CloudRuntimeException("cannot reboot"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/RemoveInstanceBootGroupMemberCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/RemoveInstanceBootGroupMemberCmdTest.java
new file mode 100644
index 000000000000..08ef6c5173cf
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/RemoveInstanceBootGroupMemberCmdTest.java
@@ -0,0 +1,85 @@
+// 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.
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.SuccessResponse;
+import org.junit.Test;
+
+public class RemoveInstanceBootGroupMemberCmdTest extends BaseBootGroupCmdTest {
+
+ private RemoveInstanceBootGroupMemberCmd createCmd() throws Exception {
+ RemoveInstanceBootGroupMemberCmd cmd = new RemoveInstanceBootGroupMemberCmd();
+ setField(cmd, "instanceBootGroupService", instanceBootGroupService);
+ setField(cmd, "id", ENTITY_ID);
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ RemoveInstanceBootGroupMemberCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getId());
+ }
+
+ @Test
+ public void testGetEntityOwnerId() throws Exception {
+ RemoveInstanceBootGroupMemberCmd cmd = createCmd();
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetApiResourceType() throws Exception {
+ RemoveInstanceBootGroupMemberCmd cmd = createCmd();
+ Long groupId = 101L;
+ when(instanceBootGroupService.getInstanceBootGroupIdForMember(ENTITY_ID)).thenReturn(groupId);
+ assertEquals(ApiCommandResourceType.InstanceBootGroup, cmd.getApiResourceType());
+ assertEquals(groupId, cmd.getApiResourceId());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ RemoveInstanceBootGroupMemberCmd cmd = createCmd();
+ when(instanceBootGroupService.removeInstanceBootGroupMember(cmd)).thenReturn(true);
+
+ cmd.execute();
+
+ SuccessResponse response = (SuccessResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("removeinstancebootgroupmemberresponse", response.getResponseName());
+ verify(instanceBootGroupService).removeInstanceBootGroupMember(cmd);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsFalse() throws Exception {
+ RemoveInstanceBootGroupMemberCmd cmd = createCmd();
+ when(instanceBootGroupService.removeInstanceBootGroupMember(cmd)).thenReturn(false);
+ cmd.execute();
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testExecutePropagatesServiceException() throws Exception {
+ RemoveInstanceBootGroupMemberCmd cmd = createCmd();
+ when(instanceBootGroupService.removeInstanceBootGroupMember(cmd)).thenThrow(new RuntimeException("conflict"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/StartInstanceBootGroupCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/StartInstanceBootGroupCmdTest.java
new file mode 100644
index 000000000000..76689a6205d1
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/StartInstanceBootGroupCmdTest.java
@@ -0,0 +1,118 @@
+// 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.
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup;
+import org.junit.Test;
+
+import com.cloud.event.EventTypes;
+import com.cloud.utils.exception.CloudRuntimeException;
+
+public class StartInstanceBootGroupCmdTest extends BaseBootGroupCmdTest {
+
+ private StartInstanceBootGroupCmd createCmd() throws Exception {
+ StartInstanceBootGroupCmd cmd = new StartInstanceBootGroupCmd();
+ setField(cmd, "instanceBootGroupService", instanceBootGroupService);
+ setField(cmd, "id", ENTITY_ID);
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ StartInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getId());
+ }
+
+ @Test
+ public void testGetEntityOwnerId() throws Exception {
+ StartInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetApiResourceType() throws Exception {
+ StartInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(ApiCommandResourceType.InstanceBootGroup, cmd.getApiResourceType());
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getApiResourceId());
+ }
+
+ @Test
+ public void testEventType() throws Exception {
+ StartInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(EventTypes.EVENT_INSTANCE_BOOT_GROUP_START, cmd.getEventType());
+ }
+
+ @Test
+ public void testEventDescription() throws Exception {
+ StartInstanceBootGroupCmd cmd = createCmd();
+ when(CallContext.current().getApiResourceUuid(ApiConstants.ID)).thenReturn("group-uuid");
+
+ assertEquals("Starting Instance Boot Group with ID: group-uuid", cmd.getEventDescription());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ StartInstanceBootGroupCmd cmd = createCmd();
+
+ InstanceBootGroup group = mock(InstanceBootGroup.class);
+ when(group.getId()).thenReturn(ENTITY_ID);
+ InstanceBootGroupResponse mockResponse = new InstanceBootGroupResponse();
+
+ when(instanceBootGroupService.startInstanceBootGroup(cmd)).thenReturn(group);
+ when(instanceBootGroupService.createInstanceBootGroupResponse(ENTITY_ID)).thenReturn(mockResponse);
+
+ cmd.execute();
+
+ InstanceBootGroupResponse response = (InstanceBootGroupResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("startinstancebootgroupresponse", response.getResponseName());
+ verify(instanceBootGroupService).startInstanceBootGroup(cmd);
+ verify(instanceBootGroupService).createInstanceBootGroupResponse(ENTITY_ID);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsNull() throws Exception {
+ StartInstanceBootGroupCmd cmd = createCmd();
+ when(instanceBootGroupService.startInstanceBootGroup(cmd)).thenReturn(null);
+ cmd.execute();
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testExecutePropagatesServiceException() throws Exception {
+ StartInstanceBootGroupCmd cmd = createCmd();
+ when(instanceBootGroupService.startInstanceBootGroup(cmd)).thenThrow(new RuntimeException("db error"));
+ cmd.execute();
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteWrapsCloudRuntimeException() throws Exception {
+ StartInstanceBootGroupCmd cmd = createCmd();
+ when(instanceBootGroupService.startInstanceBootGroup(cmd)).thenThrow(new CloudRuntimeException("cannot start"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/StopInstanceBootGroupCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/StopInstanceBootGroupCmdTest.java
new file mode 100644
index 000000000000..6c788f4a2b4d
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/StopInstanceBootGroupCmdTest.java
@@ -0,0 +1,110 @@
+// 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.
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup;
+import org.junit.Test;
+
+import com.cloud.event.EventTypes;
+
+public class StopInstanceBootGroupCmdTest extends BaseBootGroupCmdTest {
+
+ private StopInstanceBootGroupCmd createCmd() throws Exception {
+ StopInstanceBootGroupCmd cmd = new StopInstanceBootGroupCmd();
+ setField(cmd, "instanceBootGroupService", instanceBootGroupService);
+ setField(cmd, "id", ENTITY_ID);
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ StopInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getId());
+ }
+
+ @Test
+ public void testGetEntityOwnerId() throws Exception {
+ StopInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetApiResourceType() throws Exception {
+ StopInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(ApiCommandResourceType.InstanceBootGroup, cmd.getApiResourceType());
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getApiResourceId());
+ }
+
+ @Test
+ public void testEventType() throws Exception {
+ StopInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(EventTypes.EVENT_INSTANCE_BOOT_GROUP_STOP, cmd.getEventType());
+ }
+
+ @Test
+ public void testEventDescription() throws Exception {
+ StopInstanceBootGroupCmd cmd = createCmd();
+ when(CallContext.current().getApiResourceUuid(ApiConstants.ID)).thenReturn("group-uuid");
+
+ assertEquals("Stopping Instance Boot Group with ID: group-uuid", cmd.getEventDescription());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ StopInstanceBootGroupCmd cmd = createCmd();
+
+ InstanceBootGroup group = mock(InstanceBootGroup.class);
+ when(group.getId()).thenReturn(ENTITY_ID);
+ InstanceBootGroupResponse mockResponse = new InstanceBootGroupResponse();
+
+ when(instanceBootGroupService.stopInstanceBootGroup(cmd)).thenReturn(group);
+ when(instanceBootGroupService.createInstanceBootGroupResponse(ENTITY_ID)).thenReturn(mockResponse);
+
+ cmd.execute();
+
+ InstanceBootGroupResponse response = (InstanceBootGroupResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("stopinstancebootgroupresponse", response.getResponseName());
+ verify(instanceBootGroupService).stopInstanceBootGroup(cmd);
+ verify(instanceBootGroupService).createInstanceBootGroupResponse(ENTITY_ID);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsNull() throws Exception {
+ StopInstanceBootGroupCmd cmd = createCmd();
+ when(instanceBootGroupService.stopInstanceBootGroup(cmd)).thenReturn(null);
+ cmd.execute();
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testExecutePropagatesServiceException() throws Exception {
+ StopInstanceBootGroupCmd cmd = createCmd();
+ when(instanceBootGroupService.stopInstanceBootGroup(cmd)).thenThrow(new RuntimeException("db error"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupCmdTest.java
new file mode 100644
index 000000000000..f64b1298b2e2
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupCmdTest.java
@@ -0,0 +1,104 @@
+// 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.
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup;
+import org.junit.Test;
+
+public class UpdateInstanceBootGroupCmdTest extends BaseBootGroupCmdTest {
+
+ private UpdateInstanceBootGroupCmd createCmd() throws Exception {
+ UpdateInstanceBootGroupCmd cmd = new UpdateInstanceBootGroupCmd();
+ setField(cmd, "instanceBootGroupService", instanceBootGroupService);
+ setField(cmd, "id", ENTITY_ID);
+ setField(cmd, "name", "web-tier-renamed");
+ setField(cmd, "description", "updated description");
+ setField(cmd, "readinessAttemptTimeoutSeconds", 180L);
+ setField(cmd, "readinessMaxRetryAttempts", 5L);
+ setField(cmd, "readinessRebootOnRetry", false);
+ setField(cmd, "readinessInitialDelaySeconds", 30L);
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ UpdateInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getId());
+ assertEquals("web-tier-renamed", cmd.getName());
+ assertEquals("updated description", cmd.getDescription());
+ assertEquals(Long.valueOf(180L), cmd.getReadinessAttemptTimeoutSeconds());
+ assertEquals(Long.valueOf(5L), cmd.getReadinessMaxRetryAttempts());
+ assertEquals(Boolean.FALSE, cmd.getReadinessRebootOnRetry());
+ assertEquals(Long.valueOf(30L), cmd.getReadinessInitialDelaySeconds());
+ }
+
+ @Test
+ public void testGetEntityOwnerId() throws Exception {
+ UpdateInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetApiResourceType() throws Exception {
+ UpdateInstanceBootGroupCmd cmd = createCmd();
+ assertEquals(ApiCommandResourceType.InstanceBootGroup, cmd.getApiResourceType());
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getApiResourceId());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ UpdateInstanceBootGroupCmd cmd = createCmd();
+
+ InstanceBootGroup group = mock(InstanceBootGroup.class);
+ when(group.getId()).thenReturn(ENTITY_ID);
+ InstanceBootGroupResponse mockResponse = new InstanceBootGroupResponse();
+
+ when(instanceBootGroupService.updateInstanceBootGroup(cmd)).thenReturn(group);
+ when(instanceBootGroupService.createInstanceBootGroupResponse(ENTITY_ID)).thenReturn(mockResponse);
+
+ cmd.execute();
+
+ InstanceBootGroupResponse response = (InstanceBootGroupResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("updateinstancebootgroupresponse", response.getResponseName());
+ verify(instanceBootGroupService).updateInstanceBootGroup(cmd);
+ verify(instanceBootGroupService).createInstanceBootGroupResponse(ENTITY_ID);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsNull() throws Exception {
+ UpdateInstanceBootGroupCmd cmd = createCmd();
+ when(instanceBootGroupService.updateInstanceBootGroup(cmd)).thenReturn(null);
+ cmd.execute();
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testExecutePropagatesServiceException() throws Exception {
+ UpdateInstanceBootGroupCmd cmd = createCmd();
+ when(instanceBootGroupService.updateInstanceBootGroup(cmd)).thenThrow(new RuntimeException("db error"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupMemberCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupMemberCmdTest.java
new file mode 100644
index 000000000000..219ae3ce67fb
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupMemberCmdTest.java
@@ -0,0 +1,95 @@
+// 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.
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse;
+import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember;
+import org.junit.Test;
+
+public class UpdateInstanceBootGroupMemberCmdTest extends BaseBootGroupCmdTest {
+
+ private UpdateInstanceBootGroupMemberCmd createCmd() throws Exception {
+ UpdateInstanceBootGroupMemberCmd cmd = new UpdateInstanceBootGroupMemberCmd();
+ setField(cmd, "instanceBootGroupService", instanceBootGroupService);
+ setField(cmd, "id", ENTITY_ID);
+ setField(cmd, "order", 3);
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ UpdateInstanceBootGroupMemberCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getId());
+ assertEquals(3, cmd.getOrder());
+ }
+
+ @Test
+ public void testGetEntityOwnerId() throws Exception {
+ UpdateInstanceBootGroupMemberCmd cmd = createCmd();
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetApiResourceType() throws Exception {
+ UpdateInstanceBootGroupMemberCmd cmd = createCmd();
+ Long groupId = 101L;
+ when(instanceBootGroupService.getInstanceBootGroupIdForMember(ENTITY_ID)).thenReturn(groupId);
+ assertEquals(ApiCommandResourceType.InstanceBootGroup, cmd.getApiResourceType());
+ assertEquals(groupId, cmd.getApiResourceId());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ UpdateInstanceBootGroupMemberCmd cmd = createCmd();
+
+ InstanceBootGroupMember member = mock(InstanceBootGroupMember.class);
+ InstanceBootGroupMemberResponse mockResponse = new InstanceBootGroupMemberResponse();
+
+ when(instanceBootGroupService.updateInstanceBootGroupMember(cmd)).thenReturn(member);
+ when(instanceBootGroupService.createInstanceBootGroupMemberResponse(member)).thenReturn(mockResponse);
+
+ cmd.execute();
+
+ InstanceBootGroupMemberResponse response = (InstanceBootGroupMemberResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("updateinstancebootgroupmemberresponse", response.getResponseName());
+ verify(instanceBootGroupService).updateInstanceBootGroupMember(cmd);
+ verify(instanceBootGroupService).createInstanceBootGroupMemberResponse(member);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsNull() throws Exception {
+ UpdateInstanceBootGroupMemberCmd cmd = createCmd();
+ when(instanceBootGroupService.updateInstanceBootGroupMember(cmd)).thenReturn(null);
+ cmd.execute();
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testExecutePropagatesServiceException() throws Exception {
+ UpdateInstanceBootGroupMemberCmd cmd = createCmd();
+ when(instanceBootGroupService.updateInstanceBootGroupMember(cmd)).thenThrow(new RuntimeException("conflict"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupReadinessRuleCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupReadinessRuleCmdTest.java
new file mode 100644
index 000000000000..b4f0e3608287
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupReadinessRuleCmdTest.java
@@ -0,0 +1,139 @@
+// 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.
+package org.apache.cloudstack.api.command.user.bootgroup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse;
+import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule;
+import org.junit.Test;
+
+public class UpdateInstanceBootGroupReadinessRuleCmdTest extends BaseBootGroupCmdTest {
+
+ private UpdateInstanceBootGroupReadinessRuleCmd createCmd() throws Exception {
+ UpdateInstanceBootGroupReadinessRuleCmd cmd = new UpdateInstanceBootGroupReadinessRuleCmd();
+ setField(cmd, "instanceBootGroupService", instanceBootGroupService);
+ setField(cmd, "id", ENTITY_ID);
+ setField(cmd, "name", "renamed-rule");
+ setField(cmd, "enabled", false);
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getId());
+ assertEquals("renamed-rule", cmd.getName());
+ assertEquals(Boolean.FALSE, cmd.getEnabled());
+ }
+
+ @Test
+ public void testGetEnabledNullWhenNotSet() throws Exception {
+ UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ setField(cmd, "enabled", null);
+ assertNull(cmd.getEnabled());
+ }
+
+ @Test
+ public void testGetEntityOwnerId() throws Exception {
+ UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetApiResourceType() throws Exception {
+ UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ assertEquals(ApiCommandResourceType.InstanceBootGroupReadinessRule, cmd.getApiResourceType());
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getApiResourceId());
+ }
+
+ @Test
+ public void testGetDetailsNull() throws Exception {
+ UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ setField(cmd, "details", null);
+ assertNull(cmd.getDetails());
+ }
+
+ @Test
+ public void testGetDetailsEmptyMap() throws Exception {
+ UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ setField(cmd, "details", new HashMap<>());
+ assertNull(cmd.getDetails());
+ }
+
+ @Test
+ public void testGetDetailsPopulatedMap() throws Exception {
+ UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+
+ Map innerMap = new HashMap<>();
+ innerMap.put("port", "9090");
+ innerMap.put("protocol", "udp");
+
+ Map> outerMap = new HashMap<>();
+ outerMap.put("0", innerMap);
+
+ setField(cmd, "details", outerMap);
+
+ Map details = cmd.getDetails();
+ assertNotNull(details);
+ assertEquals("9090", details.get("port"));
+ assertEquals("udp", details.get("protocol"));
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+
+ InstanceBootGroupReadinessRule rule = mock(InstanceBootGroupReadinessRule.class);
+ InstanceBootGroupReadinessRuleResponse mockResponse = new InstanceBootGroupReadinessRuleResponse();
+
+ when(instanceBootGroupService.updateInstanceBootGroupReadinessRule(cmd)).thenReturn(rule);
+ when(instanceBootGroupService.createInstanceBootGroupReadinessRuleResponse(rule)).thenReturn(mockResponse);
+
+ cmd.execute();
+
+ InstanceBootGroupReadinessRuleResponse response = (InstanceBootGroupReadinessRuleResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("updateinstancebootgroupreadinessruleresponse", response.getResponseName());
+ verify(instanceBootGroupService).updateInstanceBootGroupReadinessRule(cmd);
+ verify(instanceBootGroupService).createInstanceBootGroupReadinessRuleResponse(rule);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsNull() throws Exception {
+ UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ when(instanceBootGroupService.updateInstanceBootGroupReadinessRule(cmd)).thenReturn(null);
+ cmd.execute();
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testExecutePropagatesServiceException() throws Exception {
+ UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd();
+ when(instanceBootGroupService.updateInstanceBootGroupReadinessRule(cmd)).thenThrow(new RuntimeException("db error"));
+ cmd.execute();
+ }
+}
diff --git a/core/src/main/java/com/cloud/agent/api/CheckConvertInstanceCommand.java b/core/src/main/java/com/cloud/agent/api/CheckConvertInstanceCommand.java
index c46fb697a3c1..9bd5bf168a3d 100644
--- a/core/src/main/java/com/cloud/agent/api/CheckConvertInstanceCommand.java
+++ b/core/src/main/java/com/cloud/agent/api/CheckConvertInstanceCommand.java
@@ -19,6 +19,8 @@
public class CheckConvertInstanceCommand extends Command {
boolean checkWindowsGuestConversionSupport = false;
boolean useVddk = false;
+ boolean checkVddkRbdDirectImportSupport = false;
+ boolean checkVddkInPlaceFinalizationSupport = false;
String vddkLibDir;
public CheckConvertInstanceCommand() {
@@ -50,6 +52,22 @@ public void setUseVddk(boolean useVddk) {
this.useVddk = useVddk;
}
+ public boolean isCheckVddkRbdDirectImportSupport() {
+ return checkVddkRbdDirectImportSupport;
+ }
+
+ public void setCheckVddkRbdDirectImportSupport(boolean checkVddkRbdDirectImportSupport) {
+ this.checkVddkRbdDirectImportSupport = checkVddkRbdDirectImportSupport;
+ }
+
+ public boolean isCheckVddkInPlaceFinalizationSupport() {
+ return checkVddkInPlaceFinalizationSupport;
+ }
+
+ public void setCheckVddkInPlaceFinalizationSupport(boolean checkVddkInPlaceFinalizationSupport) {
+ this.checkVddkInPlaceFinalizationSupport = checkVddkInPlaceFinalizationSupport;
+ }
+
public String getVddkLibDir() {
return vddkLibDir;
}
diff --git a/core/src/main/java/com/cloud/agent/api/CheckGuestAgentLivenessAnswer.java b/core/src/main/java/com/cloud/agent/api/CheckGuestAgentLivenessAnswer.java
new file mode 100644
index 000000000000..4019977de810
--- /dev/null
+++ b/core/src/main/java/com/cloud/agent/api/CheckGuestAgentLivenessAnswer.java
@@ -0,0 +1,35 @@
+//
+// 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.
+//
+
+package com.cloud.agent.api;
+
+public class CheckGuestAgentLivenessAnswer extends Answer {
+
+ public CheckGuestAgentLivenessAnswer() {
+ super();
+ }
+
+ public CheckGuestAgentLivenessAnswer(CheckGuestAgentLivenessCommand command, boolean alive, String details) {
+ super(command, alive, details);
+ }
+
+ public CheckGuestAgentLivenessAnswer(CheckGuestAgentLivenessCommand command, Exception e) {
+ super(command, e);
+ }
+}
diff --git a/core/src/main/java/com/cloud/agent/api/CheckGuestAgentLivenessCommand.java b/core/src/main/java/com/cloud/agent/api/CheckGuestAgentLivenessCommand.java
new file mode 100644
index 000000000000..b608beb156cd
--- /dev/null
+++ b/core/src/main/java/com/cloud/agent/api/CheckGuestAgentLivenessCommand.java
@@ -0,0 +1,42 @@
+//
+// 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.
+//
+
+package com.cloud.agent.api;
+
+public class CheckGuestAgentLivenessCommand extends Command {
+
+ private String vmName;
+
+ public CheckGuestAgentLivenessCommand(String vmName) {
+ this.vmName = vmName;
+ }
+
+ public String getVmName() {
+ return vmName;
+ }
+
+ public void setVmName(String vmName) {
+ this.vmName = vmName;
+ }
+
+ @Override
+ public boolean executeInSequence() {
+ return false;
+ }
+}
diff --git a/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java b/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java
index 38e0dca7736c..8a8e194ee220 100644
--- a/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java
+++ b/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java
@@ -18,8 +18,11 @@
import com.cloud.agent.api.to.DataStoreTO;
import com.cloud.agent.api.to.RemoteInstanceTO;
+import com.cloud.agent.api.to.VmwareVddkSourceDiskTO;
import com.cloud.hypervisor.Hypervisor;
+import java.util.List;
+
public class ConvertInstanceCommand extends Command {
private RemoteInstanceTO sourceInstance;
@@ -35,6 +38,8 @@ public class ConvertInstanceCommand extends Command {
private String vddkLibDir;
private String vddkTransports;
private String vddkThumbprint;
+ private List vmwareVddkSourceDisks;
+ private boolean windowsGuest;
public ConvertInstanceCommand() {
}
@@ -126,6 +131,22 @@ public void setVddkThumbprint(String vddkThumbprint) {
this.vddkThumbprint = vddkThumbprint;
}
+ public List getVmwareVddkSourceDisks() {
+ return vmwareVddkSourceDisks;
+ }
+
+ public void setVmwareVddkSourceDisks(List vmwareVddkSourceDisks) {
+ this.vmwareVddkSourceDisks = vmwareVddkSourceDisks;
+ }
+
+ public boolean isWindowsGuest() {
+ return windowsGuest;
+ }
+
+ public void setWindowsGuest(boolean windowsGuest) {
+ this.windowsGuest = windowsGuest;
+ }
+
@Override
public boolean executeInSequence() {
return false;
diff --git a/core/src/main/java/com/cloud/agent/api/ImportConvertedInstanceCommand.java b/core/src/main/java/com/cloud/agent/api/ImportConvertedInstanceCommand.java
index eadfa6556f86..e5b970814fab 100644
--- a/core/src/main/java/com/cloud/agent/api/ImportConvertedInstanceCommand.java
+++ b/core/src/main/java/com/cloud/agent/api/ImportConvertedInstanceCommand.java
@@ -18,6 +18,7 @@
import com.cloud.agent.api.to.DataStoreTO;
import com.cloud.agent.api.to.RemoteInstanceTO;
+import com.cloud.storage.Storage;
import java.util.List;
@@ -25,6 +26,7 @@ public class ImportConvertedInstanceCommand extends Command {
private RemoteInstanceTO sourceInstance;
private List destinationStoragePools;
+ private List destinationStoragePoolTypes;
private DataStoreTO conversionTemporaryLocation;
private String temporaryConvertUuid;
private boolean forceConvertToPool;
@@ -34,15 +36,24 @@ public ImportConvertedInstanceCommand() {
public ImportConvertedInstanceCommand(RemoteInstanceTO sourceInstance,
List destinationStoragePools,
+ List destinationStoragePoolTypes,
DataStoreTO conversionTemporaryLocation, String temporaryConvertUuid,
boolean forceConvertToPool) {
this.sourceInstance = sourceInstance;
this.destinationStoragePools = destinationStoragePools;
+ this.destinationStoragePoolTypes = destinationStoragePoolTypes;
this.conversionTemporaryLocation = conversionTemporaryLocation;
this.temporaryConvertUuid = temporaryConvertUuid;
this.forceConvertToPool = forceConvertToPool;
}
+ public ImportConvertedInstanceCommand(RemoteInstanceTO sourceInstance,
+ List destinationStoragePools,
+ DataStoreTO conversionTemporaryLocation, String temporaryConvertUuid,
+ boolean forceConvertToPool) {
+ this(sourceInstance, destinationStoragePools, null, conversionTemporaryLocation, temporaryConvertUuid, forceConvertToPool);
+ }
+
public RemoteInstanceTO getSourceInstance() {
return sourceInstance;
}
@@ -51,6 +62,10 @@ public List getDestinationStoragePools() {
return destinationStoragePools;
}
+ public List getDestinationStoragePoolTypes() {
+ return destinationStoragePoolTypes;
+ }
+
public DataStoreTO getConversionTemporaryLocation() {
return conversionTemporaryLocation;
}
diff --git a/core/src/main/java/com/cloud/agent/api/VmwareCbtCleanupCommand.java b/core/src/main/java/com/cloud/agent/api/VmwareCbtCleanupCommand.java
new file mode 100644
index 000000000000..65ae402e1140
--- /dev/null
+++ b/core/src/main/java/com/cloud/agent/api/VmwareCbtCleanupCommand.java
@@ -0,0 +1,100 @@
+// 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.
+package com.cloud.agent.api;
+
+import java.util.List;
+
+import com.cloud.agent.api.to.VmwareCbtDiskTO;
+import com.cloud.agent.api.to.VmwareCbtTargetStorageType;
+import com.cloud.storage.Storage;
+
+public class VmwareCbtCleanupCommand extends Command {
+
+ private String migrationUuid;
+ private List disks;
+ private boolean removeTemporarySnapshots;
+ private boolean removeNbdExports;
+ private boolean removePartialTargetDisks;
+ private Storage.StoragePoolType destinationStoragePoolType;
+ private String destinationStoragePoolUuid;
+ private VmwareCbtTargetStorageType targetStorageType;
+
+ public VmwareCbtCleanupCommand() {
+ }
+
+ public VmwareCbtCleanupCommand(String migrationUuid, List disks, boolean removeTemporarySnapshots,
+ boolean removeNbdExports, boolean removePartialTargetDisks) {
+ this.migrationUuid = migrationUuid;
+ this.disks = disks;
+ this.removeTemporarySnapshots = removeTemporarySnapshots;
+ this.removeNbdExports = removeNbdExports;
+ this.removePartialTargetDisks = removePartialTargetDisks;
+ }
+
+ public String getMigrationUuid() {
+ return migrationUuid;
+ }
+
+ public List getDisks() {
+ return disks;
+ }
+
+ public boolean getRemoveTemporarySnapshots() {
+ return removeTemporarySnapshots;
+ }
+
+ public boolean getRemoveNbdExports() {
+ return removeNbdExports;
+ }
+
+ public boolean getRemovePartialTargetDisks() {
+ return removePartialTargetDisks;
+ }
+
+ public Storage.StoragePoolType getDestinationStoragePoolType() {
+ return destinationStoragePoolType;
+ }
+
+ public void setDestinationStoragePoolType(Storage.StoragePoolType destinationStoragePoolType) {
+ this.destinationStoragePoolType = destinationStoragePoolType;
+ }
+
+ public String getDestinationStoragePoolUuid() {
+ return destinationStoragePoolUuid;
+ }
+
+ public void setDestinationStoragePoolUuid(String destinationStoragePoolUuid) {
+ this.destinationStoragePoolUuid = destinationStoragePoolUuid;
+ }
+
+ public VmwareCbtTargetStorageType getTargetStorageType() {
+ return targetStorageType;
+ }
+
+ public void setTargetStorageType(VmwareCbtTargetStorageType targetStorageType) {
+ this.targetStorageType = targetStorageType;
+ }
+
+ @Override
+ public boolean executeInSequence() {
+ // Cancellation must be able to interrupt a long-running prepare/sync command on the
+ // same host. If cleanup is queued behind that command, the VDDK reader keeps the source
+ // snapshot open until the entire copy finishes and the cleanup request can time out
+ // before it ever reaches the agent.
+ return false;
+ }
+}
diff --git a/core/src/main/java/com/cloud/agent/api/VmwareCbtCutoverCommand.java b/core/src/main/java/com/cloud/agent/api/VmwareCbtCutoverCommand.java
new file mode 100644
index 000000000000..24d82c434afe
--- /dev/null
+++ b/core/src/main/java/com/cloud/agent/api/VmwareCbtCutoverCommand.java
@@ -0,0 +1,133 @@
+// 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.
+package com.cloud.agent.api;
+
+import java.util.List;
+
+import com.cloud.agent.api.to.RemoteInstanceTO;
+import com.cloud.agent.api.to.VmwareCbtDiskTO;
+import com.cloud.agent.api.to.VmwareCbtTargetStorageType;
+import com.cloud.storage.Storage;
+
+public class VmwareCbtCutoverCommand extends Command {
+
+ private String migrationUuid;
+ private RemoteInstanceTO sourceInstance;
+ private List disks;
+ private int finalCycleNumber;
+ private boolean runVirtV2vFinalization;
+ private boolean allowNonInPlaceFinalization;
+ private Storage.StoragePoolType destinationStoragePoolType;
+ private String destinationStoragePoolUuid;
+ private VmwareCbtTargetStorageType targetStorageType;
+ private String vddkLibDir;
+ private String vddkTransports;
+ private String vddkThumbprint;
+
+ public VmwareCbtCutoverCommand() {
+ }
+
+ public VmwareCbtCutoverCommand(String migrationUuid, RemoteInstanceTO sourceInstance, List disks,
+ int finalCycleNumber, boolean runVirtV2vFinalization) {
+ this.migrationUuid = migrationUuid;
+ this.sourceInstance = sourceInstance;
+ this.disks = disks;
+ this.finalCycleNumber = finalCycleNumber;
+ this.runVirtV2vFinalization = runVirtV2vFinalization;
+ }
+
+ public String getMigrationUuid() {
+ return migrationUuid;
+ }
+
+ public RemoteInstanceTO getSourceInstance() {
+ return sourceInstance;
+ }
+
+ public List getDisks() {
+ return disks;
+ }
+
+ public int getFinalCycleNumber() {
+ return finalCycleNumber;
+ }
+
+ public boolean getRunVirtV2vFinalization() {
+ return runVirtV2vFinalization;
+ }
+
+ public boolean getAllowNonInPlaceFinalization() {
+ return allowNonInPlaceFinalization;
+ }
+
+ public void setAllowNonInPlaceFinalization(boolean allowNonInPlaceFinalization) {
+ this.allowNonInPlaceFinalization = allowNonInPlaceFinalization;
+ }
+
+ public Storage.StoragePoolType getDestinationStoragePoolType() {
+ return destinationStoragePoolType;
+ }
+
+ public void setDestinationStoragePoolType(Storage.StoragePoolType destinationStoragePoolType) {
+ this.destinationStoragePoolType = destinationStoragePoolType;
+ }
+
+ public String getDestinationStoragePoolUuid() {
+ return destinationStoragePoolUuid;
+ }
+
+ public void setDestinationStoragePoolUuid(String destinationStoragePoolUuid) {
+ this.destinationStoragePoolUuid = destinationStoragePoolUuid;
+ }
+
+ public VmwareCbtTargetStorageType getTargetStorageType() {
+ return targetStorageType;
+ }
+
+ public void setTargetStorageType(VmwareCbtTargetStorageType targetStorageType) {
+ this.targetStorageType = targetStorageType;
+ }
+
+ public String getVddkLibDir() {
+ return vddkLibDir;
+ }
+
+ public void setVddkLibDir(String vddkLibDir) {
+ this.vddkLibDir = vddkLibDir;
+ }
+
+ public String getVddkTransports() {
+ return vddkTransports;
+ }
+
+ public void setVddkTransports(String vddkTransports) {
+ this.vddkTransports = vddkTransports;
+ }
+
+ public String getVddkThumbprint() {
+ return vddkThumbprint;
+ }
+
+ public void setVddkThumbprint(String vddkThumbprint) {
+ this.vddkThumbprint = vddkThumbprint;
+ }
+
+ @Override
+ public boolean executeInSequence() {
+ return true;
+ }
+}
diff --git a/core/src/main/java/com/cloud/agent/api/VmwareCbtMigrationAnswer.java b/core/src/main/java/com/cloud/agent/api/VmwareCbtMigrationAnswer.java
new file mode 100644
index 000000000000..2a0435639418
--- /dev/null
+++ b/core/src/main/java/com/cloud/agent/api/VmwareCbtMigrationAnswer.java
@@ -0,0 +1,82 @@
+// 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.
+package com.cloud.agent.api;
+
+import java.util.List;
+
+import com.cloud.agent.api.to.VmwareCbtDiskSyncResultTO;
+
+public class VmwareCbtMigrationAnswer extends Answer {
+
+ private String migrationUuid;
+ private int cycleNumber;
+ private long changedBytes;
+ private long dirtyRateBytesPerSecond;
+ private long durationSeconds;
+ private boolean readyForCutover;
+ private List diskResults;
+
+ public VmwareCbtMigrationAnswer() {
+ super();
+ }
+
+ public VmwareCbtMigrationAnswer(Command command, boolean result, String details, String migrationUuid) {
+ super(command, result, details);
+ this.migrationUuid = migrationUuid;
+ }
+
+ public VmwareCbtMigrationAnswer(Command command, boolean result, String details, String migrationUuid, int cycleNumber,
+ long changedBytes, long dirtyRateBytesPerSecond, long durationSeconds,
+ boolean readyForCutover, List diskResults) {
+ super(command, result, details);
+ this.migrationUuid = migrationUuid;
+ this.cycleNumber = cycleNumber;
+ this.changedBytes = changedBytes;
+ this.dirtyRateBytesPerSecond = dirtyRateBytesPerSecond;
+ this.durationSeconds = durationSeconds;
+ this.readyForCutover = readyForCutover;
+ this.diskResults = diskResults;
+ }
+
+ public String getMigrationUuid() {
+ return migrationUuid;
+ }
+
+ public int getCycleNumber() {
+ return cycleNumber;
+ }
+
+ public long getChangedBytes() {
+ return changedBytes;
+ }
+
+ public long getDirtyRateBytesPerSecond() {
+ return dirtyRateBytesPerSecond;
+ }
+
+ public long getDurationSeconds() {
+ return durationSeconds;
+ }
+
+ public boolean getReadyForCutover() {
+ return readyForCutover;
+ }
+
+ public List getDiskResults() {
+ return diskResults;
+ }
+}
diff --git a/core/src/main/java/com/cloud/agent/api/VmwareCbtPrepareCommand.java b/core/src/main/java/com/cloud/agent/api/VmwareCbtPrepareCommand.java
new file mode 100644
index 000000000000..0791d3b06a6f
--- /dev/null
+++ b/core/src/main/java/com/cloud/agent/api/VmwareCbtPrepareCommand.java
@@ -0,0 +1,110 @@
+// 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.
+package com.cloud.agent.api;
+
+import java.util.List;
+
+import com.cloud.agent.api.to.RemoteInstanceTO;
+import com.cloud.agent.api.to.VmwareCbtDiskTO;
+import com.cloud.agent.api.to.VmwareCbtTargetStorageType;
+import com.cloud.storage.Storage;
+
+public class VmwareCbtPrepareCommand extends Command {
+
+ private String migrationUuid;
+ private RemoteInstanceTO sourceInstance;
+ private List disks;
+ private Storage.StoragePoolType destinationStoragePoolType;
+ private String destinationStoragePoolUuid;
+ private VmwareCbtTargetStorageType targetStorageType;
+ private String baselineSnapshotMor;
+ private String vddkLibDir;
+ private String vddkTransports;
+ private String vddkThumbprint;
+
+ public VmwareCbtPrepareCommand() {
+ }
+
+ public VmwareCbtPrepareCommand(String migrationUuid, RemoteInstanceTO sourceInstance, List disks,
+ Storage.StoragePoolType destinationStoragePoolType, String destinationStoragePoolUuid,
+ VmwareCbtTargetStorageType targetStorageType, String baselineSnapshotMor) {
+ this.migrationUuid = migrationUuid;
+ this.sourceInstance = sourceInstance;
+ this.disks = disks;
+ this.destinationStoragePoolType = destinationStoragePoolType;
+ this.destinationStoragePoolUuid = destinationStoragePoolUuid;
+ this.targetStorageType = targetStorageType;
+ this.baselineSnapshotMor = baselineSnapshotMor;
+ }
+
+ public String getMigrationUuid() {
+ return migrationUuid;
+ }
+
+ public RemoteInstanceTO getSourceInstance() {
+ return sourceInstance;
+ }
+
+ public List getDisks() {
+ return disks;
+ }
+
+ public Storage.StoragePoolType getDestinationStoragePoolType() {
+ return destinationStoragePoolType;
+ }
+
+ public String getDestinationStoragePoolUuid() {
+ return destinationStoragePoolUuid;
+ }
+
+ public VmwareCbtTargetStorageType getTargetStorageType() {
+ return targetStorageType;
+ }
+
+ public String getBaselineSnapshotMor() {
+ return baselineSnapshotMor;
+ }
+
+ public String getVddkLibDir() {
+ return vddkLibDir;
+ }
+
+ public void setVddkLibDir(String vddkLibDir) {
+ this.vddkLibDir = vddkLibDir;
+ }
+
+ public String getVddkTransports() {
+ return vddkTransports;
+ }
+
+ public void setVddkTransports(String vddkTransports) {
+ this.vddkTransports = vddkTransports;
+ }
+
+ public String getVddkThumbprint() {
+ return vddkThumbprint;
+ }
+
+ public void setVddkThumbprint(String vddkThumbprint) {
+ this.vddkThumbprint = vddkThumbprint;
+ }
+
+ @Override
+ public boolean executeInSequence() {
+ return true;
+ }
+}
diff --git a/core/src/main/java/com/cloud/agent/api/VmwareCbtRbdProbeCommand.java b/core/src/main/java/com/cloud/agent/api/VmwareCbtRbdProbeCommand.java
new file mode 100644
index 000000000000..73b532bfff71
--- /dev/null
+++ b/core/src/main/java/com/cloud/agent/api/VmwareCbtRbdProbeCommand.java
@@ -0,0 +1,63 @@
+// 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.
+package com.cloud.agent.api;
+
+import com.cloud.agent.api.to.VmwareCbtTargetStorageType;
+import com.cloud.storage.Storage;
+
+public class VmwareCbtRbdProbeCommand extends Command {
+
+ private Storage.StoragePoolType destinationStoragePoolType;
+ private String destinationStoragePoolUuid;
+ private String probeImageName;
+ private VmwareCbtTargetStorageType targetStorageType;
+
+ public VmwareCbtRbdProbeCommand() {
+ }
+
+ public VmwareCbtRbdProbeCommand(Storage.StoragePoolType destinationStoragePoolType,
+ String destinationStoragePoolUuid, String probeImageName) {
+ this.destinationStoragePoolType = destinationStoragePoolType;
+ this.destinationStoragePoolUuid = destinationStoragePoolUuid;
+ this.probeImageName = probeImageName;
+ }
+
+ public Storage.StoragePoolType getDestinationStoragePoolType() {
+ return destinationStoragePoolType;
+ }
+
+ public String getDestinationStoragePoolUuid() {
+ return destinationStoragePoolUuid;
+ }
+
+ public String getProbeImageName() {
+ return probeImageName;
+ }
+
+ public VmwareCbtTargetStorageType getTargetStorageType() {
+ return targetStorageType;
+ }
+
+ public void setTargetStorageType(VmwareCbtTargetStorageType targetStorageType) {
+ this.targetStorageType = targetStorageType;
+ }
+
+ @Override
+ public boolean executeInSequence() {
+ return true;
+ }
+}
diff --git a/core/src/main/java/com/cloud/agent/api/VmwareCbtSyncCommand.java b/core/src/main/java/com/cloud/agent/api/VmwareCbtSyncCommand.java
new file mode 100644
index 000000000000..a58c0029c43f
--- /dev/null
+++ b/core/src/main/java/com/cloud/agent/api/VmwareCbtSyncCommand.java
@@ -0,0 +1,138 @@
+// 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.
+package com.cloud.agent.api;
+
+import java.util.List;
+
+import com.cloud.agent.api.to.RemoteInstanceTO;
+import com.cloud.agent.api.to.VmwareCbtChangedBlockRangeTO;
+import com.cloud.agent.api.to.VmwareCbtDiskTO;
+import com.cloud.agent.api.to.VmwareCbtTargetStorageType;
+import com.cloud.storage.Storage;
+
+public class VmwareCbtSyncCommand extends Command {
+
+ private String migrationUuid;
+ private RemoteInstanceTO sourceInstance;
+ private List disks;
+ private List changedBlocks;
+ private int cycleNumber;
+ private String snapshotMor;
+ private boolean finalSync;
+ private Storage.StoragePoolType destinationStoragePoolType;
+ private String destinationStoragePoolUuid;
+ private VmwareCbtTargetStorageType targetStorageType;
+ private String vddkLibDir;
+ private String vddkTransports;
+ private String vddkThumbprint;
+
+ public VmwareCbtSyncCommand() {
+ }
+
+ public VmwareCbtSyncCommand(String migrationUuid, RemoteInstanceTO sourceInstance, List disks,
+ List changedBlocks, int cycleNumber, String snapshotMor,
+ boolean finalSync) {
+ this.migrationUuid = migrationUuid;
+ this.sourceInstance = sourceInstance;
+ this.disks = disks;
+ this.changedBlocks = changedBlocks;
+ this.cycleNumber = cycleNumber;
+ this.snapshotMor = snapshotMor;
+ this.finalSync = finalSync;
+ }
+
+ public String getMigrationUuid() {
+ return migrationUuid;
+ }
+
+ public RemoteInstanceTO getSourceInstance() {
+ return sourceInstance;
+ }
+
+ public List getDisks() {
+ return disks;
+ }
+
+ public List getChangedBlocks() {
+ return changedBlocks;
+ }
+
+ public int getCycleNumber() {
+ return cycleNumber;
+ }
+
+ public String getSnapshotMor() {
+ return snapshotMor;
+ }
+
+ public boolean getFinalSync() {
+ return finalSync;
+ }
+
+ public Storage.StoragePoolType getDestinationStoragePoolType() {
+ return destinationStoragePoolType;
+ }
+
+ public void setDestinationStoragePoolType(Storage.StoragePoolType destinationStoragePoolType) {
+ this.destinationStoragePoolType = destinationStoragePoolType;
+ }
+
+ public String getDestinationStoragePoolUuid() {
+ return destinationStoragePoolUuid;
+ }
+
+ public void setDestinationStoragePoolUuid(String destinationStoragePoolUuid) {
+ this.destinationStoragePoolUuid = destinationStoragePoolUuid;
+ }
+
+ public VmwareCbtTargetStorageType getTargetStorageType() {
+ return targetStorageType;
+ }
+
+ public void setTargetStorageType(VmwareCbtTargetStorageType targetStorageType) {
+ this.targetStorageType = targetStorageType;
+ }
+
+ public String getVddkLibDir() {
+ return vddkLibDir;
+ }
+
+ public void setVddkLibDir(String vddkLibDir) {
+ this.vddkLibDir = vddkLibDir;
+ }
+
+ public String getVddkTransports() {
+ return vddkTransports;
+ }
+
+ public void setVddkTransports(String vddkTransports) {
+ this.vddkTransports = vddkTransports;
+ }
+
+ public String getVddkThumbprint() {
+ return vddkThumbprint;
+ }
+
+ public void setVddkThumbprint(String vddkThumbprint) {
+ this.vddkThumbprint = vddkThumbprint;
+ }
+
+ @Override
+ public boolean executeInSequence() {
+ return true;
+ }
+}
diff --git a/core/src/main/java/com/cloud/agent/api/to/VmwareCbtTargetStorageType.java b/core/src/main/java/com/cloud/agent/api/to/VmwareCbtTargetStorageType.java
new file mode 100644
index 000000000000..3707ea498744
--- /dev/null
+++ b/core/src/main/java/com/cloud/agent/api/to/VmwareCbtTargetStorageType.java
@@ -0,0 +1,28 @@
+// 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.
+package com.cloud.agent.api.to;
+
+public enum VmwareCbtTargetStorageType {
+ QCOW2_FILE,
+ RBD_RAW,
+ /**
+ * Raw writes into a host-local block device provided by the destination
+ * primary storage (e.g. a Linstor/DRBD volume). The device is pre-created
+ * at source capacity through the storage adaptor before any data copy.
+ */
+ RAW_BLOCK_DEVICE
+}
diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VRScripts.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VRScripts.java
index 7bfbf786e9b4..95ac5d10c7bf 100644
--- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VRScripts.java
+++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VRScripts.java
@@ -77,6 +77,7 @@ public class VRScripts {
public static final String DIAGNOSTICS = "diagnostics.py";
public static final String RETRIEVE_DIAGNOSTICS = "get_diagnostics_files.py";
+ public static final String INSTANCE_READINESS_CHECK = "instance_readiness_check.py";
public static final String VR_FILE_CLEANUP = "cleanup.sh";
public static final String VR_UPDATE_INTERFACE_CONFIG = "update_interface_config.sh";
diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java
index b9ac455130f3..6d7988c5f6cd 100644
--- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java
+++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java
@@ -50,6 +50,8 @@
import org.apache.cloudstack.diagnostics.DiagnosticsCommand;
import org.apache.cloudstack.diagnostics.PrepareFilesAnswer;
import org.apache.cloudstack.diagnostics.PrepareFilesCommand;
+import org.apache.cloudstack.vm.bootgroup.readiness.InstanceReadinessCheckAnswer;
+import org.apache.cloudstack.vm.bootgroup.readiness.InstanceReadinessCheckCommand;
import org.apache.cloudstack.utils.security.KeyStoreUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.net.util.SubnetUtils;
@@ -231,6 +233,8 @@ private Answer executeQueryCommand(NetworkElementCommand cmd) {
return execute((GetRouterAlertsCommand)cmd);
} else if (cmd instanceof DiagnosticsCommand) {
return execute((DiagnosticsCommand) cmd);
+ } else if (cmd instanceof InstanceReadinessCheckCommand) {
+ return execute((InstanceReadinessCheckCommand) cmd);
} else if (cmd instanceof PrepareFilesCommand) {
return execute((PrepareFilesCommand) cmd);
} else if (cmd instanceof DeleteFileInVrCommand) {
@@ -486,6 +490,15 @@ private Answer execute(DiagnosticsCommand cmd) {
return new DiagnosticsAnswer(cmd, result.isSuccess(), result.getDetails());
}
+ private Answer execute(InstanceReadinessCheckCommand cmd) {
+ _eachTimeout = Duration.standardSeconds(Math.max(cmd.getWait(), 1));
+ String args = cmd.getPort() == null ?
+ String.format("%s %s", cmd.getCheckType(), cmd.getIpAddress()) :
+ String.format("%s %s %s", cmd.getCheckType(), cmd.getIpAddress(), cmd.getPort());
+ final ExecutionResult result = _vrDeployer.executeInVR(cmd.getRouterAccessIp(), VRScripts.INSTANCE_READINESS_CHECK, args, _eachTimeout);
+ return new InstanceReadinessCheckAnswer(cmd, result.isSuccess(), result.getDetails());
+ }
+
private Answer execute(PrepareFilesCommand cmd) {
String fileList = String.join(" ", cmd.getFilesToRetrieveList());
_eachTimeout = Duration.standardSeconds(cmd.getTimeout());
diff --git a/core/src/main/java/com/cloud/agent/transport/compat/AbstractTOAdaptor.java b/core/src/main/java/com/cloud/agent/transport/compat/AbstractTOAdaptor.java
new file mode 100644
index 000000000000..ed5630183ead
--- /dev/null
+++ b/core/src/main/java/com/cloud/agent/transport/compat/AbstractTOAdaptor.java
@@ -0,0 +1,86 @@
+// 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.
+package com.cloud.agent.transport.compat;
+
+import com.cloud.utils.StringUtils;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+import java.lang.reflect.Type;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * JSON serializer adapter for transport classes (com.cloud.agent.api.to.*) that ensures backward compatibility
+ * with older Agent versions due to rename of the fields
+ * (see https://github.com/apache/cloudstack/pull/10514)
+ *
+ * This class does not build its own Gson instance: doing so would silently drop whichever exclusion
+ * strategy (e.g. log redaction) and sibling compat adaptors (for nested TOs) the enclosing Gson was
+ * configured with. Instead, whoever registers an instance of this class into a GsonBuilder is
+ * responsible for also calling {@link #initGson(Gson)} with a Gson that (a) carries that same
+ * exclusion strategy and (b) has adapters registered for any nested TO types that also need field
+ * renaming, but not for this adaptor's own type (to avoid infinite recursion). See
+ * {@link com.cloud.serializer.GsonHelper#setDefaultGsonConfig(com.google.gson.GsonBuilder)}.
+ */
+public class AbstractTOAdaptor implements JsonSerializer {
+ private Gson gson;
+ private Map fieldMappings;
+
+ protected AbstractTOAdaptor(String... fields) {
+ this.fieldMappings = new LinkedHashMap<>();
+ for (int i = 0; i + 1 < fields.length; i += 2) {
+ String sourceField = fields[i];
+ String destinationField = fields[i + 1];
+ // skip empty fields
+ if (StringUtils.isBlank(sourceField) || StringUtils.isBlank(destinationField)) {
+ continue;
+ }
+ this.fieldMappings.put(sourceField, destinationField);
+ }
+ if (this.fieldMappings.isEmpty()) {
+ throw new CloudRuntimeException("Field mappings must not be empty");
+ }
+ }
+
+ public void initGson(Gson gson) {
+ this.gson = gson;
+ }
+
+ @Override
+ public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) {
+ if (src == null) {
+ return null;
+ }
+ JsonElement tree = gson.toJsonTree(src);
+ if (tree.isJsonObject()) {
+ JsonObject obj = tree.getAsJsonObject();
+ for (Map.Entry field : fieldMappings.entrySet()) {
+ String sourceField = field.getKey();
+ String destinationField = field.getValue();
+ if (obj.has(sourceField)) {
+ obj.add(destinationField, obj.get(sourceField));
+ }
+ }
+ }
+ return tree;
+ }
+}
diff --git a/core/src/main/java/com/cloud/agent/transport/compat/DiskTOAdaptor.java b/core/src/main/java/com/cloud/agent/transport/compat/DiskTOAdaptor.java
new file mode 100644
index 000000000000..6b45529de05f
--- /dev/null
+++ b/core/src/main/java/com/cloud/agent/transport/compat/DiskTOAdaptor.java
@@ -0,0 +1,28 @@
+// 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.
+package com.cloud.agent.transport.compat;
+
+import com.cloud.agent.api.to.DiskTO;
+
+/**
+ * See {@link AbstractTOAdaptor}.
+ */
+public class DiskTOAdaptor extends AbstractTOAdaptor {
+ public DiskTOAdaptor() {
+ super("details", "_details");
+ }
+}
diff --git a/core/src/main/java/com/cloud/agent/transport/compat/MigrateCommandAdaptor.java b/core/src/main/java/com/cloud/agent/transport/compat/MigrateCommandAdaptor.java
new file mode 100644
index 000000000000..ddac8afe5975
--- /dev/null
+++ b/core/src/main/java/com/cloud/agent/transport/compat/MigrateCommandAdaptor.java
@@ -0,0 +1,28 @@
+// 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.
+package com.cloud.agent.transport.compat;
+
+import com.cloud.agent.api.MigrateCommand;
+
+/**
+ * See {@link AbstractTOAdaptor}.
+ */
+public class MigrateCommandAdaptor extends AbstractTOAdaptor {
+ public MigrateCommandAdaptor() {
+ super("destinationIp", "destIp", "windows", "isWindows", "virtualMachine", "vmTO");
+ }
+}
diff --git a/core/src/main/java/com/cloud/agent/transport/compat/NetworkTOAdaptor.java b/core/src/main/java/com/cloud/agent/transport/compat/NetworkTOAdaptor.java
new file mode 100644
index 000000000000..01b0cdb4cdf9
--- /dev/null
+++ b/core/src/main/java/com/cloud/agent/transport/compat/NetworkTOAdaptor.java
@@ -0,0 +1,28 @@
+// 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.
+package com.cloud.agent.transport.compat;
+
+import com.cloud.agent.api.to.NetworkTO;
+
+/**
+ * See {@link AbstractTOAdaptor}.
+ */
+public class NetworkTOAdaptor extends AbstractTOAdaptor {
+ public NetworkTOAdaptor() {
+ super("securityGroupEnabled", "isSecurityGroupEnabled");
+ }
+}
diff --git a/core/src/main/java/com/cloud/agent/transport/compat/VirtualMachineTOAdaptor.java b/core/src/main/java/com/cloud/agent/transport/compat/VirtualMachineTOAdaptor.java
new file mode 100644
index 000000000000..24a8c02a59c5
--- /dev/null
+++ b/core/src/main/java/com/cloud/agent/transport/compat/VirtualMachineTOAdaptor.java
@@ -0,0 +1,29 @@
+// 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.
+package com.cloud.agent.transport.compat;
+
+import com.cloud.agent.api.to.VirtualMachineTO;
+
+/**
+ * See {@link AbstractTOAdaptor}.
+ */
+public class VirtualMachineTOAdaptor extends AbstractTOAdaptor {
+
+ public VirtualMachineTOAdaptor() {
+ super("details", "params");
+ }
+}
diff --git a/core/src/main/java/com/cloud/serializer/GsonHelper.java b/core/src/main/java/com/cloud/serializer/GsonHelper.java
index 7de98c08b7e0..7fcfb28f983c 100644
--- a/core/src/main/java/com/cloud/serializer/GsonHelper.java
+++ b/core/src/main/java/com/cloud/serializer/GsonHelper.java
@@ -21,6 +21,14 @@
import java.util.List;
+import com.cloud.agent.api.MigrateCommand;
+import com.cloud.agent.api.to.DiskTO;
+import com.cloud.agent.api.to.NetworkTO;
+import com.cloud.agent.api.to.VirtualMachineTO;
+import com.cloud.agent.transport.compat.DiskTOAdaptor;
+import com.cloud.agent.transport.compat.MigrateCommandAdaptor;
+import com.cloud.agent.transport.compat.NetworkTOAdaptor;
+import com.cloud.agent.transport.compat.VirtualMachineTOAdaptor;
import com.cloud.hypervisor.Hypervisor;
import org.apache.cloudstack.transport.HypervisorTypeAdaptor;
import org.apache.logging.log4j.Logger;
@@ -78,6 +86,42 @@ public static Gson setDefaultGsonConfig(GsonBuilder builder) {
}.getType(), new NwGroupsCommandTypeAdaptor());
builder.registerTypeAdapter(Storage.StoragePoolType.class, new StoragePoolTypeAdaptor());
builder.registerTypeAdapter(Hypervisor.HypervisorType.class, new HypervisorTypeAdaptor());
+
+ // added for compatibility purposes, remove after all Agents migrate to the new version
+ //
+ // Each compat adaptor below needs a "base" Gson to run its own reflective (pre-rename)
+ // serialization through, so that nested TOs are renamed too and the exclusion strategy set
+ // on `builder` (e.g. log redaction) is honoured consistently at every nesting level. That base
+ // Gson is built incrementally off the same builder, snapshotted (via builder.create()) just
+ // before each adaptor's own type is registered on it, so it carries every sibling adaptor it
+ // can nest without ever routing back into itself and recursing forever.
+ DiskTOAdaptor diskAdaptor = new DiskTOAdaptor();
+ NetworkTOAdaptor netAdaptor = new NetworkTOAdaptor();
+ VirtualMachineTOAdaptor vmAdaptor = new VirtualMachineTOAdaptor();
+ MigrateCommandAdaptor migrateAdaptor = new MigrateCommandAdaptor();
+
+ // DiskTO and NetworkTO don't nest any other compat TO, so the plain config built so far is
+ // already the correct base Gson for them.
+ Gson leafDelegateGson = builder.create();
+ diskAdaptor.initGson(leafDelegateGson);
+ netAdaptor.initGson(leafDelegateGson);
+
+ // VirtualMachineTO nests DiskTO[] and NicTO[] (NicTO extends NetworkTO), so its base Gson needs
+ // Disk/Network adapters too. registerTypeHierarchyAdapter is used for NetworkTO so that the
+ // NicTO[]-declared "nics" field is matched via its supertype.
+ builder.registerTypeAdapter(DiskTO.class, diskAdaptor);
+ builder.registerTypeHierarchyAdapter(NetworkTO.class, netAdaptor);
+ Gson vmDelegateGson = builder.create();
+ vmAdaptor.initGson(vmDelegateGson);
+
+ // MigrateCommand nests a VirtualMachineTO, so its base Gson needs the VirtualMachineTO adapter
+ // (which already renames the nested disks/nics above).
+ builder.registerTypeAdapter(VirtualMachineTO.class, vmAdaptor);
+ Gson migrateDelegateGson = builder.create();
+ migrateAdaptor.initGson(migrateDelegateGson);
+
+ builder.registerTypeAdapter(MigrateCommand.class, migrateAdaptor);
+
Gson gson = builder.create();
dsAdaptor.initGson(gson);
dtAdaptor.initGson(gson);
diff --git a/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java
index 34f8d7b8bcdd..7c2d3fc76fc1 100644
--- a/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java
+++ b/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java
@@ -33,6 +33,7 @@ public class TakeBackupCommand extends Command {
private List volumePools;
private List volumePaths;
private Boolean quiesce;
+ private Integer quiesceTimeout;
@LogLevel(LogLevel.Log4jLevel.Off)
private String mountOptions;
@@ -117,6 +118,14 @@ public void setQuiesce(Boolean quiesce) {
this.quiesce = quiesce;
}
+ public Integer getQuiesceTimeout() {
+ return quiesceTimeout;
+ }
+
+ public void setQuiesceTimeout(Integer quiesceTimeout) {
+ this.quiesceTimeout = quiesceTimeout;
+ }
+
public String getMode() {
return mode;
}
diff --git a/core/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckAnswer.java b/core/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckAnswer.java
new file mode 100644
index 000000000000..733294f2b9ce
--- /dev/null
+++ b/core/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckAnswer.java
@@ -0,0 +1,57 @@
+// 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.
+
+package org.apache.cloudstack.vm.bootgroup.readiness;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.cloud.agent.api.Answer;
+import com.cloud.utils.exception.CloudRuntimeException;
+
+public class InstanceReadinessCheckAnswer extends Answer {
+
+ public static final String STDOUT = "stdout";
+ public static final String STDERR = "stderr";
+ public static final String EXITCODE = "exitcode";
+
+ public InstanceReadinessCheckAnswer(InstanceReadinessCheckCommand cmd, boolean result, String details) {
+ super(cmd, result, details);
+ }
+
+ public Map getExecutionDetails() {
+ final Map executionDetails = new HashMap<>();
+ final String details = getDetails();
+ if (StringUtils.isNotEmpty(details) && details.contains("&&")) {
+ final String[] parts = details.split("&&");
+ if (parts.length >= 3) {
+ executionDetails.put(STDOUT, parts[0].trim());
+ executionDetails.put(STDERR, parts[1].trim());
+ executionDetails.put(EXITCODE, parts[2].trim());
+ } else {
+ throw new CloudRuntimeException("Unsupported instance boot group readiness check output format");
+ }
+ } else {
+ executionDetails.put(STDOUT, "");
+ executionDetails.put(STDERR, details);
+ executionDetails.put(EXITCODE, "-1");
+ }
+ return executionDetails;
+ }
+}
diff --git a/core/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckCommand.java b/core/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckCommand.java
new file mode 100644
index 000000000000..8ae58304ae82
--- /dev/null
+++ b/core/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckCommand.java
@@ -0,0 +1,73 @@
+// 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.
+
+package org.apache.cloudstack.vm.bootgroup.readiness;
+
+import com.cloud.agent.api.routing.NetworkElementCommand;
+
+/**
+ * Dispatched to a VR to run a readiness check (ping or TCP port connect) against a user VM's IP,
+ * on behalf of the instance boot group readiness feature. Deliberately separate from
+ * {@code org.apache.cloudstack.diagnostics.DiagnosticsCommand}, which is a general-purpose admin
+ * tool scoped to system VMs (SSVM/CPVM/VR) — this command runs its own dedicated VR script instead
+ * of sharing that one.
+ */
+public class InstanceReadinessCheckCommand extends NetworkElementCommand {
+
+ public static final String CHECK_TYPE_PING = "ping";
+ public static final String CHECK_TYPE_PORT_CHECK = "portcheck";
+
+ private final String checkType;
+ private final String ipAddress;
+ private Integer port;
+ private final boolean executeInSequence;
+
+ public InstanceReadinessCheckCommand(String ipAddress, boolean executeInSequence) {
+ this.checkType = CHECK_TYPE_PING;
+ this.ipAddress = ipAddress;
+ this.executeInSequence = executeInSequence;
+ }
+
+ public InstanceReadinessCheckCommand(String ipAddress, Integer port, boolean executeInSequence) {
+ this.checkType = CHECK_TYPE_PORT_CHECK;
+ this.ipAddress = ipAddress;
+ this.port = port;
+ this.executeInSequence = executeInSequence;
+ }
+
+ public String getCheckType() {
+ return checkType;
+ }
+
+ public String getIpAddress() {
+ return ipAddress;
+ }
+
+ public Integer getPort() {
+ return port;
+ }
+
+ @Override
+ public boolean isQuery() {
+ return true;
+ }
+
+ @Override
+ public boolean executeInSequence() {
+ return executeInSequence;
+ }
+}
diff --git a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml
index cf43b8527a97..6e53ccaf425b 100644
--- a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml
+++ b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml
@@ -375,4 +375,19 @@
+
+
+
+
+
+
+
+
diff --git a/core/src/test/java/com/cloud/agent/resource/virtualnetwork/InstanceReadinessCheckExecutionTest.java b/core/src/test/java/com/cloud/agent/resource/virtualnetwork/InstanceReadinessCheckExecutionTest.java
new file mode 100644
index 000000000000..226bf132d04b
--- /dev/null
+++ b/core/src/test/java/com/cloud/agent/resource/virtualnetwork/InstanceReadinessCheckExecutionTest.java
@@ -0,0 +1,122 @@
+// 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.
+package com.cloud.agent.resource.virtualnetwork;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
+
+import javax.naming.ConfigurationException;
+
+import org.joda.time.Duration;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import com.cloud.agent.api.Answer;
+import com.cloud.agent.api.routing.NetworkElementCommand;
+import com.cloud.utils.ExecutionResult;
+
+import org.apache.cloudstack.vm.bootgroup.readiness.InstanceReadinessCheckAnswer;
+import org.apache.cloudstack.vm.bootgroup.readiness.InstanceReadinessCheckCommand;
+
+@RunWith(MockitoJUnitRunner.class)
+public class InstanceReadinessCheckExecutionTest {
+
+ private static final String ROUTER_IP = "169.254.3.4";
+
+ VirtualRoutingResource resource;
+ VirtualRouterDeployer deployer;
+
+ @Before
+ public void setUp() throws ConfigurationException {
+ deployer = mock(VirtualRouterDeployer.class);
+ when(deployer.prepareCommand(any(NetworkElementCommand.class))).thenAnswer(invocation -> {
+ NetworkElementCommand cmd = invocation.getArgument(0);
+ cmd.setRouterAccessIp(ROUTER_IP);
+ return new ExecutionResult(true, null);
+ });
+ when(deployer.cleanupCommand(any(NetworkElementCommand.class))).thenReturn(new ExecutionResult(true, null));
+ when(deployer.executeInVR(anyString(), anyString(), anyString(), any(Duration.class))).thenReturn(new ExecutionResult(true, "out&&err&&0"));
+
+ resource = new VirtualRoutingResource(deployer);
+ resource.configure("VRResource", new HashMap<>());
+ }
+
+ @Test
+ public void zeroWaitIsFlooredToOneSecondTimeout() {
+ InstanceReadinessCheckCommand cmd = new InstanceReadinessCheckCommand("10.1.1.5", false);
+ cmd.setWait(0);
+
+ resource.executeRequest(cmd);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Duration.class);
+ org.mockito.Mockito.verify(deployer).executeInVR(eq(ROUTER_IP), anyString(), anyString(), captor.capture());
+ assertEquals(1L, captor.getValue().getStandardSeconds());
+ }
+
+ @Test
+ public void positiveWaitIsUsedAsTimeoutSeconds() {
+ InstanceReadinessCheckCommand cmd = new InstanceReadinessCheckCommand("10.1.1.5", false);
+ cmd.setWait(5);
+
+ resource.executeRequest(cmd);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Duration.class);
+ org.mockito.Mockito.verify(deployer).executeInVR(eq(ROUTER_IP), anyString(), anyString(), captor.capture());
+ assertEquals(5L, captor.getValue().getStandardSeconds());
+ }
+
+ @Test
+ public void pingCommandOmitsPortFromArgs() {
+ InstanceReadinessCheckCommand cmd = new InstanceReadinessCheckCommand("10.1.1.5", false);
+
+ resource.executeRequest(cmd);
+
+ ArgumentCaptor argsCaptor = ArgumentCaptor.forClass(String.class);
+ org.mockito.Mockito.verify(deployer).executeInVR(eq(ROUTER_IP), anyString(), argsCaptor.capture(), any(Duration.class));
+ assertEquals("ping 10.1.1.5", argsCaptor.getValue());
+ }
+
+ @Test
+ public void portCheckCommandIncludesPortInArgs() {
+ InstanceReadinessCheckCommand cmd = new InstanceReadinessCheckCommand("10.1.1.5", 8080, false);
+
+ resource.executeRequest(cmd);
+
+ ArgumentCaptor argsCaptor = ArgumentCaptor.forClass(String.class);
+ org.mockito.Mockito.verify(deployer).executeInVR(eq(ROUTER_IP), anyString(), argsCaptor.capture(), any(Duration.class));
+ assertEquals("portcheck 10.1.1.5 8080", argsCaptor.getValue());
+ }
+
+ @Test
+ public void resultIsWrappedInInstanceReadinessCheckAnswer() {
+ InstanceReadinessCheckCommand cmd = new InstanceReadinessCheckCommand("10.1.1.5", false);
+
+ Answer answer = resource.executeRequest(cmd);
+
+ org.junit.Assert.assertTrue(answer instanceof InstanceReadinessCheckAnswer);
+ assertEquals("0", ((InstanceReadinessCheckAnswer) answer).getExecutionDetails().get(InstanceReadinessCheckAnswer.EXITCODE));
+ }
+}
diff --git a/core/src/test/java/com/cloud/agent/transport/RequestTest.java b/core/src/test/java/com/cloud/agent/transport/RequestTest.java
index 0fe42c7cede8..d68107e64917 100644
--- a/core/src/test/java/com/cloud/agent/transport/RequestTest.java
+++ b/core/src/test/java/com/cloud/agent/transport/RequestTest.java
@@ -19,14 +19,21 @@
package com.cloud.agent.transport;
+import java.lang.reflect.Field;
import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
import junit.framework.TestCase;
+import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.config.Configurator;
import org.junit.Assert;
import org.mockito.Mockito;
+import com.google.gson.Gson;
+
import org.apache.cloudstack.storage.command.DownloadCommand;
import org.apache.cloudstack.storage.to.TemplateObjectTO;
@@ -35,19 +42,28 @@
import com.cloud.agent.api.Command;
import com.cloud.agent.api.GetHostStatsCommand;
import com.cloud.agent.api.GetVolumeStatsCommand;
+import com.cloud.agent.api.MigrateCommand;
import com.cloud.agent.api.SecStorageFirewallCfgCommand;
+import com.cloud.agent.api.StartCommand;
import com.cloud.agent.api.UpdateHostPasswordCommand;
import com.cloud.agent.api.storage.DownloadAnswer;
import com.cloud.agent.api.storage.ListTemplateCommand;
+import com.cloud.agent.api.to.DiskTO;
import com.cloud.agent.api.to.NfsTO;
+import com.cloud.agent.api.to.NicTO;
+import com.cloud.agent.api.to.VirtualMachineTO;
import com.cloud.agent.transport.Request.Version;
import com.cloud.exception.UnsupportedVersionException;
+import com.cloud.host.Host;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
+import com.cloud.serializer.GsonHelper;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.Storage.ImageFormat;
import com.cloud.storage.Storage.TemplateType;
import com.cloud.storage.VMTemplateStorageResourceAssoc.Status;
import com.cloud.template.VirtualMachineTemplate;
+import com.cloud.template.VirtualMachineTemplate.BootloaderType;
+import com.cloud.vm.VirtualMachine;
/**
*
@@ -57,7 +73,28 @@
*/
public class RequestTest extends TestCase {
- protected Logger logger = LogManager.getLogger(getClass());
+ private static final Logger logger = LogManager.getLogger(RequestTest.class);
+
+ /**
+ * Changes GsonHelper's logging Gson level AND clears its cached TypeAdapters.
+ *
+ * Gson bakes each Command class's shouldSkipClass/shouldSkipField decision into a TypeAdapter
+ * the first time that class is serialized, then caches it (Gson#typeTokenCache) for the life
+ * of the Gson instance. Since GsonHelper.getGsonLogger() is a static singleton shared for the
+ * whole test JVM, just calling Configurator.setLevel() again has no effect on classes already
+ * cached - they'd keep reflecting whichever level was active the first time they were logged.
+ * Clearing the cache after every level change forces a fresh (correct) evaluation.
+ */
+ private static void setGsonLoggerLevel(String loggerName, Level level) {
+ Configurator.setLevel(loggerName, level);
+ try {
+ Field field = Gson.class.getDeclaredField("typeTokenCache");
+ field.setAccessible(true);
+ ((Map, ?>)field.get(GsonHelper.getGsonLogger())).clear();
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
public void testSerDeser() {
logger.info("Testing serializing and deserializing works as expected");
@@ -73,6 +110,32 @@ public void testSerDeser() {
Request sreq = new Request(2, 3, new Command[] {cmd1, cmd2, cmd3}, true, true);
sreq.setSequence(892403717);
+ Logger gsonLogger = LogManager.getLogger(GsonHelper.class);
+ Level level = gsonLogger.getLevel();
+
+ setGsonLoggerLevel(gsonLogger.getName(), Level.DEBUG);
+ String log = sreq.log("Debug", true, Level.DEBUG);
+ assert (log.contains(UpdateHostPasswordCommand.class.getSimpleName()));
+ assert (log.contains(SecStorageFirewallCfgCommand.class.getSimpleName()));
+ assert (!log.contains(GetHostStatsCommand.class.getSimpleName()));
+ assert (!log.contains("username"));
+ assert (!log.contains("password"));
+
+ setGsonLoggerLevel(gsonLogger.getName(), Level.TRACE);
+ log = sreq.log("Trace", true, Level.TRACE);
+ System.out.println(log);
+ assert (log.contains(UpdateHostPasswordCommand.class.getSimpleName()));
+ assert (log.contains(SecStorageFirewallCfgCommand.class.getSimpleName()));
+ assert (log.contains(GetHostStatsCommand.class.getSimpleName()));
+ assert (!log.contains("username"));
+ assert (!log.contains("password"));
+
+ setGsonLoggerLevel(gsonLogger.getName(), Level.INFO);
+ log = sreq.log("Info", true, Level.INFO);
+ assert (log == null);
+
+ setGsonLoggerLevel(GsonHelper.class.getName(), level);
+
byte[] bytes = sreq.getBytes();
assert Request.getSequence(bytes) == 892403717;
@@ -186,6 +249,79 @@ public void testCompress() {
}
}
+ public void testLogging() {
+ logger.info("Testing Logging");
+ GetHostStatsCommand cmd3 = new GetHostStatsCommand("hostguid", "hostname", 101);
+ Request sreq = new Request(2, 3, new Command[] {cmd3}, true, true);
+ sreq.setSequence(1);
+ Logger gsonLogger = LogManager.getLogger(GsonHelper.class);
+ Level level = gsonLogger.getLevel();
+
+ setGsonLoggerLevel(GsonHelper.class.getName(), Level.DEBUG);
+ String log = sreq.log("Debug", true, Level.DEBUG);
+ assert (log == null);
+
+ log = sreq.log("Debug", false, Level.DEBUG);
+ assert (log != null);
+
+ setGsonLoggerLevel(GsonHelper.class.getName(), Level.TRACE);
+ log = sreq.log("Trace", true, Level.TRACE);
+ assert (log != null);
+
+ assert (log.contains(GetHostStatsCommand.class.getSimpleName()));
+ logger.debug(log);
+
+ setGsonLoggerLevel(GsonHelper.class.getName(), level);
+ }
+
+ public void testCompatFieldRenamingNestedTOs() {
+ logger.info("Testing that renamed fields are restored on nested TOs too, for backward compatibility with older Agents");
+
+ DiskTO diskTO = new DiskTO();
+ diskTO.setDetails(new HashMap());
+
+ NicTO nicTO = new NicTO();
+ nicTO.setSecurityGroupEnabled(true);
+
+ VirtualMachineTO vmTO = new VirtualMachineTO(1, "i-2-3-VM", VirtualMachine.Type.User, 1, 512, 512L * 1024 * 1024, 512L * 1024 * 1024,
+ BootloaderType.HVM, "Other PV (64-bit)", true, true, "vncpassword123");
+ vmTO.setDetails(new HashMap());
+ vmTO.setDisks(new DiskTO[] {diskTO});
+ vmTO.setNics(new NicTO[] {nicTO});
+
+ Host host = Mockito.mock(Host.class);
+ Mockito.when(host.getPrivateIpAddress()).thenReturn("10.1.1.1");
+ StartCommand startCmd = new StartCommand(vmTO, host, false);
+
+ Request startReq = new Request(1, 1, startCmd, true);
+ String startWireJson = GsonHelper.getGson().toJson(new Command[] {startCmd});
+ assert startWireJson.contains("\"params\"") : "VirtualMachineTO.details should be serialized under its old name 'params'";
+ assert startWireJson.contains("\"_details\"") : "nested DiskTO.details should be serialized under its old name '_details'";
+ assert startWireJson.contains("\"isSecurityGroupEnabled\"") : "nested NicTO.securityGroupEnabled should be serialized under its old name 'isSecurityGroupEnabled'";
+ assert startWireJson.contains("vncpassword123") : "wire serialization should still contain the real vncPassword value";
+
+ Logger gsonLogger = LogManager.getLogger(GsonHelper.class);
+ Level gsonLoggerLevel = gsonLogger.getLevel();
+ setGsonLoggerLevel(GsonHelper.class.getName(), Level.TRACE);
+ String startLogJson;
+ try {
+ startLogJson = startReq.log("Trace", true, Level.TRACE);
+ } finally {
+ setGsonLoggerLevel(GsonHelper.class.getName(), gsonLoggerLevel);
+ }
+ assert startLogJson.contains("\"isSecurityGroupEnabled\"") : "renamed fields should still show up in the logging serialization";
+ assert !startLogJson.contains("vncpassword123") : "logging serialization should never contain the plaintext vncPassword value";
+
+ MigrateCommand migrateCmd = new MigrateCommand("i-2-3-VM", "10.1.1.2", true, vmTO, false);
+ String migrateWireJson = GsonHelper.getGson().toJson(new Command[] {migrateCmd});
+ assert migrateWireJson.contains("\"destIp\"") : "MigrateCommand.destinationIp should be serialized under its old name 'destIp'";
+ assert migrateWireJson.contains("\"isWindows\"") : "MigrateCommand.windows should be serialized under its old name 'isWindows'";
+ assert migrateWireJson.contains("\"vmTO\"") : "MigrateCommand.virtualMachine should be serialized under its old name 'vmTO'";
+ assert migrateWireJson.contains("\"params\"") : "VirtualMachineTO nested in MigrateCommand should still be renamed";
+ assert migrateWireJson.contains("\"_details\"") : "DiskTO nested inside the VirtualMachineTO nested in MigrateCommand should still be renamed";
+ assert migrateWireJson.contains("\"isSecurityGroupEnabled\"") : "NicTO nested inside the VirtualMachineTO nested in MigrateCommand should still be renamed";
+ }
+
protected void compareRequest(Request req1, Request req2) {
assert req1.getSequence() == req2.getSequence();
assert req1.getAgentId() == req2.getAgentId();
diff --git a/core/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckAnswerTest.java b/core/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckAnswerTest.java
new file mode 100644
index 000000000000..29aea0dcf172
--- /dev/null
+++ b/core/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckAnswerTest.java
@@ -0,0 +1,113 @@
+// 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.
+package org.apache.cloudstack.vm.bootgroup.readiness;
+
+import java.util.Map;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.cloud.utils.exception.CloudRuntimeException;
+
+public class InstanceReadinessCheckAnswerTest {
+
+ private InstanceReadinessCheckCommand cmd() {
+ return new InstanceReadinessCheckCommand("10.1.1.5", false);
+ }
+
+ @Test
+ public void wellFormedSuccessfulDetailsAreParsed() {
+ InstanceReadinessCheckAnswer answer = new InstanceReadinessCheckAnswer(cmd(), true, " out \n && err && 0 ");
+
+ Map details = answer.getExecutionDetails();
+
+ Assert.assertEquals("out", details.get(InstanceReadinessCheckAnswer.STDOUT));
+ Assert.assertEquals("err", details.get(InstanceReadinessCheckAnswer.STDERR));
+ Assert.assertEquals("0", details.get(InstanceReadinessCheckAnswer.EXITCODE));
+ }
+
+ @Test
+ public void extraDelimitedSegmentsAreIgnoredPastTheThird() {
+ InstanceReadinessCheckAnswer answer = new InstanceReadinessCheckAnswer(cmd(), true, "out&&err&&1&&extra");
+
+ Map details = answer.getExecutionDetails();
+
+ Assert.assertEquals("out", details.get(InstanceReadinessCheckAnswer.STDOUT));
+ Assert.assertEquals("err", details.get(InstanceReadinessCheckAnswer.STDERR));
+ Assert.assertEquals("1", details.get(InstanceReadinessCheckAnswer.EXITCODE));
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void malformedSuccessfulDetailsThrow() {
+ new InstanceReadinessCheckAnswer(cmd(), true, "out&&err").getExecutionDetails();
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void malformedFailedDetailsThrow() {
+ new InstanceReadinessCheckAnswer(cmd(), false, "out&&err").getExecutionDetails();
+ }
+
+ @Test
+ public void failedResultIsStillParsedWhenWellFormed() {
+ // executeInVR (and the VR script) typically reports result=false whenever the underlying
+ // check (ping unreachable, port closed) exits non-zero, even though it still printed a
+ // well-formed "stdout&&stderr&&exitcode" triple. The non-zero exit code must not be
+ // discarded in favor of a generic -1/raw-blob fallback.
+ InstanceReadinessCheckAnswer answer = new InstanceReadinessCheckAnswer(cmd(), false, "out&&unreachable&&1");
+
+ Map details = answer.getExecutionDetails();
+
+ Assert.assertEquals("out", details.get(InstanceReadinessCheckAnswer.STDOUT));
+ Assert.assertEquals("unreachable", details.get(InstanceReadinessCheckAnswer.STDERR));
+ Assert.assertEquals("1", details.get(InstanceReadinessCheckAnswer.EXITCODE));
+ }
+
+ @Test
+ public void failedResultWithoutDelimiterFallsBackToDefaults() {
+ // A dispatch-level failure (e.g. an exception message) never contains the delimiter and
+ // must still fall back to the raw-message/-1 defaults rather than throwing.
+ InstanceReadinessCheckAnswer answer = new InstanceReadinessCheckAnswer(cmd(), false, "agent down");
+
+ Map details = answer.getExecutionDetails();
+
+ Assert.assertEquals("", details.get(InstanceReadinessCheckAnswer.STDOUT));
+ Assert.assertEquals("agent down", details.get(InstanceReadinessCheckAnswer.STDERR));
+ Assert.assertEquals("-1", details.get(InstanceReadinessCheckAnswer.EXITCODE));
+ }
+
+ @Test
+ public void blankDetailsWithSuccessfulResultFallsBackToDefaults() {
+ InstanceReadinessCheckAnswer answer = new InstanceReadinessCheckAnswer(cmd(), true, "");
+
+ Map details = answer.getExecutionDetails();
+
+ Assert.assertEquals("", details.get(InstanceReadinessCheckAnswer.STDOUT));
+ Assert.assertEquals("", details.get(InstanceReadinessCheckAnswer.STDERR));
+ Assert.assertEquals("-1", details.get(InstanceReadinessCheckAnswer.EXITCODE));
+ }
+
+ @Test
+ public void nullDetailsWithSuccessfulResultFallsBackToDefaults() {
+ InstanceReadinessCheckAnswer answer = new InstanceReadinessCheckAnswer(cmd(), true, null);
+
+ Map details = answer.getExecutionDetails();
+
+ Assert.assertEquals("", details.get(InstanceReadinessCheckAnswer.STDOUT));
+ Assert.assertNull(details.get(InstanceReadinessCheckAnswer.STDERR));
+ Assert.assertEquals("-1", details.get(InstanceReadinessCheckAnswer.EXITCODE));
+ }
+}
diff --git a/debian/changelog b/debian/changelog
index 76ac865d9efe..8657d3047701 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -4,12 +4,6 @@ cloudstack (24.0.0-SNAPSHOT) unstable; urgency=low
-- the Apache CloudStack project Wed, 02 Sep 2026 17:26:10 +0200
-cloudstack (4.24.0.0-SNAPSHOT) unstable; urgency=low
-
- * Update the version to 4.24.0.0-SNAPSHOT
-
- -- the Apache CloudStack project Tue, 01 Sep 2026 19:10:00 +0200
-
cloudstack (4.23.0.0-SNAPSHOT) unstable; urgency=low
* Update the version to 4.23.0.0-SNAPSHOT
diff --git a/docs/vmware-cbt-migration.md b/docs/vmware-cbt-migration.md
new file mode 100644
index 000000000000..cc33fa2a99df
--- /dev/null
+++ b/docs/vmware-cbt-migration.md
@@ -0,0 +1,457 @@
+
+
+# VMware CBT migration to KVM
+
+This document describes the implemented VMware Changed Block Tracking (CBT)
+migration flow for Apache CloudStack and the host-level prerequisites required
+on KVM migration hosts.
+
+The goal of this flow is to reduce the downtime and bandwidth needed when
+migrating VMware guests to KVM. Instead of repeatedly copying the full VMware
+disk, CloudStack performs one initial full synchronization and then uses VMware
+CBT to copy only changed disk ranges until the VM is quiet enough for final
+cutover.
+
+No additional CloudStack software is installed on the VMware ESXi hosts or
+vCenter for this flow. The extra migration tooling is installed on the selected
+CloudStack KVM host that performs the VDDK read, virt-v2v conversion, and CBT
+delta application work.
+
+## Implementation status
+
+The implementation contains the CloudStack management model, API
+surface, state tracking, cutover policy, VMware changed-block query
+orchestration, KVM agent initial sync, KVM agent delta sync, cutover
+finalization, import retry handling, delete/cancel cleanup, and UI status
+tracking.
+
+## High-level flow
+
+1. Start a VMware CBT migration session.
+ CloudStack records the destination zone, KVM cluster, optional conversion
+ host, optional storage pool, source vCenter/ESXi details, and discovered
+ VMware disks.
+
+2. Run the initial full sync.
+ The KVM agent exposes each VMware snapshot disk through `nbdkit-vddk` and
+ copies it to a source-equivalent target on KVM primary storage. It uses
+ `qemu-img convert` for the general path and can use `nbdcopy` as an optional
+ accelerator for supported targets. `virt-v2v` does not run in this phase.
+
+3. Register target disks.
+ The management server records the target disk path, format, initial VMware
+ CBT change ID, and snapshot reference for each migrated disk.
+
+4. Run CBT delta synchronization cycles.
+ For each cycle CloudStack creates a VMware snapshot, calls
+ `QueryChangedDiskAreas()` for the source disks, dispatches changed byte
+ ranges to the selected KVM host, records copied bytes and dirty rate, and
+ removes the VMware snapshot.
+
+5. Decide when to cut over.
+ The decision is controlled by global settings:
+
+ * `vmware.cbt.migration.min.cycles`
+ * `vmware.cbt.migration.max.cycles`
+ * `vmware.cbt.migration.quiet.cycles`
+ * `vmware.cbt.migration.quiet.bytes`
+ * `vmware.cbt.migration.quiet.dirty.rate`
+
+ Long-running KVM agent commands are controlled separately by:
+
+ * `vmware.cbt.migration.agent.command.timeout`
+
+6. Final cutover.
+ After the operator powers down the VMware VM, CloudStack performs the final
+ delta sync, runs `virt-v2v` locally against the current KVM-side replica,
+ and imports/registers the VM on KVM.
+
+## Long-running operation timeout
+
+CBT initial full sync, regular delta sync, final delta sync, and cutover
+finalization run as KVM agent commands. The command wait value and the
+agent-side child process timeout are both controlled by:
+
+```text
+vmware.cbt.migration.agent.command.timeout=86400
+```
+
+The value is in seconds. The default is 24 hours. It is intentionally separate
+from `convert.vmware.instance.to.kvm.timeout`, which applies to the older
+OVF/VDDK `ConvertInstanceCommand` import path, and from
+`remote.kvm.instance.disks.copy.timeout`, which applies to remote KVM unmanaged
+disk copy imports.
+
+For very large source disks, set
+`vmware.cbt.migration.agent.command.timeout` above the worst-case expected
+duration. A 1 TiB initial sync can easily require multiple hours depending on
+VDDK transport, ESXi storage, destination storage, and sparse extent behavior.
+The setting is finite because the shared CloudStack script runner treats zero
+as its default one-hour timeout, not as "no timeout".
+
+## KVM migration host selection
+
+CBT migration runs on a KVM host in the destination cluster. If the API call
+does not specify a conversion host, CloudStack selects an enabled KVM routing
+host in the destination cluster that reports:
+
+```text
+host.vddk.blockcopy.support=true
+```
+
+The KVM agent reports this capability during host startup. It is true only when
+the host satisfies the required VDDK, `virt-v2v`, `qemu-img`, `qemu-nbd`, and
+`qemu-io` checks.
+
+Related host detail keys:
+
+```text
+host.vddk.support
+vddk.lib.dir
+host.vddk.version
+host.vddk.blockcopy.support
+host.vddk.blockcopy.inplace.finalization.support
+host.vddk.blockcopy.rbd.support
+host.qemu.img.version
+host.qemu.nbd.version
+host.qemu.io.version
+host.virtv2v.version
+host.virtv2v.inplace.version
+```
+
+Restart `cloudstack-agent` after installing packages or changing
+`agent.properties` so these details are refreshed on the management server.
+
+## Required host capabilities
+
+Each KVM host that can be selected for VMware CBT migration needs:
+
+* A normal CloudStack KVM agent installation.
+* `virt-v2v` for final cutover conversion.
+* `nbdkit` and an `nbdkit` VDDK plugin.
+* VMware VDDK libraries, with a directory containing
+ `lib64/libvixDiskLib.so`.
+* `qemu-img` for target image inspection, creation, and manipulation.
+* `qemu-nbd` for block device exposure during incremental copy workflows.
+* `qemu-io` for applying and verifying changed block ranges.
+* Optional `nbdcopy` from libnbd to accelerate supported initial full-copy
+ paths. CloudStack falls back to `qemu-img` when it is unavailable.
+* Network access from the KVM host to vCenter and ESXi hosts.
+* Access to the destination KVM primary storage pool.
+* `virtio-win` drivers when migrating Windows guests.
+
+Destination-specific requirements:
+
+* Ceph/RBD destination pools additionally require qemu built with the RBD
+ block driver on the conversion host, plus in-place `virt-v2v` support
+ (the `virt-v2v-in-place` binary or the `virt-v2v --in-place` option).
+* Linstor destination pools additionally require in-place `virt-v2v` support,
+ and the conversion host must be a LINSTOR satellite connected to the
+ destination pool (the CloudStack primary storage must be attached to the
+ host, and the host's `hostname` must match its LINSTOR node name). Target
+ volumes are pre-created through the Linstor storage adaptor and written as
+ raw data to the local DRBD device path, so no extra qemu block driver is
+ needed. DRBD permits only one writer: make sure no other node holds the
+ target resource Primary while a migration writes to it.
+
+You do not have to install these packages on every KVM host in the zone unless
+you want every KVM host to be eligible for VMware CBT migration. A common
+deployment model is to prepare a smaller set of conversion-capable KVM hosts and
+explicitly select one of them for migration work.
+
+The KVM agent checks these commands:
+
+```bash
+virt-v2v --version
+nbdkit vddk --dump-plugin libdir=
+qemu-img --version
+qemu-nbd --version
+qemu-io --version
+```
+
+On Ubuntu and Debian hosts the conversion support check also verifies
+that the `nbdkit` package is installed with `dpkg -l nbdkit`.
+
+## VMware VDDK installation
+
+Install VMware VDDK on every KVM host that may run a VMware-to-KVM conversion or
+CBT replication session. A common layout is:
+
+```bash
+mkdir -p /opt/vmware
+tar -xzf VMware-vix-disklib-*.tar.gz -C /opt/vmware
+```
+
+The extracted directory is usually named:
+
+```text
+/opt/vmware/vmware-vix-disklib-distrib
+```
+
+CloudStack auto-detects the first directory named
+`vmware-vix-disklib-distrib`. If auto-detection is not desirable, configure the
+path explicitly in the KVM agent properties:
+
+```properties
+vddk.lib.dir=/opt/vmware/vmware-vix-disklib-distrib
+```
+
+Optional VDDK settings:
+
+```properties
+vddk.transports=nbdssl:nbd
+vddk.thumbprint=
+```
+
+If `vddk.thumbprint` is not set, CloudStack attempts to compute the vCenter
+thumbprint on the KVM host.
+
+## EL8 and Oracle Linux 8 host packages
+
+On an EL8-compatible KVM host, the CloudStack agent package already brings in
+the normal KVM runtime dependencies such as libvirt, qemu-kvm, qemu-img, and
+Python libvirt bindings. The VMware CBT path additionally needs virt-v2v,
+nbdkit, the nbdkit VDDK plugin, qemu-nbd support, and VDDK itself.
+
+Example package installation:
+
+```bash
+dnf install -y cloudstack-agent
+dnf install -y virt-v2v nbdkit qemu-img qemu-kvm-core qemu-kvm-block-curl libnbd
+dnf install -y nbdkit-plugin-vddk || dnf install -y nbdkit-vddk-plugin
+```
+
+Package names can differ between RHEL, Oracle Linux, Rocky, AlmaLinux, and
+enabled module streams. The important validation is not the exact package name,
+but that these commands work:
+
+```bash
+virt-v2v --version
+qemu-img --version
+qemu-nbd --version
+qemu-io --version
+nbdkit vddk --dump-plugin libdir=/opt/vmware/vmware-vix-disklib-distrib
+```
+
+For Windows guests, install a `virtio-win` package or otherwise make the
+VirtIO drivers available to virt-v2v. Some EL8 environments provide this from
+an additional repository rather than the base distribution repositories.
+
+Distribution `virtio-win` packages can lag the upstream project and omit
+drivers for recent Windows releases. `virt-v2v` treats missing drivers as a
+non-fatal warning and would deliver a guest that cannot boot from its VirtIO
+disk, so both the cold import and the CBT cutover conversion **fail fast
+instead**. When `virt-v2v` reports that no VirtIO drivers are available, the
+operation is aborted and identifies the conversion host's
+`/usr/share/virtio-win/virtio-win.iso` as the file to update. Install the
+upstream `latest-virtio` ISO on every conversion host:
+
+```bash
+curl -L -o /usr/share/virtio-win/virtio-win.iso \
+ https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/latest-virtio/virtio-win.iso
+```
+
+Windows Server 2025 additionally rejects the optional `smbus` driver's
+signature at first boot, which loops the virt-v2v firstboot driver
+installation forever; rebuild the ISO without `smbus.inf`/`smbus.cat` before
+converting Windows Server 2025 guests (the admin guide page "Importing VMware
+VMs into KVM" carries the exact commands). Older Windows releases work with
+the unmodified ISO.
+
+### Guest CPU model for Windows 11 and Windows Server 2025
+
+Those releases require CPU features that the KVM agent's default guest CPU
+model (`qemu64`) does not expose. The conversion itself is unaffected — the
+disks convert, the drivers install, the imported instance starts and reaches
+its UEFI firmware — but the guest then drops into *Preparing Automatic Repair*
+instead of the login screen, on every boot. Nothing in the migration output
+indicates a problem, so it reads like a corrupt image.
+
+Set a modern named model on every KVM host that will run such guests, in
+`/etc/cloudstack/agent/agent.properties`, and restart the agent:
+
+```
+guest.cpu.mode=custom
+guest.cpu.model=
+```
+
+The model has to be one the host can actually provide, and that differs by
+processor vendor and generation — `Broadwell-noTSX-IBRS` or newer on Intel,
+`EPYC-IBPB` or newer on AMD. Check before setting it:
+
+```bash
+virsh domcapabilities | grep "usable='yes'"
+```
+
+A model reported as `usable='no'` is refused at domain start with *"the CPU is
+incompatible with host CPU: Host CPU does not provide required features"*, and
+the instance never launches — including system VMs, if the host is the one
+running them. In a cluster, pick a model every host reports as usable so live
+migration stays possible.
+
+`guest.cpu.mode=host-model` and `host-passthrough` also work and need no such
+check, but they tie the instance to the host's CPU and prevent live migration
+between hosts with different processors.
+
+This applies equally to instances deployed natively on KVM — it is a host
+prerequisite for the guest OS, not a property of the migration.
+
+If the nbdkit VDDK plugin package conflicts with the installed nbdkit module
+stream, align the enabled AppStream or vendor repository so `nbdkit` and the
+VDDK plugin come from compatible builds.
+
+## Ubuntu 24.04 host packages
+
+On Ubuntu 24.04, the base KVM and conversion packages are available from the
+standard repositories, but the nbdkit VDDK plugin is not necessarily provided by
+the default Ubuntu archive. Plan for an internal package, a vendor-provided
+package, or a locally built nbdkit VDDK plugin.
+
+Example base package installation:
+
+```bash
+apt-get update
+apt-get install -y cloudstack-agent
+apt-get install -y qemu-utils qemu-system-x86 libvirt-daemon-system libvirt-clients
+apt-get install -y virt-v2v nbdkit libguestfs-tools libnbd-bin
+```
+
+Then install or build the nbdkit VDDK plugin and verify:
+
+```bash
+nbdkit vddk --dump-plugin libdir=/opt/vmware/vmware-vix-disklib-distrib
+```
+
+`qemu-img` and `qemu-nbd` are provided by `qemu-utils` on Ubuntu 24.04.
+
+For Windows guests, ensure a `virtio-win` package or equivalent VirtIO driver
+bundle is available. The KVM agent check looks for a package named
+`virtio-win` on Debian-based hosts, so an internal package with that name is
+the easiest way to satisfy the current check. The upstream-ISO, `smbus` and
+guest CPU model notes above apply here as well.
+
+## Network and VMware requirements
+
+The selected KVM migration host must be able to reach:
+
+* vCenter HTTPS, normally TCP 443.
+* ESXi hosts used for VDDK/NFC access, commonly TCP 902 for NFC paths.
+* Any network paths required by the selected VDDK transport mode.
+* The destination primary storage backend.
+
+The VMware account used for migration must be able to discover the VM, create
+and remove snapshots, read virtual disk data, and query changed disk areas.
+
+VMware CBT must be enabled and valid for the source VM disks before incremental
+cycles can be trusted. A full resync is required when VMware invalidates CBT
+state, for example after CBT reset, certain disk changes, failed snapshot
+consolidation, or storage operations that change disk backing identity.
+
+Avoid or explicitly handle VMware disk types that do not participate cleanly in
+CBT, such as independent disks and unsupported raw device mappings.
+
+## Guest identity, firmware and network defaults derived from the source
+
+Both the cold import and the CBT warm migration derive sensible defaults from
+the source VM instead of falling back to generic import defaults. Everything
+below applies only when the caller did not specify the value explicitly -
+operator-provided parameters always win.
+
+* **Guest OS type** is resolved from the source's configured identity
+ (`config.guestFullName` / `config.guestId`, available regardless of power
+ state). Windows releases newer than the guest OS catalog map to the nearest
+ older entry of the same family (for example Windows Server 2025 maps to the
+ Windows Server 2022 type until the catalog catches up); the mapped type
+ keeps Windows clock and Hyper-V enlightenment semantics correct on KVM.
+* **Firmware** follows the source: a UEFI source gets the `UEFI` setting
+ (`SECURE` when the source has secure boot enabled) and the `q35` machine
+ type. Previously every import defaulted to BIOS, which leaves a UEFI
+ source's GPT/EFI disk unbootable.
+* **Console video** for Windows guests is set to `vga`; the libvirt default
+ (legacy cirrus) renders a blank console on Windows Server 2025 Server Core.
+* **Static IPv4 preservation**: while the source runs, per-NIC addresses,
+ the subnet's default gateway and the DNS servers are captured from VMware
+ Tools. A NIC that is mapped to a CloudStack network without an explicit
+ `nicipaddresslist` entry keeps its guest-configured address when that
+ address fits the target network's CIDR, is not the gateway and is free;
+ otherwise the NIC falls back to normal allocation. DHCP NICs are untouched
+ and pick up the virtual router's lease after migration. Capture requires
+ VMware Tools running in the source, which the warm flow guarantees (the
+ source is live when the migration starts).
+
+## Validation checklist
+
+Run this checklist on every candidate KVM migration host:
+
+```bash
+virt-v2v --version
+qemu-img --version
+qemu-nbd --version
+qemu-io --version
+nbdkit vddk --dump-plugin libdir=/opt/vmware/vmware-vix-disklib-distrib
+test -f /opt/vmware/vmware-vix-disklib-distrib/lib64/libvixDiskLib.so
+systemctl restart cloudstack-agent
+```
+
+Then confirm the host details from the CloudStack API or UI show:
+
+```text
+host.vddk.support=true
+host.vddk.blockcopy.support=true
+host.vddk.blockcopy.inplace.finalization.support=
+host.vddk.blockcopy.rbd.support=
+host.qemu.img.version=
+host.qemu.nbd.version=
+host.qemu.io.version=
+host.virtv2v.version=
+```
+
+If `host.vddk.blockcopy.support` is false, check the agent log on the KVM host
+and validate each command above manually. In-place and RBD capability can be
+false on hosts intended only for QCOW2 fallback, but those hosts are not
+eligible for RBD or LINSTOR cutover.
+
+## Operational notes
+
+* Prefer `nbdssl` or another encrypted VDDK transport where supported.
+* Run migrations from hosts with fast access to destination primary storage.
+* Keep enough temporary space for virt-v2v and libguestfs scratch data; use
+ `convert.instance.env.tmpdir` and `convert.instance.env.virtv2v.tmpdir` when
+ the default temporary filesystem is too small.
+* Use the current quiet-cycle settings to avoid cutting over while the source
+ VM is still producing a high dirty rate.
+* Increase `vmware.cbt.migration.agent.command.timeout` before migrating very
+ large VMs if the baseline copy or cutover finalization can exceed 24 hours.
+* Always clean up VMware snapshots after failed or cancelled cycles.
+* Treat CBT change IDs as per-disk state. If any disk loses valid CBT state,
+ restart that disk from a full sync.
+* On Linstor destinations, prefer a conversion host that holds a diskful
+ replica of the target resources; writes from a diskless satellite traverse
+ the DRBD replication network. DRBD rounds volume sizes up to its extent
+ granularity, so target devices can be slightly larger than the source
+ disks; qemu writes only within the source capacity.
+* Linstor storage-pool heartbeats can fence (reboot) an HA-enabled KVM host if
+ the LINSTOR controller becomes unreachable. Prefer conversion hosts that do
+ not run HA-protected guests, or ensure controller availability for the whole
+ duration of long migrations.
+* Migrated Linstor volumes keep their migration-time names (the LINSTOR
+ resource is `cs-` plus the recorded volume path, e.g. `cs-cbt--`
+ for warm migrations). This is expected and does not affect volume lifecycle
+ operations, which address volumes by their recorded path.
diff --git a/docs/vmware-cbt/README.md b/docs/vmware-cbt/README.md
new file mode 100644
index 000000000000..b9d745750e1a
--- /dev/null
+++ b/docs/vmware-cbt/README.md
@@ -0,0 +1,1550 @@
+
+
+# VMware CBT Migration Implementation Notes
+
+## Introduction
+
+This document describes the VMware Changed Block Tracking (CBT) migration design
+and implementation for Apache CloudStack 24.0.0. It is written as source material
+for an Apache CloudStack CWIKI architecture page and should be treated as an
+implementation guide, not only as a design sketch.
+
+The feature provides a warm VMware-to-KVM migration flow. CloudStack creates a
+KVM-side source-equivalent disk replica from a VMware VDDK snapshot, keeps that
+replica current with one or more VMware CBT delta cycles, requires the operator
+to power off the VMware source VM, runs one final CBT delta cycle, finalizes the
+replica with `virt-v2v`, and imports the finalized disk(s) as a regular
+CloudStack KVM VM.
+
+The essential architectural point is that VMware CBT migration is a two-phase
+disk strategy:
+
+- Phase 1 keeps a KVM-side source-equivalent replica current using VDDK and
+ VMware CBT.
+- Phase 2 finalizes the fully synchronized replica with `virt-v2v` only after
+ the source VM is powered off.
+
+This gives CloudStack warm migration behavior without trying to apply VMware
+changed blocks to a disk that has already been transformed for KVM, and without
+requiring a second full VMware read at cutover time.
+
+## Problem Statement
+
+The existing VMware import paths are useful for one-time imports, but they do
+not provide a warm migration loop where the target disk is kept current while
+the source VM continues to run on VMware. Large VMs therefore either require a
+longer downtime window or repeated full-copy style attempts.
+
+VMware CBT provides the changed-block metadata needed for a replication-style
+workflow, but the design must avoid a common trap: once `virt-v2v` modifies a
+disk for KVM guest bootability, VMware CBT byte ranges can no longer be safely
+applied to that transformed disk. The implementation must therefore keep the
+replication target source-equivalent until final cutover, then run guest
+conversion only after the final delta is applied.
+
+## Goals
+
+- Provide an operator-driven warm migration flow from VMware to CloudStack KVM.
+- Support both existing registered vCenter records and external vCenter details
+ supplied during import.
+- Run the initial full copy once through VDDK and `qemu-img`.
+- Apply subsequent VMware CBT delta cycles to the KVM-side replica.
+- Make long-running start, sync, and cutover operations asynchronous.
+- Keep migration progress visible through list APIs and the UI migration table.
+- Validate destination storage, host capabilities, and compute offering sizing
+ before expensive work starts.
+- Persist external vCenter credentials only through CloudStack encrypted DAO
+ fields and never expose passwords in API responses.
+- Reuse the existing CloudStack converted-VM import path after cutover
+ finalization.
+
+## Scope
+
+Current implementation scope:
+
+- Target hypervisor: KVM.
+- Source hypervisor: VMware through VDDK and VMware CBT.
+- Admin API surface for preflight, start, list, sync, cutover, cancel, and
+ delete.
+- Async API execution for long-running start, sync, and cutover operations.
+- Management-server state model for migrations, disks, and delta cycles.
+- External vCenter credential persistence for long-lived UI sessions, encrypted
+ with CloudStack DAO encryption.
+- KVM agent wrappers for initial VDDK full sync, delta block copy, cutover
+ finalization, and cleanup.
+- Filesystem-like primary storage target support using QCOW2 files.
+- Ceph/RBD primary storage target support using raw RBD images.
+- Non-in-place `virt-v2v` fallback for QCOW2 file targets, gated by global
+ configuration.
+- Early compute-offering validation against discovered VMware VM CPU and memory.
+- Dynamic/custom service offering support through the same `details` keys used
+ by the existing OVF/VDDK import flow.
+- UI integration in the Import/Export Instances area, including job polling and
+ table auto-refresh while CBT migrations are active.
+
+## Non-Goals
+
+Not implemented or intentionally blocked:
+
+- Non-in-place `virt-v2v` fallback for Ceph/RBD targets. RBD targets require
+ in-place finalization because fallback produces local QCOW2 files.
+- Automatic source VM shutdown during cutover.
+- Continuous byte-level progress from `qemu-img convert`. The UI currently
+ receives state transitions, disk target paths, delta cycle byte counts, dirty
+ rates, and cycle descriptions through the list API.
+- Applying CBT deltas to a guest-converted disk.
+- Replacing the existing OVF/VDDK one-time import flows.
+
+## Key Principles
+
+- CloudStack does not power off the VMware source VM automatically. The
+ operator must shut it down before final cutover.
+- CloudStack does not store vCenter passwords in clear text.
+- Initial sync and delta sync keep the target disk source-equivalent.
+- `virt-v2v` runs only at finalization, after the final CBT delta cycle.
+- The final `virt-v2v` work is local to the KVM-side replica and does not
+ perform a second full VMware read.
+- The detailed status source is `listVmwareCbtMigrations`, not only the async
+ job result.
+- Failed final import after successful finalization is retryable through the
+ `ReadyForImport` state.
+
+## High-Level Architecture
+
+The implementation has four main layers:
+
+- CloudStack management-server APIs and orchestration.
+- CloudStack database state for migration sessions, source disks, and delta
+ cycles.
+- KVM agent commands that perform VDDK reads, QCOW2 or raw RBD writes,
+ `virt-v2v` finalization, and cleanup.
+- UI workflow in the Import/Export Instances area.
+
+Management-server API and orchestration:
+
+- `StartVmwareCbtMigrationCmd`
+- `CheckVmwareCbtMigrationPrerequisitesCmd`
+- `ListVmwareCbtMigrationsCmd`
+- `SyncVmwareCbtMigrationCmd`
+- `CutoverVmwareCbtMigrationCmd`
+- `CancelVmwareCbtMigrationCmd`
+- `DeleteVmwareCbtMigrationCmd`
+- `VmwareCbtMigrationManagerImpl`
+- `VmwareCbtStorageTarget`
+- `VmwareCbtMigrationCutoverPolicy`
+
+Persistence:
+
+- `cloud.vmware_cbt_migration`
+- `cloud.vmware_cbt_migration_disk`
+- `cloud.vmware_cbt_migration_cycle`
+
+KVM agent wrappers:
+
+- `LibvirtVmwareCbtPrepareCommandWrapper`
+- `LibvirtVmwareCbtSyncCommandWrapper`
+- `LibvirtVmwareCbtCutoverCommandWrapper`
+- `LibvirtVmwareCbtCleanupCommandWrapper`
+
+UI:
+
+- `ui/src/views/tools/ImportUnmanagedInstance.vue`
+- `ui/src/views/tools/ManageInstances.vue`
+- `ui/src/views/tools/VmwareCbtMigrations.vue`
+
+## Architecture Diagrams
+
+The diagrams below are intentionally plain text so they can be pasted directly
+into Apache CWIKI source and remain editable.
+
+### End-To-End CBT Flow
+
+```text
+VMware source VM
+ |
+ | VMware snapshot + CBT metadata
+ v
+VDDK / nbdkit-vddk on KVM conversion host
+ |
+ | Initial baseline: full logical disk read
+ v
+CloudStack primary storage
+ |
+ | Source-equivalent replica
+ | - qcow2 file on filesystem-like storage
+ | - raw RBD image on Ceph/RBD storage
+ v
+Delta sync cycles
+ |
+ | CBT changed ranges only
+ v
+Operator powers off source VM
+ |
+ | Cutover: final delta + virt-v2v finalization
+ v
+CloudStack KVM VM
+```
+
+### Initial Sync Transfer Behavior
+
+VMware CBT initial sync is not guest-filesystem-used-space-oriented. CloudStack
+does not inspect the guest filesystem to decide which files are in use; it reads
+the disk image exposed by VDDK/NBD and writes the target replica.
+
+The amount of data transferred depends on what VDDK exposes for the source disk
+and datastore. Some source paths expose the baseline as one allocated logical
+range, while others expose sparse `hole,zero` extents. The destination can remain
+thin or sparse when the source and copy path expose zero ranges correctly, but
+operators must size the initial-copy window for the full logical disk capacity.
+
+The KVM agent uses `qemu-img convert` for the general full-copy path. When
+`nbdcopy` is installed, it can accelerate supported pre-created raw targets and
+RBD targets served through a local `qemu-nbd` bridge. `nbdcopy` is optional;
+capability and correctness do not depend on it.
+
+```text
+Example VMware disk capacity: 5 GiB
+Guest-used data: near 0 GiB
+Datastore/path behavior: datastore-dependent
+
+During initial sync:
+ VMware/VDDK exposes allocation/extents to NBD.
+ CloudStack reads the ranges that qemu sees through that NBD export.
+ qemu-img writes a qcow2 target and can preserve thin/sparse output.
+
+Possible outcome: full logical transfer or sparse-aware transfer
+Target allocation: backend- and extent-dependent
+```
+
+### Linked Clone And Full Clone Baselines
+
+The initial baseline reads the logical disk presented by VDDK. Linked clone vs
+full clone is not enough by itself to predict transfer volume. The decisive
+question is whether VDDK/NBD exposes trustworthy allocation and zero extents for
+the source datastore and disk chain.
+
+```text
+Linked clone source
+ child delta VMDK
+ |
+ v
+ parent/base VMDK chain
+ |
+ v
+ VDDK presents one logical disk
+
+Full clone source
+ single/full VMDK
+ |
+ v
+ VDDK presents one logical disk
+
+CBT initial sync:
+ both are read as the full logical disk capacity.
+ The target qcow2 can still be sparse/thin after the write.
+```
+
+### Delta Sync And Cutover Eligibility
+
+```text
+Initial sync completed
+ |
+ v
+Delta cycle 1 -> changed bytes / dirty rate measured
+ |
+ v
+Delta cycle 2 -> quiet-cycle counters updated
+ |
+ v
+Repeat until one boundary is met:
+
+ min cycles satisfied
+ AND quiet bytes threshold satisfied
+ AND quiet dirty-rate threshold satisfied
+ AND quiet cycle count satisfied
+
+OR
+
+ max cycle count reached
+
+ |
+ v
+Migration becomes ReadyForCutover
+```
+
+### Cutover Finalization Paths
+
+```text
+Final CBT delta sync
+ |
+ v
+Check selected conversion host capability
+ |
+ +--> virt-v2v-in-place available
+ | |
+ | v
+ | run virt-v2v-in-place finalization
+ |
+ +--> virt-v2v supports --in-place
+ | |
+ | v
+ | run virt-v2v --in-place finalization
+ |
+ +--> non-in-place fallback explicitly allowed
+ |
+ v
+ run regular virt-v2v -o local fallback finalization
+
+Result:
+ finalized qcow2 moved to primary storage root
+ VM imported with VirtIO root disk controller
+```
+
+### Final Disk Placement
+
+```text
+During sync:
+ /mnt//cloudstack-cbt//...
+
+After finalization/import:
+ /mnt//.qcow2
+
+CloudStack DB target path:
+ updated to the final primary-storage-root location
+```
+
+## End-To-End Flow
+
+### 1. Preflight
+
+`checkVmwareCbtMigrationPrerequisites` validates the source and destination
+before a long-running job is started. The command is synchronous and returns a
+structured preflight response with pass, warn, and fail findings.
+
+The preflight checks include:
+
+- Destination zone and KVM cluster lookup.
+- Destination primary storage lookup or implicit pool selection.
+- Storage target classification.
+- KVM conversion host selection.
+- Host VMware CBT capability checks.
+- Host in-place finalization capability checks.
+- Host qemu RBD capability checks and an active temporary RBD create/write/read/delete
+ probe when the selected primary storage is Ceph/RBD.
+- vCenter source resolution, either by `existingvcenterid` or external
+ vCenter fields.
+- Source VM discovery.
+- Source VM CBT support.
+- Source VM CBT enabled state.
+- Source VM consolidation-needed state.
+- Existing VMware snapshot count.
+- Source disk discovery.
+- Windows guest conversion dependency check on the selected KVM conversion host
+ when the source VM is a Windows guest.
+- Optional compute offering sizing validation.
+
+The preflight response includes, among other fields:
+
+- `ready`
+- `storagewritertype`
+- `storagewritersupported`
+- `storagerequiresinplacefinalization`
+- `convertinstancehostinplacefinalizationsupported`
+- `noninplacefinalizationfallbackallowed`
+- `noninplacefinalizationfallbacksupported`
+- `sourcecpunumber`
+- `sourcecpuspeed`
+- `sourcememory`
+- `sourceguestosid`
+- `sourceguestos`
+- `disk`
+- `finding`
+
+If CBT is supported but not enabled, preflight reports a warning. The start
+path can enable CBT before creating the baseline snapshot.
+
+The source guest OS fields are populated from the VMware inventory and are used
+to decide whether Windows-specific conversion prerequisites apply. They do not
+change the migration mode or disk-copy behavior by themselves.
+
+### Guest OS Mapping And Source OS Labels
+
+VMware exposes more than one guest OS signal. The VM's configured guest OS
+option can differ from the runtime guest OS label reported by VMware Tools. For
+example, a powered-on VM can show `Ubuntu Linux (64-bit)` in the vSphere summary
+while VM Options still show `Other (64-bit)`. CloudStack discovery and preflight
+therefore treat source guest OS values as inventory facts rather than as a safe
+automatic target OS selection.
+
+The target CloudStack guest OS is the optional `osid` supplied to
+`startVmwareCbtMigration` and used later by the shared KVM import path. Operators
+should verify this value before starting migration, especially for generic VMware
+guest IDs such as `ubuntu64Guest`, which can represent multiple Ubuntu releases.
+If the desired target guest OS is not selectable, the corresponding
+`guest_os_hypervisor` mapping must exist for the source hypervisor version.
+
+### 2. Start And Initial Full Sync
+
+`startVmwareCbtMigration` is async. It creates the migration record, persists
+source disk rows, creates a baseline VMware snapshot, prepares target paths, and
+dispatches the initial full sync to the selected KVM host.
+
+For Windows source VMs, the start path repeats the same conversion dependency
+check reported by preflight before the baseline snapshot and initial sync are
+started. This prevents a long CBT copy from producing a disk that cannot be made
+bootable because the selected KVM conversion host is missing `virtio-win`.
+
+State transitions:
+
+- `Created`
+- `InitialSync`
+- `Replicating` when the initial VDDK full sync completes successfully
+- `Failed` if any initial copy or metadata step fails
+
+Important implementation details:
+
+- For filesystem-like storage, target paths are prepared before dispatch and
+ are shown in API/UI while the sync is running.
+- Default target path pattern with explicit primary storage:
+ `/mnt//cloudstack-cbt//.qcow2`
+- Default target path pattern without explicit pool:
+ `/var/lib/libvirt/images/cloudstack-cbt//.qcow2`
+- The agent writes the vCenter password to a temporary password file and passes
+ it to `nbdkit` as `password=+`.
+- After successful copy, management refreshes and records the baseline disk
+ `changeId` values from the VMware snapshot.
+- The baseline snapshot is removed after the initial sync attempt when possible.
+
+### 3. Delta Synchronization
+
+`syncVmwareCbtMigration` is async and serialized by migration ID through
+`BaseAsyncCmd.migrationSyncObject`.
+
+Allowed migration states:
+
+- `Replicating`
+- `ReadyForCutover`
+
+Delta cycle sequence:
+
+1. Create a new `vmware_cbt_migration_cycle` row.
+2. Create a VMware snapshot for the cycle.
+3. Query VMware CBT changed disk areas from the previous per-disk `changeId`.
+4. Persist cycle state as `QueryingChangedAreas`, then `CopyingChangedBlocks`.
+5. Send changed block ranges to the KVM agent.
+6. Agent copies only those ranges from the VDDK NBD source into the target
+ replica.
+7. Management records changed bytes, duration, dirty rate, and the next
+ per-disk `changeId`.
+8. The cycle snapshot is removed when possible.
+9. The cutover policy decides whether to remain in `Replicating` or move to
+ `ReadyForCutover`.
+
+The cycle response exposes:
+
+- `cyclenumber`
+- `snapshotmor`
+- `changedbytes`
+- `dirtyrate`
+- `duration`
+- `state`
+- `description`
+- `created`
+- `lastupdated`
+
+Delta sync progress is represented as state and cycle metadata rather than a
+streamed progress bar. While a cycle is running, the row description can show
+what the manager knows, for example that changed areas are being queried or
+that a number of ranges has been dispatched. After the agent returns, the cycle
+shows copied bytes, dirty rate, duration, and a textual summary.
+
+### 4. Cutover
+
+`cutoverVmwareCbtMigration` is async and serialized by migration ID.
+
+Allowed migration states:
+
+- `ReadyForCutover`
+- `ReadyForImport`
+
+Cutover requires the source VM to be powered off. CloudStack checks the VMware
+power state and rejects cutover if the source VM is still powered on:
+
+```text
+Cannot cut over VMware CBT migration while source VM is in power state PowerOn.
+Gracefully shut down the source VM, then retry cutover.
+```
+
+CloudStack does not attempt a graceful shutdown or power-off. The operator must
+shut down the VMware source VM before final cutover. This avoids surprising the
+operator and avoids needing to encode site-specific guest shutdown policy in the
+migration path.
+
+Cutover sequence:
+
+1. Verify source VM is powered off.
+2. Set migration state to `CuttingOver`.
+3. Run a final CBT delta sync while the VM is off.
+4. Run `virt-v2v` finalization on the KVM host.
+5. Move finalized disk files to the primary storage root with generated UUID
+ names, then update disk target paths from the agent result.
+6. Set state to `ReadyForImport`.
+7. Call the shared external KVM VM import path.
+8. On success, set state to `Completed`, store the new CloudStack VM ID, and
+ clear stored external vCenter credentials.
+9. On import failure, keep state as `ReadyForImport` so the operator can retry
+ cutover to repeat only the import step against already finalized disks.
+
+### 5. Cancel And Delete
+
+`cancelVmwareCbtMigration` is synchronous. If the migration is not terminal, it
+sends a best-effort cleanup command, marks the migration `Cancelled`, and clears
+stored external vCenter credentials.
+
+`deleteVmwareCbtMigration` is synchronous. It is allowed for `Failed`,
+`Cancelled`, and `Completed` migrations. With `cleanup=true` (the default), it
+sends a cleanup command before deleting failed or cancelled child rows and the
+parent row. `Completed` migration deletion is always record-only: CloudStack
+removes the CBT bookkeeping rows but never deletes the imported VM, CloudStack
+volumes, finalized target disks, or primary-storage files.
+
+Deletion behavior:
+
+- Deletes `vmware_cbt_migration_cycle` rows.
+- Deletes `vmware_cbt_migration_disk` rows.
+- Deletes the `vmware_cbt_migration` row.
+- Clears stored source credentials before row removal.
+- For `Failed` or `Cancelled` rows, cleans target files only if the paths are
+ under the migration-specific `/cloudstack-cbt//` directory
+ and cleanup is enabled. For RBD targets, cleanup removes only temporary RBD
+ image names containing the CloudStack-owned `cloudstack-cbt--`
+ marker.
+- For `Completed` rows, ignores cleanup and removes only CBT migration records.
+- Skips immediate cleanup if another active migration is running on the same
+ conversion host, to avoid disturbing active agent-side work.
+
+The KVM cleanup wrapper resolves migration directories from disk target paths
+and removes only the migration-specific directory tree for file targets. For RBD
+targets it uses the destination storage pool and deletes only marked temporary
+images, never arbitrary RBD images.
+
+## Conversion Model: One VMware Baseline Read
+
+The feature deliberately separates replication from final conversion.
+
+Initial full sync does not run `virt-v2v`. The KVM agent opens the VMware source
+snapshot through `nbdkit` with the VDDK plugin. Filesystem-like primary storage
+writes QCOW2 with `qemu-img convert`:
+
+```text
+nbdkit -r -U - vddk ... --run \
+ 'qemu-img convert -f raw -O qcow2 "$uri" ""'
+```
+
+Ceph/RBD primary storage is a raw RBD image. When `nbdcopy` is available, the
+agent pre-creates the image, exposes it through a temporary local `qemu-nbd`
+bridge, and copies NBD to NBD. Otherwise it uses `qemu-img convert` directly
+against the qemu RBD URL. LINSTOR similarly uses `nbdcopy` when available for
+its pre-created raw DRBD device and falls back to `qemu-img convert`.
+
+Example RBD fallback path:
+
+```text
+nbdkit -r -U - vddk ... --run \
+ 'qemu-img convert -f raw -O raw "$uri" "rbd:/:..."'
+```
+
+This produces a source-equivalent replica on the selected primary storage. It
+is not yet a CloudStack-imported VM disk. It is a replication target that can
+safely receive raw changed blocks from later CBT cycles.
+
+Delta sync also does not run `virt-v2v`. Management creates a VMware snapshot,
+queries changed disk areas using VMware CBT metadata, and dispatches changed
+byte ranges to the KVM agent. The agent opens the snapshot through `nbdkit`,
+parses the temporary nbdkit Unix socket from the NBD URI, uses `qemu-img
+convert --image-opts` to materialize bounded raw chunks for only the changed
+ranges, and patches the replica with `qemu-io`. File targets use `-f qcow2`;
+RBD targets use `-f raw` against the qemu RBD URL for the target image.
+
+Final cutover is where `virt-v2v` runs. After the source VM is powered off, the
+manager runs one final CBT delta cycle so the replica is current. Then the agent
+runs one of the finalization paths:
+
+- `virt-v2v-in-place`, when available.
+- `virt-v2v --in-place`, when the installed `virt-v2v` supports it.
+- Regular `virt-v2v -o local`, only when the non-in-place fallback is explicitly
+ enabled and the target is a QCOW2 file target.
+
+Ceph/RBD targets require one of the in-place finalization paths. They do not
+allow non-in-place fallback because fallback creates local QCOW2 output rather
+than modifying the raw RBD image that CloudStack will import.
+
+So there is not a second full VMware read during finalization. The final
+`virt-v2v` step is local work against the up-to-date KVM-side replica. It may
+still require local storage reads and writes, especially in fallback mode, but
+it is not another VDDK full copy from vCenter.
+
+The initial baseline may read up to the full logical VMware disk range,
+regardless of guest filesystem usage. This does not imply thick allocation on
+the destination: QCOW2, RBD, and LINSTOR can remain sparse or thin when the
+source and copy path expose zero ranges correctly. Delta cycles are CBT-range
+based and copy only changed ranges.
+
+This design avoids applying future CBT raw block deltas to a disk that has
+already been modified by `virt-v2v`. Applying VMware CBT ranges to a converted
+guest disk would be unsafe because the layout no longer represents the source
+VMware disk byte-for-byte.
+
+## API Changes
+
+All commands are admin-only.
+
+### `checkVmwareCbtMigrationPrerequisites`
+
+Synchronous preflight command.
+
+Required:
+
+- `zoneid`
+- `clusterid`
+- `sourcevmname`
+
+Source vCenter selection:
+
+- Existing registered vCenter: `existingvcenterid`
+- External vCenter: `vcenter`, `datacentername`, `username`, `password`
+
+Optional:
+
+- `hostip`
+- `clustername`
+- `convertinstancehostid`
+- `convertinstancestoragepoolid`
+- `serviceofferingid`
+- `details`
+
+The command has `requestHasSensitiveInfo=true` because external vCenter
+credentials can be provided.
+
+### `startVmwareCbtMigration`
+
+Async command. Starts the migration and initial full sync.
+
+Required:
+
+- `zoneid`
+- `clusterid`
+- `serviceofferingid`
+- `sourcevmname`
+
+Source vCenter selection:
+
+- Existing registered vCenter: `existingvcenterid`
+- External vCenter: `vcenter`, `datacentername`, `username`, `password`
+
+Optional import inputs:
+
+- `displayname`
+- `hostname`
+- `account`
+- `domainid`
+- `projectid`
+- `templateid`
+- `osid`
+- `datadiskofferinglist`
+- `nicnetworklist`
+- `nicipaddresslist`
+- `forced`
+
+Optional conversion inputs:
+
+- `convertinstancehostid`
+- `convertinstancestoragepoolid`
+- `details`
+
+If `convertinstancestoragepoolid` is omitted, CloudStack auto-selects the only
+CBT-compatible primary storage pool in the destination cluster/zone. Supported
+target pool types are `NetworkFilesystem`, `Filesystem`, `SharedMountPoint`,
+and `RBD`. If more than one compatible pool is available, the operator must
+provide the pool explicitly.
+
+Useful `details` keys:
+
+- `vddk.lib.dir`
+- `vddk.transports`
+- `vddk.thumbprint`
+- `cpuNumber`
+- `cpuSpeed`
+- `memory`
+
+The command has `requestHasSensitiveInfo=true` and
+`responseHasSensitiveInfo=false`.
+
+### `listVmwareCbtMigrations`
+
+Synchronous status command and the main progress source for UI and automation.
+
+Filters:
+
+- `id`
+- `zoneid`
+- `accountid`
+- `vcenter`
+- `sourcevmname`
+- `state`
+
+The response includes migration-level state, current step, current step
+duration, last error, cycle counters, changed byte totals, disks, and cycles.
+`currentstepduration` is populated for non-terminal migrations and is intended
+for the UI table marker similar to Import VM Tasks.
+
+### `syncVmwareCbtMigration`
+
+Async command. Runs one delta sync cycle.
+
+Required:
+
+- `id`
+
+Optional:
+
+- `username`
+- `password`
+
+For an external vCenter migration, stored credentials are used when available.
+If override credentials are supplied, both username and password must be
+provided. Overrides are stored back to the migration so a UI operator can return
+later and continue syncing.
+
+### `cutoverVmwareCbtMigration`
+
+Async command. Runs final powered-off sync, finalizes disks, and imports the VM.
+
+Required:
+
+- `id`
+
+Optional:
+
+- `username`
+- `password`
+
+The command fails early if the source VMware VM is not powered off.
+
+### `cancelVmwareCbtMigration`
+
+Synchronous command.
+
+Required:
+
+- `id`
+
+Cancels a non-terminal migration, attempts cleanup, and clears stored external
+source credentials.
+
+### `deleteVmwareCbtMigration`
+
+Synchronous command.
+
+Required:
+
+- `id`
+
+Optional:
+
+- `cleanup` (default `true`)
+
+Allowed for `Failed`, `Cancelled`, and `Completed` migrations. Completed
+migration deletes are record-only and never remove the imported CloudStack VM,
+volumes, finalized target disks, or primary-storage files.
+
+## Configuration And Cutover Policy
+
+The cutover recommendation is based on global settings:
+
+| Global setting | Default | Meaning |
+| --- | ---: | --- |
+| `vmware.cbt.migration.min.cycles` | `1` | Minimum number of manual/API delta cycles that must complete before quiet-cycle readiness can make the migration eligible for cutover. |
+| `vmware.cbt.migration.max.cycles` | `5` | Maximum number of manual/API delta cycles before CloudStack marks the migration ready for cutover even if the quiet-cycle thresholds were not met. |
+| `vmware.cbt.migration.quiet.cycles` | `2` | Number of consecutive quiet cycles required before CloudStack marks the migration ready for cutover. |
+| `vmware.cbt.migration.quiet.bytes` | `1073741824` | Maximum changed bytes in one cycle for that cycle to be considered quiet. The default is 1 GiB. |
+| `vmware.cbt.migration.quiet.dirty.rate` | `16777216` | Maximum changed bytes per second for that cycle to be considered quiet. The default is 16 MiB/s. |
+| `vmware.cbt.migration.agent.command.timeout` | `86400` | Timeout in seconds for long-running VMware CBT data-plane commands on the KVM agent. This covers initial full sync, regular delta sync, final delta sync, and cutover finalization. The default is 24 hours. |
+
+The manager tracks:
+
+- Number of completed delta cycles.
+- Number of consecutive quiet cycles.
+- Last changed bytes.
+- Last dirty rate.
+- Total changed bytes.
+
+A cycle is quiet when its changed bytes and dirty rate are below the configured
+thresholds. Both thresholds must pass. For example, with defaults, a cycle that
+copies 1.2 GiB at 10 MiB/s is not quiet because the changed-byte threshold is
+exceeded even though the dirty-rate threshold passed.
+
+Once enough quiet cycles have accumulated, or once the maximum manual/API
+delta-cycle count is reached, the migration moves to `ReadyForCutover`.
+
+The operator can still manually run sync cycles while the migration is in
+`Replicating` or `ReadyForCutover`.
+
+The UI intentionally exposes the Cutover button only when the server-side
+migration state is exactly `ReadyForCutover`. Before that point the UI exposes
+Sync delta, because the server has not yet recommended final cutover. If a
+cutover is started, CloudStack creates one additional final CBT delta cycle
+inside the cutover operation. Therefore the final displayed cycle count can be
+one higher than `vmware.cbt.migration.max.cycles`: for example, five ordinary
+delta cycles can make the migration ready, and cutover can then record cycle 6
+as the final synchronization cycle.
+
+### Timeouts And Large VMs
+
+The long-running CBT work is done by KVM agent commands. CloudStack sets the
+agent command `wait` value from
+`vmware.cbt.migration.agent.command.timeout`, and the KVM wrappers use the same
+value as the timeout for child processes such as `nbdkit`, `qemu-img`, and
+`virt-v2v`.
+
+The timeout is finite by design. The shared CloudStack `Script` helper treats a
+zero timeout as its default one-hour timeout, so `0` is not a safe way to mean
+"run forever". For large source disks, tune the timeout above the expected
+worst-case duration. A rough estimate is:
+
+```text
+timeout_seconds >= source_bytes / expected_bytes_per_second * safety_factor
+```
+
+For example, a 1 TiB initial sync at 100 MiB/s is roughly 3 hours before
+overhead; at 25 MiB/s it is roughly 12 hours. The default 24-hour timeout is
+intended to be safe for large migrations while still
+protecting the agent from permanently stuck child processes.
+
+Related import settings that operators often notice are:
+
+| Global setting | Applies to CBT? | Default | Meaning |
+| --- | --- | ---: | --- |
+| `convert.vmware.instance.to.kvm.timeout` | No | `3` | OVF/VDDK import timeout, in hours, for the `ConvertInstanceCommand` virt-v2v path. CBT cutover has its own timeout because it uses CBT-specific agent commands. |
+| `remote.kvm.instance.disks.copy.timeout` | No | `30` | Remote KVM unmanaged import disk-copy timeout, in minutes. It is unrelated to VMware CBT. |
+
+If a CBT command exceeds `vmware.cbt.migration.agent.command.timeout`, the
+migration is marked failed with the agent error details in `last_error`.
+Failed or cancelled migration records can be deleted with cleanup enabled to
+remove CloudStack-owned `cloudstack-cbt/` working files.
+
+## Database Changes
+
+The feature targets the Apache CloudStack 24.0.0 release, so the schema changes
+are delivered through the canonical 4.23.0.0 to 24.0.0 upgrade path:
+
+- `engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql`
+- `engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to2400.java`
+
+### `cloud.vmware_cbt_migration`
+
+One row per migration session.
+
+Important columns:
+
+- `uuid`
+- `zone_id`
+- `account_id`
+- `user_id`
+- `vm_id`
+- `existing_vcenter_id`
+- `source_username`
+- `source_password`
+- `destination_cluster_id`
+- `convert_host_id`
+- `storage_pool_id`
+- `display_name`
+- `host_name`
+- `template_id`
+- `service_offering_id`
+- `guest_os_id`
+- `data_disk_offering_map`
+- `nic_network_map`
+- `nic_ip_address_map`
+- `import_details`
+- `forced`
+- `vcenter`
+- `datacenter`
+- `source_host`
+- `source_cluster`
+- `source_vm_name`
+- `vddk_lib_dir`
+- `vddk_transports`
+- `vddk_thumbprint`
+- `state`
+- `current_step`
+- `last_error`
+- `completed_cycles`
+- `quiet_cycles`
+- `total_changed_bytes`
+- `last_changed_bytes`
+- `last_dirty_rate`
+- `created`
+- `updated`
+- `removed`
+
+`source_password` is annotated with `@Encrypt` in `VmwareCbtMigrationVO`, using
+the same CloudStack encrypted DAO field mechanism used by VMware datacenter
+credentials and VM VNC passwords.
+
+### `cloud.vmware_cbt_migration_disk`
+
+One row per source VMware disk.
+
+Important columns:
+
+- `migration_id`
+- `source_disk_id`
+- `source_disk_device_key`
+- `source_disk_path`
+- `datastore_name`
+- `capacity_bytes`
+- `target_path`
+- `target_format`
+- `change_id`
+- `snapshot_moref`
+- `state`
+
+Disk states:
+
+- `Created`
+- `Prepared`
+- `Syncing`
+- `Ready`
+- `Failed`
+
+### `cloud.vmware_cbt_migration_cycle`
+
+One row per delta cycle.
+
+Important columns:
+
+- `migration_id`
+- `cycle_number`
+- `snapshot_moref`
+- `changed_bytes`
+- `dirty_rate`
+- `duration`
+- `state`
+- `description`
+
+Cycle states:
+
+- `Created`
+- `QueryingChangedAreas`
+- `CopyingChangedBlocks`
+- `Completed`
+- `Failed`
+
+## Migration State Machine
+
+Migration states:
+
+- `Created`
+- `InitialSync`
+- `Replicating`
+- `ReadyForCutover`
+- `CuttingOver`
+- `ReadyForImport`
+- `Completed`
+- `Failed`
+- `Cancelled`
+
+Terminal states:
+
+- `Completed`
+- `Failed`
+- `Cancelled`
+
+Normal successful path:
+
+```text
+Created
+ -> InitialSync
+ -> Replicating
+ -> ReadyForCutover
+ -> CuttingOver
+ -> ReadyForImport
+ -> Completed
+```
+
+`ReadyForImport` is intentionally retryable. If the final `virt-v2v`
+finalization succeeded but CloudStack VM import failed, the next cutover command
+does not rerun the final CBT copy and finalization. It retries the VM import
+against the already finalized disk paths.
+
+## Storage Target And Finalization
+
+`VmwareCbtStorageTarget` maps primary storage to a target writer:
+
+- `NetworkFilesystem`, `Filesystem`, and `SharedMountPoint` use `QCOW2_FILE`.
+- No explicit pool also uses a filesystem QCOW2 target under the default local
+ CBT path.
+- `RBD` maps to `RBD_RAW`. Initial sync writes a raw RBD image with `nbdcopy`
+ through a local NBD bridge when available, otherwise with
+ `qemu-img convert -O raw`. Delta sync writes changed ranges with `qemu-io -f
+ raw`, and cutover finalization is in-place only.
+- `Linstor` maps to `RAW_BLOCK_DEVICE`. The agent pre-creates each target
+ volume at source capacity through the Linstor storage adaptor (qemu cannot
+ create DRBD devices the way it creates RBD images), then the initial sync
+ writes the local DRBD device with `nbdcopy` when available or
+ `qemu-img convert -n -O raw` otherwise. Delta sync patches changed ranges
+ with `qemu-io -f raw` against the device path, and cutover finalization is
+ in-place only, using a plain `` libvirt XML (no qemu-nbd
+ bridges are needed for local block devices).
+ Target names are `cbt--` where `mig8` is the first eight
+ characters of the migration UUID: LINSTOR resource names ("cs-" + name) are
+ capped at 48 characters, so the RBD-style naming with the full migration
+ UUID does not fit; the short marker still guards cleanup on both the
+ management server and the agent. The conversion host must be a LINSTOR
+ satellite connected to the pool; host selection and the preflight storage
+ probe (create a 4 MiB volume, qemu-io write/read on the device, delete)
+ enforce this.
+- Other primary storage types are unsupported.
+
+For QCOW2 file targets, the initial replica is stored under the selected
+primary storage:
+
+```text
+/mnt//cloudstack-cbt//.qcow2
+```
+
+If no storage pool is selected, the fallback path is:
+
+```text
+/var/lib/libvirt/images/cloudstack-cbt//.qcow2
+```
+
+For Ceph/RBD targets, the initial replica is a raw RBD image in the selected
+pool:
+
+```text
+/cloudstack-cbt---
+```
+
+Only the image name is persisted as the migration disk target path. The agent
+reconstructs the qemu RBD URL from the selected storage pool when writing
+initial and delta data. During in-place `virt-v2v` finalization, the agent
+starts a temporary localhost `qemu-nbd` bridge for each RBD image and gives
+`virt-v2v` a libvirt XML disk that points to that local NBD endpoint. This keeps
+RBD credentials and monitor options in the qemu layer and presents a local NBD
+endpoint to the conversion appliance.
+
+The preferred cutover finalization is in-place:
+
+- `virt-v2v-in-place`, if present.
+- `virt-v2v --in-place`, if the installed `virt-v2v` supports it.
+
+If neither in-place method is available, QCOW2 file targets can use regular
+`virt-v2v -o local` only when this global configuration is enabled:
+
+```text
+vmware.cbt.allow.non.inplace.finalization=true
+```
+
+Default:
+
+```text
+false
+```
+
+When fallback is enabled:
+
+- The fallback is only allowed for supported QCOW2 file targets.
+- The agent stages both `TMPDIR` and `virt-v2v` output under the current
+ migration directory on the selected primary storage.
+- The agent validates free space before running fallback finalization.
+- Required free space is conservatively estimated as two times the summed disk
+ capacity.
+- After successful fallback finalization, source replica disks are deleted.
+
+Fallback is never allowed for RBD targets. RBD migrations require
+`virt-v2v-in-place` or `virt-v2v --in-place` support on the selected conversion
+host.
+
+Example fallback output path:
+
+```text
+/mnt//cloudstack-cbt//virt-v2v-output-/-sda
+```
+
+After successful finalization, both in-place and fallback paths are normalized
+to the same final layout used by the OVF/VDDK import flow: the KVM agent moves
+each finalized QCOW2 file to the selected primary storage root and gives it a
+generated UUID filename:
+
+```text
+/mnt//
+```
+
+The agent returns the relocated target paths in the cutover answer. The
+management server persists them in `vmware_cbt_migration_disk.target_path`
+through the normal disk-result update path, so no manual database update or
+schema change is required for this relocation. CloudStack then imports the VM
+using the flat root-level file name, matching the final placement used by
+regular OVF/VDDK migrations.
+
+For RBD targets, successful in-place finalization keeps the raw RBD image in
+place and CloudStack imports it by image name from the selected RBD storage
+pool. No post-finalization file move is performed, and completed migration
+deletion remains record-only.
+
+RBD finalization creates only temporary localhost `qemu-nbd` processes and
+temporary XML/script files on the conversion host. The wrapper installs a shell
+trap to stop those bridge processes and remove the PID files after `virt-v2v`
+finishes or fails. The persistent storage artifact remains the raw RBD image
+whose name contains the `cloudstack-cbt--` marker until
+CloudStack imports it or cleanup removes it for a failed/cancelled migration.
+
+CloudStack imports finalized CBT disks with the KVM `virtio` root disk
+controller. The conversion host must have `virtio-win` available for Windows
+guests so that `virt-v2v` can enable the matching boot driver before the VM is
+started under KVM. This keeps the persisted `rootDiskController` detail aligned
+with the controller model used by the converted guest.
+
+## Ceph/RBD Conversion Host Requirements
+
+Ceph/RBD support relies on the same KVM host plumbing that normal CloudStack RBD
+primary storage uses, plus the CBT-specific VMware conversion tools. The
+selected conversion host must be able to access the destination RBD pool from
+both Java/libvirt storage code and the qemu command-line tools used by the CBT
+wrappers.
+
+Required conversion-host capabilities:
+
+- CloudStack KVM agent with the normal RBD primary-storage dependencies.
+- The selected KVM/conversion host must already be able to use the destination
+ CloudStack RBD primary storage pool.
+- `librados` and `librbd` client libraries compatible with the Ceph cluster.
+- Java RADOS/RBD bindings used by CloudStack's RBD storage adaptor.
+- qemu RBD block driver support so `qemu-img` and `qemu-io` can open
+ `rbd:/:...` URLs.
+- VDDK and `nbdkit-vddk` for VMware source reads.
+- `virt-v2v-in-place` or `virt-v2v --in-place` for RBD cutover finalization.
+- `virtio-win` or equivalent VirtIO driver media/packages for Windows guests,
+ same as the existing VMware import requirements.
+
+CloudStack RBD primary-storage configuration must be valid on the selected
+KVM/conversion host. For CBT-to-RBD, CloudStack uses the existing KVM
+storage-pool metadata to build qemu RBD URLs, including monitor addresses,
+authentication mode, Ceph user, and secret. The CBT qemu path therefore does not
+rely on `/etc/ceph/ceph.conf` or a local keyring as its primary credential source
+when CloudStack supplies explicit RBD URL options.
+
+A local Ceph configuration and keyring can still be useful for operator
+diagnostics and site-local tooling, for example running `ceph -s`,
+`rbd ls `, or similar commands without manually passing monitor and
+authentication options each time. Sites may choose to install those files for
+operational convenience, but they should not be documented as the required
+credential source for CBT RBD writes.
+
+The Ceph client packages on every KVM/conversion host should match the Ceph
+cluster major release, or at least be explicitly supported by that cluster
+release. Avoid relying on the distribution's stock Ceph packages if they are
+older than the deployed Ceph cluster. Ancient `librbd`, `librados`, qemu RBD
+block-driver, or Java binding packages can fail in ways that look like CBT
+errors even though the actual problem is a client/cluster compatibility mismatch.
+
+Practical guidance:
+
+- If the Ceph cluster is deployed from upstream Ceph packages, install the
+ corresponding upstream Ceph client repository on all KVM/conversion hosts.
+- If the Ceph cluster is deployed from a vendor repository, use that vendor's
+ matching client packages on the KVM/conversion hosts.
+- Keep `ceph-common`, `librados`, `librbd`, qemu RBD block-driver packages, and
+ Java RADOS/RBD bindings aligned as a set. Do not mix a modern Ceph cluster
+ with old OS-default client libraries unless the Ceph vendor explicitly
+ supports that combination.
+- Verify the KVM host can already use the selected RBD primary storage for
+ ordinary CloudStack VM volumes before enabling CBT migrations to the same
+ pool.
+- Verify that qemu tools can see RBD support. Package names vary by OS, but on
+ many EL-like distributions this is provided by a qemu RBD block-driver package
+ such as `qemu-kvm-block-rbd`; on Debian/Ubuntu-like systems it is commonly
+ part of qemu block extras.
+
+CBT preflight and `startVmwareCbtMigration` actively probe RBD access when the
+selected destination pool is RBD. The management server sends the selected
+conversion host a small probe command using a temporary image named
+`cloudstack-cbt-probe-`. The KVM agent builds the qemu RBD URL from the
+existing CloudStack storage-pool metadata, runs `qemu-img create`, writes and
+reads a 4 KiB pattern with `qemu-io`, and removes the temporary image through
+CloudStack's existing RBD storage adaptor. This catches qemu RBD driver issues,
+Ceph monitor/authentication problems, and Java RADOS/RBD cleanup failures before
+the long-running VMware copy starts.
+
+Useful host checks:
+
+```bash
+ceph -s
+ceph osd lspools
+rbd ls
+qemu-img --help | grep -i rbd
+qemu-io --help | grep -i rbd
+```
+
+Useful package/version checks:
+
+```bash
+ceph --version
+rbd --version
+ldconfig -p | egrep 'librbd|librados'
+java -version
+```
+
+Useful CloudStack database check after the host reconnects:
+
+```sql
+SELECT h.id, h.name, hd.name, hd.value
+FROM cloud.host h
+JOIN cloud.host_details hd ON hd.host_id = h.id
+WHERE h.name = ''
+ AND hd.name IN (
+ 'host.vddk.blockcopy.support',
+ 'host.vddk.blockcopy.inplace.finalization.support',
+ 'host.vddk.blockcopy.rbd.support',
+ 'host.vddk.support',
+ 'host.vddk.version',
+ 'host.virtv2v.inplace.version',
+ 'vddk.lib.dir'
+ )
+ORDER BY hd.name;
+```
+
+An RBD-capable conversion host should already report VMware CBT capability as
+`true` before a migration starts, and the selected destination storage pool must
+be the RBD pool that the host can access. If these host details are stale after
+installing packages, restart `cloudstack-agent`, ensure the management server is
+running, and wait for host details to be refreshed.
+
+## Host Capability Reporting
+
+The agent checks these on startup/ready and sends them as host details. Management then persists them into `cloud.host_details`.
+
+| Host detail | How agent checks it |
+| --- | --- |
+| `host.virtv2v.version` | Runs `virt-v2v --version`. On Ubuntu/Debian it also checks `dpkg -l nbdkit`. Version is parsed from `virt-v2v --version`. |
+| `vddk.lib.dir` | Reads `vddk.lib.dir` from `agent.properties`; if blank/invalid, auto-detects with `find / -type d -name 'vmware-vix-disklib-distrib' 2>/dev/null \| head -n 1`. Valid means `/lib64/libvixDiskLib.so*` exists. |
+| `host.vddk.version` | Runs `nbdkit vddk --dump-plugin libdir=` and parses `vddk_library_version=`. |
+| `host.vddk.support` | True only if `virt-v2v`/`nbdkit` support is present, VDDK dir is valid, and `nbdkit-vddk-plugin` can load VDDK via `nbdkit vddk --dump-plugin libdir=`. |
+| `host.qemu.img.version` | Runs `qemu-img --version`, parses first non-empty line. |
+| `host.qemu.nbd.version` | Runs `qemu-nbd --version`, parses first non-empty line. |
+| `host.qemu.io.version` | Runs `qemu-io --version`, parses first non-empty line. |
+| `host.vddk.blockcopy.support` | True only if `host.vddk.support` is true and `qemu-img --version`, `qemu-nbd --version`, and `qemu-io --version` all exit `0`. |
+| `host.vddk.blockcopy.inplace.finalization.support` | True only if CBT support is true and either `virt-v2v-in-place --version` works, or `virt-v2v --help 2>&1 \| grep -q -- '--in-place'` succeeds. |
+| `host.virtv2v.inplace.version` | If `virt-v2v-in-place` exists, parses `virt-v2v-in-place --version`. If only `virt-v2v --in-place` exists, reports the normal `virt-v2v` version. |
+| `host.vddk.blockcopy.rbd.support` | True only if in-place finalization support is true and `qemu-img --help` advertises the `rbd` block driver. |
+
+Important nuance: `host.vddk.blockcopy.rbd.support` is only the coarse host capability. For an actual selected RBD pool, preflight/start also runs an active probe: create a temporary `cloudstack-cbt-probe-` RBD image, write/read 4 KiB using `qemu-io`, then delete it through CloudStack's RBD storage adapter. That catches Ceph auth, monitor, qemu RBD, librados/librbd, and Java binding problems.
+
+Windows source VMs also require `virtio-win` on the selected KVM conversion
+host. CBT preflight and `startVmwareCbtMigration` use the same
+`CheckConvertInstanceCommand` path as OVF/VDDK import with Windows guest
+conversion enabled, so a missing `virtio-win` package is reported before the
+initial sync starts.
+
+VDDK detection uses either agent configuration or auto-detection. The agent
+validates the VDDK library by checking that `lib64/libvixDiskLib.so*` exists
+and that `nbdkit vddk --dump-plugin libdir=` reports a library version.
+
+## VDDK Details
+
+VDDK options can be provided as API `details` at migration start:
+
+- `vddk.lib.dir`
+- `vddk.transports`
+- `vddk.thumbprint`
+
+If a value is not provided by the API, the agent uses its configured or detected
+value. If `vddk.thumbprint` is not provided, the agent attempts to fetch the
+vCenter SHA1 certificate thumbprint with `openssl`.
+
+The implementation does not hard-code one VDDK version. The installed VDDK
+build must be compatible with the host libraries and pass the agent capability
+check.
+
+## Credential Handling
+
+There are two source credential modes:
+
+- Existing registered vCenter: credentials are read from the registered
+ `vmware_data_center` record.
+- External vCenter: credentials are supplied to start/preflight and stored on
+ the migration record so later UI sync/cutover actions can run without asking
+ again.
+
+External credential behavior:
+
+- `source_password` is stored through the CloudStack encrypted DAO field
+ mechanism via `@Encrypt`.
+- Start, preflight, sync, and cutover commands that can carry credentials are
+ marked `requestHasSensitiveInfo=true`.
+- API responses are marked `responseHasSensitiveInfo=false`.
+- Agent-side VDDK commands pass the password through a temporary password file
+ and `nbdkit` `password=+`, not by embedding the clear-text password in
+ the command line.
+- Management sanitizes returned error messages by replacing the resolved source
+ password with `******` before storing `last_error` or cycle descriptions.
+- Stored external credentials are cleared when the migration completes,
+ when it is cancelled, and before deletion.
+
+The implementation avoids logging clear-text passwords. As with other
+CloudStack secret handling, this also depends on callers not placing secrets in
+unrelated free-form log messages.
+
+## Compute Offering And Preflight Validation
+
+CBT migration uses the same eventual import path as the existing OVF/VDDK VM
+import code. The final CloudStack VM sizing still comes from the selected
+service offering and any dynamic offering details.
+
+The CBT-specific difference is timing: CBT validates obvious sizing problems
+early, before the long initial full sync begins. This is meant to avoid spending
+hours copying disks only to fail final VM import because the selected offering
+is too small.
+
+The source VM sizing captured during preflight/start includes:
+
+- `sourcecpunumber`
+- `sourcecpuspeed`
+- `sourcememory`
+
+Validation rule:
+
+- If the source value and requested value are both known and greater than zero,
+ the requested value must be greater than or equal to the source value.
+- Unknown or zero source values are not used to reject the offering.
+
+Validated fields:
+
+- CPU number
+- CPU speed
+- Memory
+
+Fixed offerings:
+
+- CPU number comes from `service_offering.cpu`.
+- CPU speed comes from `service_offering.speed`.
+- Memory comes from `service_offering.ram_size`.
+
+Dynamic/custom offerings:
+
+- Caller details can provide `cpuNumber`, `cpuSpeed`, and `memory`.
+- If caller details are missing, offering minimums such as `mincpunumber` and
+ `minmemory` can be used where applicable.
+- This mirrors the shared import behavior in
+ `UnmanagedVMsManagerImpl.addServiceOfferingDetailsToParams`.
+
+Example custom offering details:
+
+```text
+details[0].key=cpuNumber
+details[0].value=4
+details[1].key=cpuSpeed
+details[1].value=2100
+details[2].key=memory
+details[2].value=4096
+```
+
+This early validation is not intended to be stricter than OVF/VDDK import. It
+is intended to fail earlier for the same class of sizing mismatch.
+
+## UI Changes
+
+The CBT UI is integrated into the Import/Export Instances area.
+
+Implemented UI behavior:
+
+- Start migration uses the async `startVmwareCbtMigration` job.
+- Sync delta uses the async `syncVmwareCbtMigration` job.
+- Cutover uses the async `cutoverVmwareCbtMigration` job.
+- Each async action uses CloudStack job polling.
+- The CBT migration table also calls `listVmwareCbtMigrations` as the detailed
+ status source.
+- The table auto-refreshes while the CBT tab is active and any migration is in
+ an active state or a CBT action job is still running.
+
+Active states for UI refresh:
+
+- `Created`
+- `InitialSync`
+- `Replicating`
+- `CuttingOver`
+
+The table exposes:
+
+- Migration state.
+- Current step.
+- Completed cycles.
+- Quiet cycles.
+- Last changed bytes.
+- Last dirty rate.
+- Total changed bytes.
+- Last error.
+- Per-disk target path, target format, change ID, snapshot MOR, and state.
+- Per-cycle state, changed bytes, dirty rate, duration, description, created,
+ and updated timestamps.
+
+Initial sync progress is currently coarse. While `qemu-img convert` runs on the
+agent, management can show the migration state, current step, target path, and
+disk state, but not a continuously updated percentage. Delta syncs have better
+post-cycle metrics because changed bytes and dirty rate are returned by the
+cycle result.
+
+## Failure And Retry Semantics
+
+Initial full sync failure:
+
+- Migration moves to `Failed`.
+- Created or syncing disks are marked `Failed`.
+- Last error is stored on the migration.
+- Delete with cleanup can remove the migration directory.
+
+Delta sync failure:
+
+- The cycle is marked `Failed`.
+- The migration moves to `Failed`.
+- Last error and cycle description are stored.
+- VMware cycle snapshot removal is still attempted.
+
+Cutover finalization failure:
+
+- Migration moves to `Failed`.
+- The source VM remains under operator control in vCenter.
+- Delete with cleanup can remove replicated/finalized target directories if the
+ paths are under the migration directory.
+
+Import failure after successful finalization:
+
+- Migration remains `ReadyForImport`.
+- Target disk paths point at the finalized disk files.
+- Retrying cutover retries the import step.
+
+Cancel:
+
+- Best-effort cleanup.
+- State becomes `Cancelled`.
+- Stored external credentials are cleared.
+
+Delete:
+
+- `Failed`, `Cancelled`, or `Completed`.
+- Optional cleanup, default `true`, applies only to `Failed` and `Cancelled`.
+- `Completed` deletion is record-only and intentionally preserves the imported
+ VM and all destination storage artifacts.
+- Child rows and parent row are removed.
+
+## Security And Logging Considerations
+
+Important security choices:
+
+- Credential-bearing commands are marked sensitive.
+- External vCenter password storage uses encrypted DAO fields.
+- Passwords are passed to VDDK through temp files instead of clear-text command
+ arguments.
+- Password temp files are owner-readable and owner-writable where POSIX
+ permissions are supported.
+- Returned failure messages are sanitized against the active source password
+ before persistence.
+- CBT delta and cutover agent wrappers include the last useful command output
+ line in returned failure details when available. This keeps management server
+ and UI errors actionable for `qemu-nbd`, `qemu-io`, `nbdkit`, and `virt-v2v`
+ failures without logging or returning clear-text passwords.
+
+Operational caution:
+
+- `source_username` is not encrypted.
+- vCenter host, datacenter, VM name, datastore names, disk paths, change IDs,
+ and snapshot MORs are not treated as secrets and can appear in API responses
+ and logs.
+- Operators should still avoid manually pasting credentials into log-visible
+ free-form fields.
+
+## Testing Strategy
+
+Relevant unit coverage includes:
+
+- `VmwareCbtMigrationOfferingValidationTest`
+- `VmwareCbtMigrationDeletePolicyTest`
+- `VmwareCbtStorageTargetTest`
+- `VmwareCbtMigrationCutoverPolicyTest`
+- `LibvirtVmwareCbtSyncCommandWrapperTest`
+- `VmwareCbtSyncPlanTest`
+- `LibvirtVmwareCbtCutoverCommandWrapperTest`
+- `LibvirtVmwareCbtCleanupCommandWrapperTest`
+- `LibvirtVmwareCbtRbdProbeCommandWrapperTest`
+- `LibvirtStoragePoolTest`
+
+The tests cover:
+
+- Fixed offering validation.
+- Dynamic/custom offering detail handling.
+- Storage target classification.
+- Non-in-place finalization gating.
+- Cutover policy decisions.
+- Delta copy script planning.
+- Chunked changed-range reads from the nbdkit Unix socket.
+- RBD probe create/write/read/delete command planning.
+- RBD finalization through temporary localhost `qemu-nbd` bridges.
+- Fallback `virt-v2v` output path handling.
+- Cleanup scoping to migration directories.
+- Cleanup scoping to migration-owned RBD image names.
+- Nested relative KVM volume path resolution under filesystem pools.
+
+## Operational Verification
+
+Useful host capability check:
+
+```bash
+cmk list hosts name= details=all | egrep \
+'host.vddk.blockcopy.support|host.vddk.blockcopy.inplace.finalization.support|host.vddk.blockcopy.rbd.support|host.vddk.support|host.vddk.version|host.virtv2v.version|host.virtv2v.inplace.version|host.qemu.img.version|host.qemu.nbd.version|host.qemu.io.version|vddk.lib.dir'
+```
+
+Useful VDDK plugin check on the KVM host:
+
+```bash
+nbdkit vddk --dump-plugin libdir=/opt/vmware-vix-disklib-distrib | \
+ egrep 'vddk_library_version|vddk_dll|vddk_transport_modes'
+```
+
+Useful process check during initial sync:
+
+```bash
+pgrep -af 'nbdkit|qemu-img|qemu-nbd|nbdcopy'
+```
+
+Useful target disk inspection:
+
+```bash
+qemu-img info /mnt//cloudstack-cbt//.qcow2
+```
+
+## References
+
+- [Apache CloudStack: Importing VMware VMs into KVM](https://docs.cloudstack.apache.org/en/latest/adminguide/virtual_machines/importing_vmware_vms_into_kvm.html)
+- [Broadcom vSphere `QueryChangedDiskAreas` API](https://developer.broadcom.com/xapis/virtual-infrastructure-json-api/latest/sdk/vim25/release/VirtualMachine/moId/QueryChangedDiskAreas/post/)
+- [Broadcom Virtual Disk API disk operations](https://developer.broadcom.com/xapis/virtual-disk-api/latest/vddkFunctions.6.4.html)
+- [Broadcom VDDK CBT notes and limitations](https://developer.broadcom.com/xapis/virtual-disk-api/latest/vddkBkupVadp.9.5.html)
+- [nbdkit VDDK plugin](https://libguestfs.org/nbdkit-vddk-plugin.1.html)
diff --git a/docs/vmware-cbt/architecture.md b/docs/vmware-cbt/architecture.md
new file mode 100644
index 000000000000..299a0d769afe
--- /dev/null
+++ b/docs/vmware-cbt/architecture.md
@@ -0,0 +1,266 @@
+
+
+# VMware CBT migration architecture
+
+This document describes the implemented Apache CloudStack architecture for
+warm VMware-to-KVM migration based on VMware Changed Block Tracking (CBT).
+Detailed API, database, configuration, and operational information is in
+[`README.md`](README.md). KVM host preparation is documented in
+[`../vmware-cbt-migration.md`](../vmware-cbt-migration.md).
+
+## Design invariant
+
+The KVM-side replica remains byte-compatible with the VMware source disk until
+the final CBT cycle has completed. Raw VMware CBT ranges are never applied to a
+disk after guest conversion has changed its layout.
+
+The migration therefore has two distinct phases:
+
+1. Replication: copy the initial source-equivalent baseline and apply CBT delta
+ ranges to it.
+2. Finalization: after the source VM is powered off, apply one final delta and
+ run `virt-v2v` locally against the current KVM-side replica.
+
+This avoids a second full VMware read during cutover. CloudStack does not power
+off the source VM; shutdown remains an explicit operator action.
+
+## Components
+
+### Management server
+
+The API commands are:
+
+- `checkVmwareCbtMigrationPrerequisites`
+- `startVmwareCbtMigration`
+- `listVmwareCbtMigrations`
+- `syncVmwareCbtMigration`
+- `cutoverVmwareCbtMigration`
+- `cancelVmwareCbtMigration`
+- `deleteVmwareCbtMigration`
+
+`VmwareCbtMigrationManagerImpl` owns migration orchestration, host and storage
+validation, state transitions, cutover policy, agent command dispatch, import,
+and cleanup. `VmwareCbtMigrationServiceImpl` performs VMware inventory,
+snapshot, power-state, CBT enablement, and changed-area operations.
+
+`VmwareCbtStorageTarget` classifies the selected primary storage and determines
+the target representation and finalization constraints.
+
+### Persistence
+
+The feature persists its state in three CloudStack tables:
+
+- `cloud.vmware_cbt_migration`: one row per migration session.
+- `cloud.vmware_cbt_migration_disk`: source and target metadata for each disk,
+ including the current per-disk VMware `changeId`.
+- `cloud.vmware_cbt_migration_cycle`: metrics and status for each delta cycle.
+
+The schema and configuration entries are delivered through the standard
+4.23.0.0-to-24.0.0 database upgrade path:
+
+- `engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql`
+- `engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42300to2400.java`
+
+### KVM agent
+
+The management server sends these commands to the selected conversion host:
+
+- `VmwareCbtPrepareCommand`: create or resolve target disks and perform the
+ initial VDDK full copy.
+- `VmwareCbtSyncCommand`: read and apply VMware CBT ranges.
+- `VmwareCbtCutoverCommand`: finalize the current replica with `virt-v2v`.
+- `VmwareCbtCleanupCommand`: terminate migration-owned processes and remove
+ migration-owned temporary artifacts.
+- `VmwareCbtRbdProbeCommand`: actively validate RBD create/write/read/delete
+ access for the selected pool.
+
+The corresponding KVM wrappers are
+`LibvirtVmwareCbtPrepareCommandWrapper`,
+`LibvirtVmwareCbtSyncCommandWrapper`,
+`LibvirtVmwareCbtCutoverCommandWrapper`,
+`LibvirtVmwareCbtCleanupCommandWrapper`, and
+`LibvirtVmwareCbtRbdProbeCommandWrapper`.
+
+### User interface
+
+The workflow is integrated into Import/Export Instances. The UI starts async
+operations through the API and uses `listVmwareCbtMigrations` as the detailed
+source for migration, disk, and cycle state.
+
+## End-to-end flow
+
+```text
+VMware source VM
+ |
+ | preflight and source discovery
+ v
+baseline VMware snapshot
+ |
+ | nbdkit-vddk full logical disk read
+ v
+source-equivalent replica on KVM primary storage
+ |
+ | repeated VMware snapshots + QueryChangedDiskAreas
+ | changed ranges copied through VDDK and patched in place
+ v
+ReadyForCutover
+ |
+ | operator powers off the VMware VM
+ | final CBT delta
+ | local virt-v2v finalization
+ v
+CloudStack KVM import
+```
+
+### Preflight
+
+Preflight resolves the source, destination cluster, conversion host, and
+primary storage. It validates source CBT state, source disks, snapshots,
+storage support, host block-copy and in-place-finalization capabilities, RBD or
+LINSTOR access where applicable, Windows conversion dependencies, and optional
+compute-offering sizing.
+
+The selected conversion host reports capability through host details. The
+principal gates are:
+
+- `host.vddk.blockcopy.support`
+- `host.vddk.blockcopy.inplace.finalization.support`
+- `host.vddk.blockcopy.rbd.support`
+- `vddk.lib.dir`
+- `host.vddk.version`
+- qemu and `virt-v2v` version details
+
+### Initial synchronization
+
+Start creates the migration and disk records, enables CBT when required,
+creates a baseline VMware snapshot, prepares destination targets, and dispatches
+`VmwareCbtPrepareCommand`.
+
+The KVM agent exposes each snapshot disk with `nbdkit-vddk`. It uses
+`qemu-img convert` for the general copy path and can use `nbdcopy` as an
+optional accelerator for supported pre-created or NBD-bridged targets. The
+result remains source-equivalent; `virt-v2v` is not run during initial sync.
+
+After a successful copy, CloudStack records the baseline per-disk `changeId`
+and removes the baseline snapshot when possible.
+
+### Delta synchronization
+
+Each synchronization cycle:
+
+1. Creates a VMware snapshot.
+2. Calls VMware `QueryChangedDiskAreas` from the previous per-disk `changeId`.
+3. Persists the changed ranges and cycle state.
+4. Opens the snapshot disk through `nbdkit-vddk` on the KVM host.
+5. Copies only the changed ranges and writes them at their original byte
+ offsets with `qemu-io`.
+6. Records changed bytes, duration, dirty rate, and the new `changeId`.
+7. Removes the cycle snapshot when possible.
+
+`VmwareCbtMigrationCutoverPolicy` evaluates the configured minimum, maximum,
+and quiet-cycle thresholds and moves the migration to `ReadyForCutover` when
+the policy boundary is reached.
+
+### Cutover
+
+Cutover is accepted only when the source VM is powered off. CloudStack runs one
+final CBT cycle, then finalizes the current KVM-side replica with one of these
+paths:
+
+- `virt-v2v-in-place`;
+- `virt-v2v --in-place`; or
+- regular `virt-v2v -o local` for QCOW2 file targets only, when the explicit
+ non-in-place fallback configuration is enabled.
+
+After finalization, CloudStack imports the disks through the shared external
+KVM import path. A successful import sets the migration to `Completed`. If
+finalization succeeds but import fails, the migration remains `ReadyForImport`
+and a retry repeats only the import stage.
+
+## Storage targets
+
+| Primary storage | Replica | Initial copy | Delta writes | Finalization |
+| --- | --- | --- | --- | --- |
+| NFS, filesystem, shared mount | QCOW2 file | `qemu-img` | `qemu-io -f qcow2` | In-place preferred; explicitly gated local fallback supported |
+| Ceph/RBD | Raw RBD image | Direct qemu RBD path or `nbdcopy` through a local NBD bridge | `qemu-io -f raw` | In-place only |
+| LINSTOR | Pre-created raw DRBD block device | `nbdcopy` when available, otherwise `qemu-img` | `qemu-io -f raw` | In-place only |
+
+For filesystem targets, temporary replicas are stored under a migration-owned
+`cloudstack-cbt//` directory. RBD and LINSTOR resources use
+CloudStack-owned migration markers. Cleanup is restricted to these owned paths
+and resource names.
+
+## State model
+
+Migration states:
+
+```text
+Created
+ -> InitialSync
+ -> Replicating
+ -> ReadyForCutover
+ -> CuttingOver
+ -> ReadyForImport
+ -> Completed
+```
+
+`Failed` and `Cancelled` are terminal states. `ReadyForImport` is deliberately
+retryable.
+
+Disk states are `Created`, `Prepared`, `Syncing`, `Ready`, and `Failed`.
+Cycle states are `Created`, `QueryingChangedAreas`, `CopyingChangedBlocks`,
+`Completed`, and `Failed`.
+
+## Concurrency and cleanup
+
+Sync and cutover commands are serialized by migration ID. Cancellation sends a
+best-effort agent cleanup command before marking a non-terminal migration as
+cancelled. Failed and cancelled migrations can delete their migration-owned
+temporary replicas when cleanup is requested.
+
+Deleting a completed migration is record-only. It removes CBT bookkeeping but
+does not delete the imported VM, its CloudStack volumes, or finalized primary
+storage artifacts.
+
+VMware snapshots are removed after initial and delta attempts when possible.
+Snapshot identifiers and cleanup errors remain visible in migration state so
+operators can identify VMware-side cleanup work.
+
+## Credential handling
+
+Existing registered vCenter credentials are resolved from CloudStack's VMware
+datacenter record. External-source credentials are accepted by sensitive API
+commands and the password is stored through CloudStack's encrypted DAO field
+handling until the migration no longer needs it.
+
+On the KVM host, the password is written to a temporary owner-only file and
+passed to `nbdkit` with `password=+`. It is not placed directly in the
+process command line. Returned errors are sanitized before persistence.
+
+## Operational boundaries
+
+- The operator is responsible for shutting down the source VM before cutover.
+- Initial baseline transfer size depends on the extents exposed by VMware VDDK
+ and the source datastore; CBT optimization applies to subsequent cycles.
+- Invalidated VMware CBT state requires a new baseline migration.
+- RBD and LINSTOR require in-place finalization and active backend access from
+ the selected conversion host.
+- Windows guests require a usable `virtio-win` driver bundle on that host.
+- The source VMware VM remains authoritative until cutover succeeds.
diff --git a/docs/vmware-linstor-migration.md b/docs/vmware-linstor-migration.md
new file mode 100644
index 000000000000..bf3f8b69b20e
--- /dev/null
+++ b/docs/vmware-linstor-migration.md
@@ -0,0 +1,238 @@
+
+
+# Linstor primary storage as a destination for VMware-to-KVM migration
+
+This document describes the Linstor-destination portion of the VMware-to-KVM
+migration work, in enough detail for LINSTOR/DRBD maintainers to review the
+design choices and advise on Linstor-side behavior.
+
+## Summary
+
+This change lets Linstor (DRBD) primary storage be the **destination** for
+VMware-to-KVM instance imports, across all three migration modes CloudStack
+offers for Ceph/RBD:
+
+- **Cold, staged** — convert with virt-v2v to a temporary NFS location, then
+ copy the finalized disks into Linstor.
+- **Cold, direct (VDDK)** — read each disk over nbdkit/VDDK straight into the
+ DRBD device and finalize it in place, with no NFS staging.
+- **Warm (CBT)** — replicate the running VM with an initial full sync plus
+ incremental changed-block cycles into the DRBD device, then finalize in
+ place at cutover.
+
+Before this change Linstor was blocked everywhere in the import/conversion
+path by NFS-only (and, more recently, RBD-only) assumptions. The work
+generalizes those code paths rather than special-casing Linstor: a new
+`RAW_BLOCK_DEVICE` target type covers "write RAW into a host-local block
+device provided by the storage adaptor", which also lays the groundwork for
+other block backends (PowerFlex, FiberChannel, StorPool) later.
+
+## Background and motivation
+
+CloudStack can import VMware VMs onto KVM by converting them with virt-v2v
+(optionally reading disks over the VMware VDDK). The converted disks have to
+land on KVM primary storage. That path historically supported only
+filesystem-style pools (NFS/Filesystem/SharedMountPoint); the companion
+commits in this series add Ceph/RBD as a destination and a full VMware
+**CBT warm-migration** framework. Linstor — a very common CloudStack block
+backend — was still unsupported as a conversion destination.
+
+Linstor differs from RBD in ways that shaped the design:
+
+- A Linstor volume is a **local block device** (`/dev/drbd/by-res//0`),
+ which is the universal abstraction the whole toolchain (qemu-img, qemu-io,
+ virt-v2v-in-place) already consumes — so finalization is actually *simpler*
+ than RBD (no qemu-nbd credential bridge needed; a plain
+ `` works).
+- Unlike RBD, `qemu-img convert` **cannot create** the device — the resource
+ must be pre-created at the source capacity through the storage adaptor,
+ then written with `qemu-img convert -n` (no-create).
+- LINSTOR resource names are capped at ~48 characters and cannot be renamed,
+ so the RBD-style naming carrying a full migration UUID does not fit; short,
+ deterministic names are used and imported volumes keep the name as their
+ recorded volume path.
+
+## What this change does
+
+### New generic block-device target type
+`VmwareCbtTargetStorageType.RAW_BLOCK_DEVICE`, alongside the existing
+`QCOW2_FILE` and `RBD_RAW`. Linstor pools classify to it and require in-place
+finalization (the qcow2 fallback cannot write to a device).
+
+### Server-side (orchestration)
+- Linstor added to the staged-conversion destination pool types, the direct
+ VDDK-convert allow-list, and the CBT-compatible pool types, for both
+ explicit and implicit destination-pool selection.
+- Conversion/import host selection validates that the host is a LINSTOR
+ satellite connected to the pool (in addition to the VDDK / in-place
+ virt-v2v capability checks), for auto-selection and explicit host IDs.
+- `CheckConvertInstanceCommand` carries a new in-place-finalization capability
+ check so unsupported hosts fail fast with a clear message.
+- The powered-off, non-cloned source requirement (already enforced for direct
+ RBD) now applies to any direct block-storage import.
+
+### KVM agent-side (data plane)
+- **Staged**: the converted-disk move already dispatches per pool type to
+ `LinstorStorageAdaptor.copyPhysicalDisk` (qemu-img into the DRBD device);
+ converted-disk metadata for Linstor reports the pool UUID and volume name
+ instead of parsing an NFS mount.
+- **Direct VDDK**: pre-creates each resource at source capacity via the
+ adaptor, then `nbdkit vddk --run 'qemu-img convert -n ... '`, and
+ finalizes with `virt-v2v-in-place` fed a `