diff --git a/.gitattributes b/.gitattributes index 1b06f3ebf53c..73d5e4d0d6ed 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ .github/workflows/*.lock.yml linguist-generated=true merge=ours +*.sh text eol=lf diff --git a/.gitignore b/.gitignore index abaef83e4555..61ec22f43741 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,7 @@ plugins/network-elements/juniper-contrail/logs/ replace.properties.override replace.properties.tmp scripts/.pydevproject +settings.toml scripts/vm/hypervisor/xenserver/vhd-util systemvm/.pydevproject target/ 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/host/Host.java b/api/src/main/java/com/cloud/host/Host.java index b52348201516..9c13dd305b08 100644 --- a/api/src/main/java/com/cloud/host/Host.java +++ b/api/src/main/java/com/cloud/host/Host.java @@ -60,6 +60,9 @@ 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_VMWARE_CBT_SUPPORT = "host.vmware.cbt.support"; + String HOST_QEMU_IMG_VERSION = "host.qemu.img.version"; + String HOST_QEMU_NBD_VERSION = "host.qemu.nbd.version"; String HOST_OVFTOOL_VERSION = "host.ovftool.version"; String HOST_VIRTV2V_VERSION = "host.virtv2v.version"; String HOST_SSH_PORT = "host.ssh.port"; 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 4d4ead277e5d..9fcbf6da936b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -635,6 +635,30 @@ 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_DISK_LIST = "targetdisklist"; + 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"; 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..872557c9d90a --- /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.22.1") +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/CutoverVmwareCbtMigrationCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CutoverVmwareCbtMigrationCmd.java new file mode 100644 index 000000000000..127af1eda291 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CutoverVmwareCbtMigrationCmd.java @@ -0,0 +1,89 @@ +// 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 = "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.22.1") +public class CutoverVmwareCbtMigrationCmd 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.USERNAME, type = CommandType.STRING, + description = "the username for the source vCenter, required for sessions not linked to an existing vCenter") + private String username; + + @Parameter(name = ApiConstants.PASSWORD, type = CommandType.STRING, + description = "the password for the source vCenter, required for sessions not linked to an existing vCenter") + private String password; + + public Long getId() { + return id; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + @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/ImportVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java index db7dcc3fb44f..b547346a8011 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 ///////////////////// ///////////////////////////////////////////////////// @@ -186,6 +204,13 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd { "This parameter is mutually exclusive with " + ApiConstants.FORCE_MS_TO_IMPORT_VM_FILES + ".") 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 +292,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..577afef1ca43 --- /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.22.1") +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/RegisterVmwareCbtMigrationTargetCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/RegisterVmwareCbtMigrationTargetCmd.java new file mode 100644 index 000000000000..17a4865ad535 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/RegisterVmwareCbtMigrationTargetCmd.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.command.admin.vm; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +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.VmwareCbtMigrationResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.VmwareCbtMigrationManager; +import org.apache.cloudstack.vm.VmwareCbtTargetDiskInfo; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; + +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.user.Account; + +@APICommand(name = "registerVmwareCbtMigrationTarget", + description = "Register KVM target disk paths produced by the initial full sync for a VMware CBT migration", + responseObject = VmwareCbtMigrationResponse.class, + responseView = ResponseObject.ResponseView.Full, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin}, + since = "4.22.1") +public class RegisterVmwareCbtMigrationTargetCmd 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.TARGET_DISK_LIST, type = CommandType.MAP, required = true, + description = "source disk to KVM target disk mapping. Example: targetdisklist[0].sourcediskid=scsi0:0&" + + "targetdisklist[0].targetpath=/var/lib/libvirt/images/vm-disk.qcow2&targetdisklist[0].targetformat=qcow2&" + + "targetdisklist[0].changeid=") + private Map targetDiskList; + + public Long getId() { + return id; + } + + @SuppressWarnings("unchecked") + public List getTargetDisks() { + if (MapUtils.isEmpty(targetDiskList)) { + throw new InvalidParameterValueException("Target disk list cannot be empty"); + } + + List targetDisks = new ArrayList<>(); + for (Map entry : (Collection>)targetDiskList.values()) { + String sourceDiskId = StringUtils.trimToNull(entry.get(ApiConstants.SOURCE_DISK_ID)); + String targetPath = StringUtils.trimToNull(entry.get(ApiConstants.TARGET_PATH)); + String targetFormat = StringUtils.trimToNull(entry.get(ApiConstants.TARGET_FORMAT)); + String changeId = StringUtils.trimToNull(entry.get(ApiConstants.CHANGE_ID)); + String snapshotMor = StringUtils.trimToNull(entry.get(ApiConstants.SNAPSHOT_MOR)); + + if (StringUtils.isAnyBlank(sourceDiskId, targetPath)) { + throw new InvalidParameterValueException(String.format("%s and %s are required for each target disk", + ApiConstants.SOURCE_DISK_ID, ApiConstants.TARGET_PATH)); + } + targetDisks.add(new VmwareCbtTargetDiskInfo(sourceDiskId, targetPath, targetFormat, changeId, snapshotMor)); + } + return targetDisks; + } + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, + ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + VmwareCbtMigrationResponse response = vmwareCbtMigrationManager.registerVmwareCbtMigrationTarget(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/StartVmwareCbtMigrationCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/StartVmwareCbtMigrationCmd.java new file mode 100644 index 000000000000..a70aefbb4808 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/StartVmwareCbtMigrationCmd.java @@ -0,0 +1,203 @@ +// 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.StoragePoolResponse; +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 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 = "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.22.1") +public class StartVmwareCbtMigrationCmd 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.DISPLAY_NAME, type = CommandType.STRING, + description = "the display name of the migrated VM") + private String displayName; + + @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 VDDK details for CBT replication, such as vddk.lib.dir, vddk.transports and vddk.thumbprint") + private Map details; + + public Long getZoneId() { + return zoneId; + } + + public Long getClusterId() { + return clusterId; + } + + public String getDisplayName() { + return displayName; + } + + 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; + } + + @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() { + 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..d52fe9ba2096 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/SyncVmwareCbtMigrationCmd.java @@ -0,0 +1,89 @@ +// 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 = "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.22.1") +public class SyncVmwareCbtMigrationCmd 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.USERNAME, type = CommandType.STRING, + description = "the username for the source vCenter, required for sessions not linked to an existing vCenter") + private String username; + + @Parameter(name = ApiConstants.PASSWORD, type = CommandType.STRING, + description = "the password for the source vCenter, required for sessions not linked to an existing vCenter") + private String password; + + public Long getId() { + return id; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + @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/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..3a6ff90fa496 --- /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 KVM target disk path after initial full sync") + 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/VmwareCbtMigrationResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationResponse.java new file mode 100644 index 000000000000..35c78a18a4e3 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/VmwareCbtMigrationResponse.java @@ -0,0 +1,280 @@ +// 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(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 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/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..c64493eb37b3 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigration.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.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface VmwareCbtMigration extends Identity, InternalIdentity { + enum State { + Created, + InitialSync, + Replicating, + ReadyForCutover, + CuttingOver, + 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..d50edaa14d35 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigrationManager.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 org.apache.cloudstack.vm; + +import org.apache.cloudstack.api.command.admin.vm.CancelVmwareCbtMigrationCmd; +import org.apache.cloudstack.api.command.admin.vm.CutoverVmwareCbtMigrationCmd; +import org.apache.cloudstack.api.command.admin.vm.ListVmwareCbtMigrationsCmd; +import org.apache.cloudstack.api.command.admin.vm.RegisterVmwareCbtMigrationTargetCmd; +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.VmwareCbtMigrationResponse; + +import com.cloud.utils.component.PluggableService; + +public interface VmwareCbtMigrationManager extends PluggableService { + VmwareCbtMigrationResponse startVmwareCbtMigration(StartVmwareCbtMigrationCmd cmd); + + ListResponse listVmwareCbtMigrations(ListVmwareCbtMigrationsCmd cmd); + + VmwareCbtMigrationResponse syncVmwareCbtMigration(SyncVmwareCbtMigrationCmd cmd); + + VmwareCbtMigrationResponse registerVmwareCbtMigrationTarget(RegisterVmwareCbtMigrationTargetCmd cmd); + + VmwareCbtMigrationResponse cutoverVmwareCbtMigration(CutoverVmwareCbtMigrationCmd cmd); + + VmwareCbtMigrationResponse cancelVmwareCbtMigration(CancelVmwareCbtMigrationCmd 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..84dea224480a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigrationService.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 org.apache.cloudstack.vm; + +import java.util.List; + +public interface VmwareCbtMigrationService { + List listSourceDisks(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); + + 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/VmwareCbtSnapshotInfo.java b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtSnapshotInfo.java new file mode 100644 index 000000000000..eec220f8dde4 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtSnapshotInfo.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 VmwareCbtSnapshotInfo { + + private final String snapshotName; + private final String snapshotMor; + + public VmwareCbtSnapshotInfo(String snapshotName, String snapshotMor) { + this.snapshotName = snapshotName; + this.snapshotMor = snapshotMor; + } + + public String getSnapshotName() { + return snapshotName; + } + + public String getSnapshotMor() { + return snapshotMor; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtTargetDiskInfo.java b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtTargetDiskInfo.java new file mode 100644 index 000000000000..ebcd9ed926b0 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/vm/VmwareCbtTargetDiskInfo.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.vm; + +public class VmwareCbtTargetDiskInfo { + + private final String sourceDiskId; + private final String targetPath; + private final String targetFormat; + private final String changeId; + private final String snapshotMor; + + public VmwareCbtTargetDiskInfo(String sourceDiskId, String targetPath, String targetFormat, + String changeId, String snapshotMor) { + this.sourceDiskId = sourceDiskId; + this.targetPath = targetPath; + this.targetFormat = targetFormat; + this.changeId = changeId; + this.snapshotMor = snapshotMor; + } + + public String getSourceDiskId() { + return sourceDiskId; + } + + public String getTargetPath() { + return targetPath; + } + + public String getTargetFormat() { + return targetFormat; + } + + public String getChangeId() { + return changeId; + } + + public String getSnapshotMor() { + return snapshotMor; + } +} 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..6471bee43e36 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/VmwareCbtCleanupCommand.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 com.cloud.agent.api; + +import java.util.List; + +import com.cloud.agent.api.to.VmwareCbtDiskTO; + +public class VmwareCbtCleanupCommand extends Command { + + private String migrationUuid; + private List disks; + private boolean removeTemporarySnapshots; + private boolean removeNbdExports; + private boolean removePartialTargetDisks; + + 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; + } + + @Override + public boolean executeInSequence() { + return true; + } +} 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..2b4765dbe2b9 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/VmwareCbtCutoverCommand.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 com.cloud.agent.api; + +import java.util.List; + +import com.cloud.agent.api.to.RemoteInstanceTO; +import com.cloud.agent.api.to.VmwareCbtDiskTO; + +public class VmwareCbtCutoverCommand extends Command { + + private String migrationUuid; + private RemoteInstanceTO sourceInstance; + private List disks; + private int finalCycleNumber; + private boolean runVirtV2vFinalization; + 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 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..26ccf86a7264 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/VmwareCbtPrepareCommand.java @@ -0,0 +1,89 @@ +// 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; + +public class VmwareCbtPrepareCommand extends Command { + + private String migrationUuid; + private RemoteInstanceTO sourceInstance; + private List disks; + private String destinationStoragePoolUuid; + private String vddkLibDir; + private String vddkTransports; + private String vddkThumbprint; + + public VmwareCbtPrepareCommand() { + } + + public VmwareCbtPrepareCommand(String migrationUuid, RemoteInstanceTO sourceInstance, List disks, + String destinationStoragePoolUuid) { + this.migrationUuid = migrationUuid; + this.sourceInstance = sourceInstance; + this.disks = disks; + this.destinationStoragePoolUuid = destinationStoragePoolUuid; + } + + public String getMigrationUuid() { + return migrationUuid; + } + + public RemoteInstanceTO getSourceInstance() { + return sourceInstance; + } + + public List getDisks() { + return disks; + } + + public String getDestinationStoragePoolUuid() { + return destinationStoragePoolUuid; + } + + 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/VmwareCbtSyncCommand.java b/core/src/main/java/com/cloud/agent/api/VmwareCbtSyncCommand.java new file mode 100644 index 000000000000..959500601927 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/VmwareCbtSyncCommand.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 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; + +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 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 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/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java index 1215829d92f8..a1dde8fbe1ae 100644 --- a/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java @@ -808,8 +808,12 @@ protected AgentAttache notifyMonitorsOfConnection(final AgentAttache attache, fi String vddkSupport = detailsMap.get(Host.HOST_VDDK_SUPPORT); String vddkLibDir = detailsMap.get(Host.HOST_VDDK_LIB_DIR); String vddkVersion = detailsMap.get(Host.HOST_VDDK_VERSION); + String vmwareCbtSupport = detailsMap.get(Host.HOST_VMWARE_CBT_SUPPORT); + String qemuImgVersion = detailsMap.get(Host.HOST_QEMU_IMG_VERSION); + String qemuNbdVersion = detailsMap.get(Host.HOST_QEMU_NBD_VERSION); logger.debug("Got HOST_UEFI_ENABLE [{}] for host [{}]:", uefiEnabled, host); - if (ObjectUtils.anyNotNull(uefiEnabled, virtv2vVersion, ovftoolVersion, vddkSupport, vddkLibDir, vddkVersion)) { + if (ObjectUtils.anyNotNull(uefiEnabled, virtv2vVersion, ovftoolVersion, vddkSupport, vddkLibDir, vddkVersion, + vmwareCbtSupport, qemuImgVersion, qemuNbdVersion)) { _hostDao.loadDetails(host); boolean updateNeeded = false; if (StringUtils.isNotBlank(uefiEnabled) && !uefiEnabled.equals(host.getDetails().get(Host.HOST_UEFI_ENABLE))) { @@ -844,6 +848,26 @@ protected AgentAttache notifyMonitorsOfConnection(final AgentAttache attache, fi } updateNeeded = true; } + if (StringUtils.isNotBlank(vmwareCbtSupport) && !vmwareCbtSupport.equals(host.getDetails().get(Host.HOST_VMWARE_CBT_SUPPORT))) { + host.getDetails().put(Host.HOST_VMWARE_CBT_SUPPORT, vmwareCbtSupport); + updateNeeded = true; + } + if (!StringUtils.defaultString(qemuImgVersion).equals(StringUtils.defaultString(host.getDetails().get(Host.HOST_QEMU_IMG_VERSION)))) { + if (StringUtils.isBlank(qemuImgVersion)) { + host.getDetails().remove(Host.HOST_QEMU_IMG_VERSION); + } else { + host.getDetails().put(Host.HOST_QEMU_IMG_VERSION, qemuImgVersion); + } + updateNeeded = true; + } + if (!StringUtils.defaultString(qemuNbdVersion).equals(StringUtils.defaultString(host.getDetails().get(Host.HOST_QEMU_NBD_VERSION)))) { + if (StringUtils.isBlank(qemuNbdVersion)) { + host.getDetails().remove(Host.HOST_QEMU_NBD_VERSION); + } else { + host.getDetails().put(Host.HOST_QEMU_NBD_VERSION, qemuNbdVersion); + } + updateNeeded = true; + } if (updateNeeded) { _hostDao.saveDetails(host); } diff --git a/engine/schema/src/main/java/com/cloud/vm/VmwareCbtMigrationCycleVO.java b/engine/schema/src/main/java/com/cloud/vm/VmwareCbtMigrationCycleVO.java new file mode 100644 index 000000000000..78e492a434d9 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/VmwareCbtMigrationCycleVO.java @@ -0,0 +1,171 @@ +// 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.vm; + +import org.apache.cloudstack.vm.VmwareCbtMigrationCycle; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Date; +import java.util.UUID; + +@Entity +@Table(name = "vmware_cbt_migration_cycle") +public class VmwareCbtMigrationCycleVO implements VmwareCbtMigrationCycle { + + public VmwareCbtMigrationCycleVO() { + uuid = UUID.randomUUID().toString(); + } + + public VmwareCbtMigrationCycleVO(long migrationId, int cycleNumber) { + this(); + this.migrationId = migrationId; + this.cycleNumber = cycleNumber; + this.state = State.Created; + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "migration_id") + private long migrationId; + + @Column(name = "cycle_number") + private int cycleNumber; + + @Column(name = "snapshot_moref") + private String snapshotMor; + + @Column(name = "changed_bytes") + private Long changedBytes; + + @Column(name = "dirty_rate") + private Long dirtyRate; + + @Column(name = "duration") + private Long duration; + + @Column(name = "state") + @Enumerated(value = EnumType.STRING) + private State state; + + @Column(name = "description") + private String description; + + @Column(name = "created") + @Temporal(value = TemporalType.TIMESTAMP) + private Date created; + + @Column(name = "updated") + @Temporal(value = TemporalType.TIMESTAMP) + private Date updated; + + @Column(name = "removed") + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + public long getMigrationId() { + return migrationId; + } + + public int getCycleNumber() { + return cycleNumber; + } + + public String getSnapshotMor() { + return snapshotMor; + } + + public void setSnapshotMor(String snapshotMor) { + this.snapshotMor = snapshotMor; + } + + public Long getChangedBytes() { + return changedBytes; + } + + public void setChangedBytes(Long changedBytes) { + this.changedBytes = changedBytes; + } + + public Long getDirtyRate() { + return dirtyRate; + } + + public void setDirtyRate(Long dirtyRate) { + this.dirtyRate = dirtyRate; + } + + public Long getDuration() { + return duration; + } + + public void setDuration(Long duration) { + this.duration = duration; + } + + public State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getCreated() { + return created; + } + + public Date getUpdated() { + return updated; + } + + public void setUpdated(Date updated) { + this.updated = updated; + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/VmwareCbtMigrationDiskVO.java b/engine/schema/src/main/java/com/cloud/vm/VmwareCbtMigrationDiskVO.java new file mode 100644 index 000000000000..30b435ab09ab --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/VmwareCbtMigrationDiskVO.java @@ -0,0 +1,193 @@ +// 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.vm; + +import org.apache.cloudstack.vm.VmwareCbtMigrationDisk; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Date; +import java.util.UUID; + +@Entity +@Table(name = "vmware_cbt_migration_disk") +public class VmwareCbtMigrationDiskVO implements VmwareCbtMigrationDisk { + + public VmwareCbtMigrationDiskVO() { + uuid = UUID.randomUUID().toString(); + } + + public VmwareCbtMigrationDiskVO(long migrationId, String sourceDiskId, Integer sourceDiskDeviceKey, + String sourceDiskPath, String datastoreName, Long capacityBytes) { + this(); + this.migrationId = migrationId; + this.sourceDiskId = sourceDiskId; + this.sourceDiskDeviceKey = sourceDiskDeviceKey; + this.sourceDiskPath = sourceDiskPath; + this.datastoreName = datastoreName; + this.capacityBytes = capacityBytes; + this.state = State.Created; + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "migration_id") + private long migrationId; + + @Column(name = "source_disk_id") + private String sourceDiskId; + + @Column(name = "source_disk_device_key") + private Integer sourceDiskDeviceKey; + + @Column(name = "source_disk_path") + private String sourceDiskPath; + + @Column(name = "datastore_name") + private String datastoreName; + + @Column(name = "capacity_bytes") + private Long capacityBytes; + + @Column(name = "target_path") + private String targetPath; + + @Column(name = "target_format") + private String targetFormat; + + @Column(name = "change_id") + private String changeId; + + @Column(name = "snapshot_moref") + private String snapshotMor; + + @Column(name = "state") + @Enumerated(value = EnumType.STRING) + private State state; + + @Column(name = "created") + @Temporal(value = TemporalType.TIMESTAMP) + private Date created; + + @Column(name = "updated") + @Temporal(value = TemporalType.TIMESTAMP) + private Date updated; + + @Column(name = "removed") + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + public long getMigrationId() { + return migrationId; + } + + public String getSourceDiskId() { + return sourceDiskId; + } + + public Integer getSourceDiskDeviceKey() { + return sourceDiskDeviceKey; + } + + public String getSourceDiskPath() { + return sourceDiskPath; + } + + public String getDatastoreName() { + return datastoreName; + } + + public Long getCapacityBytes() { + return capacityBytes; + } + + public String getTargetPath() { + return targetPath; + } + + public void setTargetPath(String targetPath) { + this.targetPath = targetPath; + } + + public String getTargetFormat() { + return targetFormat; + } + + public void setTargetFormat(String targetFormat) { + this.targetFormat = targetFormat; + } + + public String getChangeId() { + return changeId; + } + + public void setChangeId(String changeId) { + this.changeId = changeId; + } + + public String getSnapshotMor() { + return snapshotMor; + } + + public void setSnapshotMor(String snapshotMor) { + this.snapshotMor = snapshotMor; + } + + public State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + } + + public Date getCreated() { + return created; + } + + public Date getUpdated() { + return updated; + } + + public void setUpdated(Date updated) { + this.updated = updated; + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/VmwareCbtMigrationVO.java b/engine/schema/src/main/java/com/cloud/vm/VmwareCbtMigrationVO.java new file mode 100644 index 000000000000..906e6cdd5b3e --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/VmwareCbtMigrationVO.java @@ -0,0 +1,341 @@ +// 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.vm; + +import org.apache.cloudstack.vm.VmwareCbtMigration; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Date; +import java.util.UUID; + +@Entity +@Table(name = "vmware_cbt_migration") +public class VmwareCbtMigrationVO implements VmwareCbtMigration { + + public VmwareCbtMigrationVO() { + uuid = UUID.randomUUID().toString(); + } + + public VmwareCbtMigrationVO(long zoneId, long accountId, long userId, long destinationClusterId, + String displayName, String vcenter, String datacenter, String sourceHost, + String sourceCluster, String sourceVmName) { + this(); + this.zoneId = zoneId; + this.accountId = accountId; + this.userId = userId; + this.destinationClusterId = destinationClusterId; + this.displayName = displayName; + this.vcenter = vcenter; + this.datacenter = datacenter; + this.sourceHost = sourceHost; + this.sourceCluster = sourceCluster; + this.sourceVmName = sourceVmName; + this.state = State.Created; + this.currentStep = "Created"; + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "zone_id") + private long zoneId; + + @Column(name = "account_id") + private long accountId; + + @Column(name = "user_id") + private long userId; + + @Column(name = "vm_id") + private Long vmId; + + @Column(name = "existing_vcenter_id") + private Long existingVcenterId; + + @Column(name = "destination_cluster_id") + private long destinationClusterId; + + @Column(name = "convert_host_id") + private Long convertHostId; + + @Column(name = "storage_pool_id") + private Long storagePoolId; + + @Column(name = "display_name") + private String displayName; + + @Column(name = "vcenter") + private String vcenter; + + @Column(name = "datacenter") + private String datacenter; + + @Column(name = "source_host") + private String sourceHost; + + @Column(name = "source_cluster") + private String sourceCluster; + + @Column(name = "source_vm_name") + private String sourceVmName; + + @Column(name = "vddk_lib_dir") + private String vddkLibDir; + + @Column(name = "vddk_transports") + private String vddkTransports; + + @Column(name = "vddk_thumbprint") + private String vddkThumbprint; + + @Column(name = "state") + @Enumerated(value = EnumType.STRING) + private State state; + + @Column(name = "current_step") + private String currentStep; + + @Column(name = "last_error") + private String lastError; + + @Column(name = "completed_cycles") + private int completedCycles; + + @Column(name = "quiet_cycles") + private int quietCycles; + + @Column(name = "total_changed_bytes") + private long totalChangedBytes; + + @Column(name = "last_changed_bytes") + private Long lastChangedBytes; + + @Column(name = "last_dirty_rate") + private Long lastDirtyRate; + + @Column(name = "created") + @Temporal(value = TemporalType.TIMESTAMP) + private Date created; + + @Column(name = "updated") + @Temporal(value = TemporalType.TIMESTAMP) + private Date updated; + + @Column(name = "removed") + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + public long getZoneId() { + return zoneId; + } + + public long getAccountId() { + return accountId; + } + + public long getUserId() { + return userId; + } + + public Long getVmId() { + return vmId; + } + + public void setVmId(Long vmId) { + this.vmId = vmId; + } + + public Long getExistingVcenterId() { + return existingVcenterId; + } + + public void setExistingVcenterId(Long existingVcenterId) { + this.existingVcenterId = existingVcenterId; + } + + public long getDestinationClusterId() { + return destinationClusterId; + } + + public Long getConvertHostId() { + return convertHostId; + } + + public void setConvertHostId(Long convertHostId) { + this.convertHostId = convertHostId; + } + + public Long getStoragePoolId() { + return storagePoolId; + } + + public void setStoragePoolId(Long storagePoolId) { + this.storagePoolId = storagePoolId; + } + + public String getDisplayName() { + return displayName; + } + + public String getVcenter() { + return vcenter; + } + + public String getDatacenter() { + return datacenter; + } + + public String getSourceHost() { + return sourceHost; + } + + public String getSourceCluster() { + return sourceCluster; + } + + public String getSourceVmName() { + return sourceVmName; + } + + 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; + } + + public State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + } + + public String getCurrentStep() { + return currentStep; + } + + public void setCurrentStep(String currentStep) { + this.currentStep = currentStep; + } + + public String getLastError() { + return lastError; + } + + public void setLastError(String lastError) { + this.lastError = lastError; + } + + public int getCompletedCycles() { + return completedCycles; + } + + public void setCompletedCycles(int completedCycles) { + this.completedCycles = completedCycles; + } + + public int getQuietCycles() { + return quietCycles; + } + + public void setQuietCycles(int quietCycles) { + this.quietCycles = quietCycles; + } + + public long getTotalChangedBytes() { + return totalChangedBytes; + } + + public void setTotalChangedBytes(long totalChangedBytes) { + this.totalChangedBytes = totalChangedBytes; + } + + public Long getLastChangedBytes() { + return lastChangedBytes; + } + + public void setLastChangedBytes(Long lastChangedBytes) { + this.lastChangedBytes = lastChangedBytes; + } + + public Long getLastDirtyRate() { + return lastDirtyRate; + } + + public void setLastDirtyRate(Long lastDirtyRate) { + this.lastDirtyRate = lastDirtyRate; + } + + public Date getCreated() { + return created; + } + + public Date getUpdated() { + return updated; + } + + public void setUpdated(Date updated) { + this.updated = updated; + } + + public Date getRemoved() { + return removed; + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationCycleDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationCycleDao.java new file mode 100644 index 000000000000..25debad70156 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationCycleDao.java @@ -0,0 +1,26 @@ +// 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.vm.dao; + +import com.cloud.utils.db.GenericDao; +import com.cloud.vm.VmwareCbtMigrationCycleVO; + +import java.util.List; + +public interface VmwareCbtMigrationCycleDao extends GenericDao { + List listByMigrationId(long migrationId); +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationCycleDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationCycleDaoImpl.java new file mode 100644 index 000000000000..0d823a29287b --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationCycleDaoImpl.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 com.cloud.vm.dao; + +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.VmwareCbtMigrationCycleVO; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class VmwareCbtMigrationCycleDaoImpl extends GenericDaoBase implements VmwareCbtMigrationCycleDao { + + private final SearchBuilder migrationSearch; + + public VmwareCbtMigrationCycleDaoImpl() { + migrationSearch = createSearchBuilder(); + migrationSearch.and("migrationId", migrationSearch.entity().getMigrationId(), SearchCriteria.Op.EQ); + migrationSearch.done(); + } + + @Override + public List listByMigrationId(long migrationId) { + SearchCriteria sc = migrationSearch.create(); + sc.setParameters("migrationId", migrationId); + Filter filter = new Filter(VmwareCbtMigrationCycleVO.class, "cycleNumber", true, null, null); + return listBy(sc, filter); + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationDao.java new file mode 100644 index 000000000000..4cbe60d38f77 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationDao.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 com.cloud.vm.dao; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.GenericDao; +import com.cloud.vm.VmwareCbtMigrationVO; +import org.apache.cloudstack.vm.VmwareCbtMigration; + +import java.util.List; + +public interface VmwareCbtMigrationDao extends GenericDao { + Pair, Integer> listMigrations(Long id, Long zoneId, Long accountId, String vcenter, + String sourceVmName, VmwareCbtMigration.State state, + Long startIndex, Long pageSizeVal); +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationDaoImpl.java new file mode 100644 index 000000000000..0e6b05ff377f --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationDaoImpl.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 com.cloud.vm.dao; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.VmwareCbtMigrationVO; +import org.apache.cloudstack.vm.VmwareCbtMigration; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class VmwareCbtMigrationDaoImpl extends GenericDaoBase implements VmwareCbtMigrationDao { + + private final SearchBuilder migrationSearch; + + public VmwareCbtMigrationDaoImpl() { + migrationSearch = createSearchBuilder(); + migrationSearch.and("id", migrationSearch.entity().getId(), SearchCriteria.Op.EQ); + migrationSearch.and("zoneId", migrationSearch.entity().getZoneId(), SearchCriteria.Op.EQ); + migrationSearch.and("accountId", migrationSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + migrationSearch.and("vcenter", migrationSearch.entity().getVcenter(), SearchCriteria.Op.EQ); + migrationSearch.and("sourceVmName", migrationSearch.entity().getSourceVmName(), SearchCriteria.Op.EQ); + migrationSearch.and("state", migrationSearch.entity().getState(), SearchCriteria.Op.EQ); + migrationSearch.done(); + } + + @Override + public Pair, Integer> listMigrations(Long id, Long zoneId, Long accountId, String vcenter, + String sourceVmName, VmwareCbtMigration.State state, + Long startIndex, Long pageSizeVal) { + SearchCriteria sc = migrationSearch.create(); + if (id != null) { + sc.setParameters("id", id); + } + if (zoneId != null) { + sc.setParameters("zoneId", zoneId); + } + if (accountId != null) { + sc.setParameters("accountId", accountId); + } + if (StringUtils.isNotBlank(vcenter)) { + sc.setParameters("vcenter", vcenter); + } + if (StringUtils.isNotBlank(sourceVmName)) { + sc.setParameters("sourceVmName", sourceVmName); + } + if (state != null) { + sc.setParameters("state", state); + } + Filter filter = new Filter(VmwareCbtMigrationVO.class, "created", false, startIndex, pageSizeVal); + return searchAndCount(sc, filter); + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationDiskDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationDiskDao.java new file mode 100644 index 000000000000..60a3f13355c6 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationDiskDao.java @@ -0,0 +1,26 @@ +// 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.vm.dao; + +import com.cloud.utils.db.GenericDao; +import com.cloud.vm.VmwareCbtMigrationDiskVO; + +import java.util.List; + +public interface VmwareCbtMigrationDiskDao extends GenericDao { + List listByMigrationId(long migrationId); +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationDiskDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationDiskDaoImpl.java new file mode 100644 index 000000000000..55463f249366 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VmwareCbtMigrationDiskDaoImpl.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 com.cloud.vm.dao; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.VmwareCbtMigrationDiskVO; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class VmwareCbtMigrationDiskDaoImpl extends GenericDaoBase implements VmwareCbtMigrationDiskDao { + + private final SearchBuilder migrationSearch; + + public VmwareCbtMigrationDiskDaoImpl() { + migrationSearch = createSearchBuilder(); + migrationSearch.and("migrationId", migrationSearch.entity().getMigrationId(), SearchCriteria.Op.EQ); + migrationSearch.done(); + } + + @Override + public List listByMigrationId(long migrationId) { + SearchCriteria sc = migrationSearch.create(); + sc.setParameters("migrationId", migrationId); + return listBy(sc); + } +} diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index c99f798d3d56..11671f80af5a 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -131,3 +131,91 @@ CREATE TABLE IF NOT EXISTS `cloud_usage`.`quota_tariff_usage` ( -- Add the 'keep_mac_address_on_public_nic' column to the 'cloud.networks' and 'cloud.vpc' tables CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.networks', 'keep_mac_address_on_public_nic', 'TINYINT(1) NOT NULL DEFAULT 1'); CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc', 'keep_mac_address_on_public_nic', 'TINYINT(1) NOT NULL DEFAULT 1'); + +-- VMware CBT warm migration session state +CREATE TABLE IF NOT EXISTS `cloud`.`vmware_cbt_migration` ( + `id` bigint unsigned NOT NULL auto_increment COMMENT 'id', + `uuid` varchar(40) NOT NULL COMMENT 'UUID', + `zone_id` bigint unsigned NOT NULL COMMENT 'Zone ID', + `account_id` bigint unsigned NOT NULL COMMENT 'Account ID', + `user_id` bigint unsigned NOT NULL COMMENT 'User ID', + `vm_id` bigint unsigned COMMENT 'Imported VM ID after cutover', + `existing_vcenter_id` bigint unsigned COMMENT 'Linked VMware datacenter ID when the source vCenter is registered', + `destination_cluster_id` bigint unsigned NOT NULL COMMENT 'Destination KVM cluster ID', + `convert_host_id` bigint unsigned COMMENT 'KVM host used for conversion and synchronization', + `storage_pool_id` bigint unsigned COMMENT 'Destination primary storage pool ID', + `display_name` varchar(255) COMMENT 'Target VM display name', + `vcenter` varchar(255) COMMENT 'Source vCenter', + `datacenter` varchar(255) COMMENT 'Source vCenter datacenter name', + `source_host` varchar(255) COMMENT 'Source VMware host name or IP', + `source_cluster` varchar(255) COMMENT 'Source VMware cluster name', + `source_vm_name` varchar(255) NOT NULL COMMENT 'Source VM name on vCenter', + `vddk_lib_dir` varchar(1024) COMMENT 'Optional VDDK library directory override', + `vddk_transports` varchar(255) COMMENT 'Optional VDDK transport list override', + `vddk_thumbprint` varchar(255) COMMENT 'Optional vCenter TLS thumbprint for VDDK connections', + `state` varchar(32) NOT NULL COMMENT 'Migration state', + `current_step` varchar(255) COMMENT 'Current migration step', + `last_error` varchar(1024) COMMENT 'Last error message', + `completed_cycles` int unsigned NOT NULL DEFAULT 0 COMMENT 'Completed CBT delta cycles', + `quiet_cycles` int unsigned NOT NULL DEFAULT 0 COMMENT 'Consecutive quiet CBT delta cycles', + `total_changed_bytes` bigint unsigned NOT NULL DEFAULT 0 COMMENT 'Total changed bytes copied across delta cycles', + `last_changed_bytes` bigint unsigned COMMENT 'Changed bytes copied in the latest delta cycle', + `last_dirty_rate` bigint unsigned COMMENT 'Changed bytes per second in the latest delta cycle', + `created` datetime NOT NULL COMMENT 'date created', + `updated` datetime COMMENT 'date updated if not null', + `removed` datetime COMMENT 'date removed if not null', + PRIMARY KEY (`id`), + CONSTRAINT `fk_vmware_cbt_migration__zone_id` FOREIGN KEY (`zone_id`) REFERENCES `data_center`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_vmware_cbt_migration__account_id` FOREIGN KEY (`account_id`) REFERENCES `account`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_vmware_cbt_migration__user_id` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_vmware_cbt_migration__vm_id` FOREIGN KEY (`vm_id`) REFERENCES `vm_instance`(`id`) ON DELETE SET NULL, + CONSTRAINT `fk_vmware_cbt_migration__existing_vcenter_id` FOREIGN KEY (`existing_vcenter_id`) REFERENCES `vmware_data_center`(`id`) ON DELETE SET NULL, + CONSTRAINT `fk_vmware_cbt_migration__destination_cluster_id` FOREIGN KEY (`destination_cluster_id`) REFERENCES `cluster`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_vmware_cbt_migration__convert_host_id` FOREIGN KEY (`convert_host_id`) REFERENCES `host`(`id`) ON DELETE SET NULL, + CONSTRAINT `fk_vmware_cbt_migration__storage_pool_id` FOREIGN KEY (`storage_pool_id`) REFERENCES `storage_pool`(`id`) ON DELETE SET NULL, + INDEX `i_vmware_cbt_migration__zone_id` (`zone_id`), + INDEX `i_vmware_cbt_migration__state` (`state`), + INDEX `i_vmware_cbt_migration__source_vm_name` (`source_vm_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `cloud`.`vmware_cbt_migration_disk` ( + `id` bigint unsigned NOT NULL auto_increment COMMENT 'id', + `uuid` varchar(40) NOT NULL COMMENT 'UUID', + `migration_id` bigint unsigned NOT NULL COMMENT 'VMware CBT migration ID', + `source_disk_id` varchar(255) COMMENT 'Source VMware disk key or label', + `source_disk_device_key` int COMMENT 'Source VMware virtual disk device key for QueryChangedDiskAreas', + `source_disk_path` varchar(1024) COMMENT 'Source VMware disk path', + `datastore_name` varchar(255) COMMENT 'Source VMware datastore name', + `capacity_bytes` bigint unsigned COMMENT 'Source disk capacity in bytes', + `target_path` varchar(1024) COMMENT 'Target KVM disk path', + `target_format` varchar(32) COMMENT 'Target KVM disk format', + `change_id` varchar(255) COMMENT 'Latest VMware CBT change ID', + `snapshot_moref` varchar(255) COMMENT 'Latest VMware snapshot managed object reference', + `state` varchar(32) NOT NULL COMMENT 'Disk synchronization state', + `created` datetime NOT NULL COMMENT 'date created', + `updated` datetime COMMENT 'date updated if not null', + `removed` datetime COMMENT 'date removed if not null', + PRIMARY KEY (`id`), + CONSTRAINT `fk_vmware_cbt_migration_disk__migration_id` FOREIGN KEY (`migration_id`) REFERENCES `vmware_cbt_migration`(`id`) ON DELETE CASCADE, + INDEX `i_vmware_cbt_migration_disk__migration_id` (`migration_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `cloud`.`vmware_cbt_migration_cycle` ( + `id` bigint unsigned NOT NULL auto_increment COMMENT 'id', + `uuid` varchar(40) NOT NULL COMMENT 'UUID', + `migration_id` bigint unsigned NOT NULL COMMENT 'VMware CBT migration ID', + `cycle_number` int unsigned NOT NULL COMMENT 'CBT delta cycle number', + `snapshot_moref` varchar(255) COMMENT 'VMware snapshot managed object reference used for the cycle', + `changed_bytes` bigint unsigned COMMENT 'Changed bytes copied in this cycle', + `dirty_rate` bigint unsigned COMMENT 'Changed bytes per second in this cycle', + `duration` bigint unsigned COMMENT 'Cycle duration in milliseconds', + `state` varchar(32) NOT NULL COMMENT 'CBT delta cycle state', + `description` varchar(1024) COMMENT 'Cycle description or error message', + `created` datetime NOT NULL COMMENT 'date created', + `updated` datetime COMMENT 'date updated if not null', + `removed` datetime COMMENT 'date removed if not null', + PRIMARY KEY (`id`), + CONSTRAINT `fk_vmware_cbt_migration_cycle__migration_id` FOREIGN KEY (`migration_id`) REFERENCES `vmware_cbt_migration`(`id`) ON DELETE CASCADE, + UNIQUE KEY `uc_vmware_cbt_migration_cycle__migration_id__cycle_number` (`migration_id`, `cycle_number`), + INDEX `i_vmware_cbt_migration_cycle__migration_id` (`migration_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index 64ec0ed95d2e..b70216bede05 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -18,10 +18,13 @@ import static com.cloud.host.Host.HOST_INSTANCE_CONVERSION; import static com.cloud.host.Host.HOST_OVFTOOL_VERSION; +import static com.cloud.host.Host.HOST_QEMU_IMG_VERSION; +import static com.cloud.host.Host.HOST_QEMU_NBD_VERSION; import static com.cloud.host.Host.HOST_VDDK_LIB_DIR; import static com.cloud.host.Host.HOST_VDDK_SUPPORT; import static com.cloud.host.Host.HOST_VDDK_VERSION; import static com.cloud.host.Host.HOST_VIRTV2V_VERSION; +import static com.cloud.host.Host.HOST_VMWARE_CBT_SUPPORT; import static com.cloud.host.Host.HOST_VOLUME_ENCRYPTION; import static org.apache.cloudstack.utils.linux.KVMHostInfo.isHostS390x; @@ -369,6 +372,8 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv public static final String UBUNTU_WINDOWS_GUEST_CONVERSION_SUPPORTED_CHECK_CMD = "dpkg -l virtio-win"; public static final String UBUNTU_NBDKIT_PKG_CHECK_CMD = "dpkg -l nbdkit"; public static final String VDDK_AUTODETECT_PATH_CMD = "find / -type d -name 'vmware-vix-disklib-distrib' 2>/dev/null | head -n 1"; + public static final String QEMU_IMG_SUPPORTED_CHECK_CMD = "qemu-img --version"; + public static final String QEMU_NBD_SUPPORTED_CHECK_CMD = "qemu-nbd --version"; public static final int LIBVIRT_CGROUP_CPU_SHARES_MIN = 2; public static final int LIBVIRT_CGROUP_CPU_SHARES_MAX = 262144; @@ -4286,12 +4291,21 @@ public StartupCommand[] initialize() { boolean instanceConversionSupported = hostSupportsInstanceConversion(); cmd.getHostDetails().put(HOST_INSTANCE_CONVERSION, String.valueOf(instanceConversionSupported)); cmd.getHostDetails().put(HOST_VDDK_SUPPORT, String.valueOf(hostSupportsVddk())); + cmd.getHostDetails().put(HOST_VMWARE_CBT_SUPPORT, String.valueOf(hostSupportsVmwareCbtMigration())); if (StringUtils.isNotBlank(vddkLibDir)) { cmd.getHostDetails().put(HOST_VDDK_LIB_DIR, vddkLibDir); } if (StringUtils.isNotBlank(vddkVersion)) { cmd.getHostDetails().put(HOST_VDDK_VERSION, vddkVersion); } + String qemuImgVersion = getQemuImgVersion(); + if (StringUtils.isNotBlank(qemuImgVersion)) { + cmd.getHostDetails().put(HOST_QEMU_IMG_VERSION, qemuImgVersion); + } + String qemuNbdVersion = getQemuNbdVersion(); + if (StringUtils.isNotBlank(qemuNbdVersion)) { + cmd.getHostDetails().put(HOST_QEMU_NBD_VERSION, qemuNbdVersion); + } if (instanceConversionSupported) { cmd.getHostDetails().put(HOST_VIRTV2V_VERSION, getHostVirtV2vVersion()); } @@ -6028,6 +6042,55 @@ public boolean hostSupportsVddk(String overriddenVddkLibDir) { return hostSupportsInstanceConversion() && isVddkLibDirValid(effectiveVddkLibDir) && StringUtils.isNotBlank(detectVddkVersion()); } + public boolean hostSupportsVmwareCbtMigration() { + return hostSupportsVmwareCbtMigration(null); + } + + public boolean hostSupportsVmwareCbtMigration(String overriddenVddkLibDir) { + return hostSupportsVddk(overriddenVddkLibDir) + && Script.runSimpleBashScriptForExitValue(QEMU_IMG_SUPPORTED_CHECK_CMD) == 0 + && Script.runSimpleBashScriptForExitValue(QEMU_NBD_SUPPORTED_CHECK_CMD) == 0; + } + + public String getQemuImgVersion() { + return detectFirstLineVersion("qemu-img", "--version"); + } + + public String getQemuNbdVersion() { + return detectFirstLineVersion("qemu-nbd", "--version"); + } + + protected String detectFirstLineVersion(String... command) { + try { + ProcessBuilder pb = new ProcessBuilder(command); + Process process = pb.start(); + + String output = new String(process.getInputStream().readAllBytes()); + process.waitFor(); + + for (String line : output.split("\\R")) { + String trimmed = StringUtils.trimToNull(line); + if (StringUtils.isNotBlank(trimmed)) { + return parseVersionToken(trimmed); + } + } + } catch (Exception e) { + LOGGER.debug("Failed to detect version for command {}: {}", String.join(" ", command), e.getMessage()); + } + return null; + } + + protected String parseVersionToken(String versionLine) { + String versionMarker = " version "; + int markerIndex = versionLine.indexOf(versionMarker); + if (markerIndex < 0) { + return versionLine; + } + String value = versionLine.substring(markerIndex + versionMarker.length()); + String[] parts = value.split("\\s+", 2); + return parts.length > 0 ? parts[0] : versionLine; + } + protected boolean isVddkLibDirValid(String path) { if (StringUtils.isBlank(path)) { return false; diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java index 5a7d6d2c203a..a2858a172328 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java @@ -54,6 +54,9 @@ public Answer execute(final ReadyCommand command, final LibvirtComputingResource hostDetails.put(Host.HOST_VDDK_SUPPORT, Boolean.toString(libvirtComputingResource.hostSupportsVddk())); hostDetails.put(Host.HOST_VDDK_LIB_DIR, StringUtils.defaultString(libvirtComputingResource.getVddkLibDir())); hostDetails.put(Host.HOST_VDDK_VERSION, StringUtils.defaultString(libvirtComputingResource.getVddkVersion())); + hostDetails.put(Host.HOST_VMWARE_CBT_SUPPORT, Boolean.toString(libvirtComputingResource.hostSupportsVmwareCbtMigration())); + hostDetails.put(Host.HOST_QEMU_IMG_VERSION, StringUtils.defaultString(libvirtComputingResource.getQemuImgVersion())); + hostDetails.put(Host.HOST_QEMU_NBD_VERSION, StringUtils.defaultString(libvirtComputingResource.getQemuNbdVersion())); if (libvirtComputingResource.hostSupportsOvfExport()) { hostDetails.put(Host.HOST_OVFTOOL_VERSION, libvirtComputingResource.getHostOvfToolVersion()); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtVmwareCbtCleanupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtVmwareCbtCleanupCommandWrapper.java new file mode 100644 index 000000000000..7b6c04f63d7b --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtVmwareCbtCleanupCommandWrapper.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 com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.VmwareCbtCleanupCommand; +import com.cloud.agent.api.VmwareCbtMigrationAnswer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; + +@ResourceWrapper(handles = VmwareCbtCleanupCommand.class) +public class LibvirtVmwareCbtCleanupCommandWrapper extends CommandWrapper { + + @Override + public Answer execute(VmwareCbtCleanupCommand cmd, LibvirtComputingResource serverResource) { + String msg = String.format("VMware CBT cleanup for migration %s did not find any agent-side replicated state to remove yet.", + cmd.getMigrationUuid()); + logger.info(msg); + return new VmwareCbtMigrationAnswer(cmd, true, msg, cmd.getMigrationUuid()); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtVmwareCbtCutoverCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtVmwareCbtCutoverCommandWrapper.java new file mode 100644 index 000000000000..f0018c78f8b6 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtVmwareCbtCutoverCommandWrapper.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 com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.VmwareCbtCutoverCommand; +import com.cloud.agent.api.VmwareCbtMigrationAnswer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; + +@ResourceWrapper(handles = VmwareCbtCutoverCommand.class) +public class LibvirtVmwareCbtCutoverCommandWrapper extends CommandWrapper { + + @Override + public Answer execute(VmwareCbtCutoverCommand cmd, LibvirtComputingResource serverResource) { + if (!serverResource.hostSupportsVmwareCbtMigration(cmd.getVddkLibDir())) { + String msg = String.format("Cannot cut over VMware CBT migration %s on host %s. VDDK, qemu-img and qemu-nbd are required.", + cmd.getMigrationUuid(), serverResource.getPrivateIp()); + logger.info(msg); + return new VmwareCbtMigrationAnswer(cmd, false, msg, cmd.getMigrationUuid()); + } + + String msg = String.format("VMware CBT cutover for migration %s reached the KVM agent, but final cutover execution is not implemented yet.", + cmd.getMigrationUuid()); + logger.info(msg); + return new VmwareCbtMigrationAnswer(cmd, false, msg, cmd.getMigrationUuid(), cmd.getFinalCycleNumber(), + 0, 0, 0, false, null); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtVmwareCbtPrepareCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtVmwareCbtPrepareCommandWrapper.java new file mode 100644 index 000000000000..7e8b6585dad3 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtVmwareCbtPrepareCommandWrapper.java @@ -0,0 +1,43 @@ +// 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.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.VmwareCbtMigrationAnswer; +import com.cloud.agent.api.VmwareCbtPrepareCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; + +@ResourceWrapper(handles = VmwareCbtPrepareCommand.class) +public class LibvirtVmwareCbtPrepareCommandWrapper extends CommandWrapper { + + @Override + public Answer execute(VmwareCbtPrepareCommand cmd, LibvirtComputingResource serverResource) { + if (!serverResource.hostSupportsVmwareCbtMigration(cmd.getVddkLibDir())) { + String msg = String.format("Cannot prepare VMware CBT migration %s on host %s. VDDK, qemu-img and qemu-nbd are required.", + cmd.getMigrationUuid(), serverResource.getPrivateIp()); + logger.info(msg); + return new VmwareCbtMigrationAnswer(cmd, false, msg, cmd.getMigrationUuid()); + } + + String msg = String.format("VMware CBT prepare for migration %s reached the KVM agent, but block replication preparation is not implemented yet.", + cmd.getMigrationUuid()); + logger.info(msg); + return new VmwareCbtMigrationAnswer(cmd, false, msg, cmd.getMigrationUuid()); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtVmwareCbtSyncCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtVmwareCbtSyncCommandWrapper.java new file mode 100644 index 000000000000..4aff573983c6 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtVmwareCbtSyncCommandWrapper.java @@ -0,0 +1,71 @@ +// 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.hypervisor.kvm.resource.wrapper; + +import java.util.List; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.VmwareCbtMigrationAnswer; +import com.cloud.agent.api.VmwareCbtSyncCommand; +import com.cloud.agent.api.to.VmwareCbtChangedBlockRangeTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; + +@ResourceWrapper(handles = VmwareCbtSyncCommand.class) +public class LibvirtVmwareCbtSyncCommandWrapper extends CommandWrapper { + + @Override + public Answer execute(VmwareCbtSyncCommand cmd, LibvirtComputingResource serverResource) { + if (!serverResource.hostSupportsVmwareCbtMigration(cmd.getVddkLibDir())) { + String msg = String.format("Cannot synchronize VMware CBT migration %s on host %s. VDDK, qemu-img and qemu-nbd are required.", + cmd.getMigrationUuid(), serverResource.getPrivateIp()); + logger.info(msg); + return new VmwareCbtMigrationAnswer(cmd, false, msg, cmd.getMigrationUuid(), cmd.getCycleNumber(), + 0, 0, 0, false, null); + } + + long startTime = System.currentTimeMillis(); + List changedBlocks = cmd.getChangedBlocks(); + if (changedBlocks == null || changedBlocks.isEmpty()) { + long durationSeconds = Math.max(1L, (System.currentTimeMillis() - startTime) / 1000L); + String msg = String.format("VMware CBT cycle %s for migration %s completed with no changed blocks.", + cmd.getCycleNumber(), cmd.getMigrationUuid()); + logger.info(msg); + return new VmwareCbtMigrationAnswer(cmd, true, msg, cmd.getMigrationUuid(), cmd.getCycleNumber(), + 0, 0, durationSeconds, true, null); + } + + VmwareCbtSyncPlan syncPlan = VmwareCbtSyncPlan.create(cmd.getDisks(), changedBlocks); + if (!syncPlan.isValid()) { + String msg = String.format("Cannot synchronize VMware CBT cycle %s for migration %s: %s", + cmd.getCycleNumber(), cmd.getMigrationUuid(), syncPlan.getValidationError()); + logger.info(msg); + return new VmwareCbtMigrationAnswer(cmd, false, msg, cmd.getMigrationUuid(), cmd.getCycleNumber(), + 0, 0, 0, false, null); + } + + String msg = String.format("VMware CBT cycle %s for migration %s validated %s changed block range(s), " + + "coalesced them into %s copy range(s) " + + "across %s disk(s), totaling %s bytes, but changed-block copy is not implemented yet.", + cmd.getCycleNumber(), cmd.getMigrationUuid(), syncPlan.getChangedRangeCount(), syncPlan.getCopyRangeCount(), + syncPlan.getDiskPlans().size(), syncPlan.getChangedBytes()); + logger.info(msg); + return new VmwareCbtMigrationAnswer(cmd, false, msg, cmd.getMigrationUuid(), cmd.getCycleNumber(), + syncPlan.getChangedBytes(), 0, 0, false, null); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/VmwareCbtSyncPlan.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/VmwareCbtSyncPlan.java new file mode 100644 index 000000000000..458b5e6bba60 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/VmwareCbtSyncPlan.java @@ -0,0 +1,273 @@ +// 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.hypervisor.kvm.resource.wrapper; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import com.cloud.agent.api.to.VmwareCbtChangedBlockRangeTO; +import com.cloud.agent.api.to.VmwareCbtDiskTO; + +class VmwareCbtSyncPlan { + + private final boolean valid; + private final String validationError; + private final List diskPlans; + private final int changedRangeCount; + private final int copyRangeCount; + private final long changedBytes; + + private VmwareCbtSyncPlan(boolean valid, String validationError, List diskPlans, + int changedRangeCount, int copyRangeCount, long changedBytes) { + this.valid = valid; + this.validationError = validationError; + this.diskPlans = diskPlans; + this.changedRangeCount = changedRangeCount; + this.copyRangeCount = copyRangeCount; + this.changedBytes = changedBytes; + } + + static VmwareCbtSyncPlan create(List disks, List changedBlocks) { + if (CollectionUtils.isEmpty(changedBlocks)) { + return new VmwareCbtSyncPlan(true, null, Collections.emptyList(), 0, 0, 0); + } + + Map diskPlansById = getDiskPlansById(disks); + int changedRangeCount = 0; + + for (VmwareCbtChangedBlockRangeTO changedBlock : changedBlocks) { + ValidationResult validationResult = validateChangedBlock(changedBlock, diskPlansById); + if (!validationResult.valid) { + return invalid(validationResult.error); + } + + DiskPlanBuilder diskPlan = diskPlansById.get(changedBlock.getDiskId()); + long rangeEnd = changedBlock.getStartOffset() + changedBlock.getLength(); + if (rangeEnd < changedBlock.getStartOffset()) { + return invalid(String.format("Changed block range for disk %s overflows the virtual disk address space.", + changedBlock.getDiskId())); + } + if (diskPlan.disk.getCapacityBytes() > 0 && rangeEnd > diskPlan.disk.getCapacityBytes()) { + return invalid(String.format("Changed block range for disk %s exceeds disk capacity.", changedBlock.getDiskId())); + } + + diskPlan.addChangedBlock(changedBlock); + changedRangeCount++; + } + + List diskPlans = getPopulatedDiskPlans(diskPlansById); + long changedBytes = 0; + int copyRangeCount = 0; + for (DiskPlan diskPlan : diskPlans) { + changedBytes += diskPlan.getChangedBytes(); + copyRangeCount += diskPlan.getChangedBlocks().size(); + } + + return new VmwareCbtSyncPlan(true, null, diskPlans, changedRangeCount, copyRangeCount, changedBytes); + } + + private static Map getDiskPlansById(List disks) { + Map diskPlansById = new LinkedHashMap<>(); + if (CollectionUtils.isEmpty(disks)) { + return diskPlansById; + } + + for (VmwareCbtDiskTO disk : disks) { + if (disk != null && StringUtils.isNotBlank(disk.getDiskId())) { + diskPlansById.put(disk.getDiskId(), new DiskPlanBuilder(disk)); + } + } + return diskPlansById; + } + + private static ValidationResult validateChangedBlock(VmwareCbtChangedBlockRangeTO changedBlock, + Map diskPlansById) { + if (changedBlock == null) { + return ValidationResult.invalid("Changed block range cannot be null."); + } + if (StringUtils.isBlank(changedBlock.getDiskId())) { + return ValidationResult.invalid("Changed block range is missing a disk id."); + } + + DiskPlanBuilder diskPlan = diskPlansById.get(changedBlock.getDiskId()); + if (diskPlan == null) { + return ValidationResult.invalid(String.format("Changed block range references unknown disk %s.", changedBlock.getDiskId())); + } + if (StringUtils.isBlank(diskPlan.disk.getTargetPath())) { + return ValidationResult.invalid(String.format("Changed block range references disk %s, but no target path is known. " + + "The initial full sync must create and persist the KVM target disk before CBT delta sync can run.", + changedBlock.getDiskId())); + } + if (changedBlock.getStartOffset() < 0) { + return ValidationResult.invalid(String.format("Changed block range for disk %s has a negative start offset.", + changedBlock.getDiskId())); + } + if (changedBlock.getLength() <= 0) { + return ValidationResult.invalid(String.format("Changed block range for disk %s has a non-positive length.", + changedBlock.getDiskId())); + } + + return ValidationResult.valid(); + } + + private static List getPopulatedDiskPlans(Map diskPlansById) { + List diskPlans = new ArrayList<>(); + for (DiskPlanBuilder diskPlan : diskPlansById.values()) { + if (CollectionUtils.isNotEmpty(diskPlan.changedBlocks)) { + diskPlans.add(diskPlan.build()); + } + } + return diskPlans; + } + + private static VmwareCbtSyncPlan invalid(String validationError) { + return new VmwareCbtSyncPlan(false, validationError, Collections.emptyList(), 0, 0, 0); + } + + boolean isValid() { + return valid; + } + + String getValidationError() { + return validationError; + } + + List getDiskPlans() { + return diskPlans; + } + + int getChangedRangeCount() { + return changedRangeCount; + } + + int getCopyRangeCount() { + return copyRangeCount; + } + + long getChangedBytes() { + return changedBytes; + } + + static class DiskPlan { + + private final VmwareCbtDiskTO disk; + private final List changedBlocks; + private final long changedBytes; + + DiskPlan(VmwareCbtDiskTO disk, List changedBlocks, long changedBytes) { + this.disk = disk; + this.changedBlocks = Collections.unmodifiableList(changedBlocks); + this.changedBytes = changedBytes; + } + + VmwareCbtDiskTO getDisk() { + return disk; + } + + List getChangedBlocks() { + return changedBlocks; + } + + long getChangedBytes() { + return changedBytes; + } + } + + private static class DiskPlanBuilder { + + private final VmwareCbtDiskTO disk; + private final List changedBlocks = new ArrayList<>(); + + DiskPlanBuilder(VmwareCbtDiskTO disk) { + this.disk = disk; + } + + void addChangedBlock(VmwareCbtChangedBlockRangeTO changedBlock) { + changedBlocks.add(changedBlock); + } + + DiskPlan build() { + List copyBlocks = coalesceChangedBlocks(changedBlocks); + return new DiskPlan(disk, copyBlocks, sumChangedBytes(copyBlocks)); + } + + private List coalesceChangedBlocks(List changedBlocks) { + if (CollectionUtils.isEmpty(changedBlocks)) { + return Collections.emptyList(); + } + + List sortedBlocks = new ArrayList<>(changedBlocks); + sortedBlocks.sort(Comparator.comparingLong(VmwareCbtChangedBlockRangeTO::getStartOffset) + .thenComparingLong(VmwareCbtChangedBlockRangeTO::getLength)); + + List copyBlocks = new ArrayList<>(); + String diskId = disk.getDiskId(); + long currentStart = sortedBlocks.get(0).getStartOffset(); + long currentEnd = currentStart + sortedBlocks.get(0).getLength(); + + for (int i = 1; i < sortedBlocks.size(); i++) { + VmwareCbtChangedBlockRangeTO changedBlock = sortedBlocks.get(i); + long blockStart = changedBlock.getStartOffset(); + long blockEnd = blockStart + changedBlock.getLength(); + if (blockStart <= currentEnd) { + currentEnd = Math.max(currentEnd, blockEnd); + continue; + } + + copyBlocks.add(new VmwareCbtChangedBlockRangeTO(diskId, currentStart, currentEnd - currentStart)); + currentStart = blockStart; + currentEnd = blockEnd; + } + copyBlocks.add(new VmwareCbtChangedBlockRangeTO(diskId, currentStart, currentEnd - currentStart)); + return copyBlocks; + } + + private long sumChangedBytes(List changedBlocks) { + long changedBytes = 0; + for (VmwareCbtChangedBlockRangeTO changedBlock : changedBlocks) { + changedBytes += changedBlock.getLength(); + } + return changedBytes; + } + } + + private static class ValidationResult { + + private final boolean valid; + private final String error; + + private ValidationResult(boolean valid, String error) { + this.valid = valid; + this.error = error; + } + + static ValidationResult valid() { + return new ValidationResult(true, null); + } + + static ValidationResult invalid(String error) { + return new ValidationResult(false, error); + } + } +} diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtVmwareCbtSyncCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtVmwareCbtSyncCommandWrapperTest.java new file mode 100644 index 000000000000..69a263f5e3b5 --- /dev/null +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtVmwareCbtSyncCommandWrapperTest.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 com.cloud.hypervisor.kvm.resource.wrapper; + +import java.util.Collections; +import java.util.List; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.VmwareCbtMigrationAnswer; +import com.cloud.agent.api.VmwareCbtSyncCommand; +import com.cloud.agent.api.to.VmwareCbtChangedBlockRangeTO; +import com.cloud.agent.api.to.VmwareCbtDiskTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; + +@RunWith(MockitoJUnitRunner.class) +public class LibvirtVmwareCbtSyncCommandWrapperTest { + + private final LibvirtVmwareCbtSyncCommandWrapper wrapper = new LibvirtVmwareCbtSyncCommandWrapper(); + + @Mock + private LibvirtComputingResource libvirtComputingResource; + + @Before + public void setUp() { + Mockito.when(libvirtComputingResource.hostSupportsVmwareCbtMigration(Mockito.isNull())).thenReturn(true); + } + + @Test + public void testExecuteNoChangedBlocksReturnsReadyForCutover() { + VmwareCbtSyncCommand command = createCommand(Collections.emptyList(), Collections.emptyList()); + + Answer answer = wrapper.execute(command, libvirtComputingResource); + + Assert.assertTrue(answer.getResult()); + Assert.assertTrue(((VmwareCbtMigrationAnswer) answer).getReadyForCutover()); + } + + @Test + public void testExecuteRejectsChangedBlocksWithoutTargetPath() { + VmwareCbtDiskTO disk = createDisk("disk-1", null, 8192); + VmwareCbtSyncCommand command = createCommand(List.of(disk), + List.of(new VmwareCbtChangedBlockRangeTO("disk-1", 0, 1024))); + + Answer answer = wrapper.execute(command, libvirtComputingResource); + + Assert.assertFalse(answer.getResult()); + Assert.assertTrue(answer.getDetails().contains("no target path")); + } + + @Test + public void testExecuteReportsValidatedChangedBlocks() { + VmwareCbtDiskTO disk = createDisk("disk-1", "/var/lib/libvirt/images/disk-1.qcow2", 8192); + VmwareCbtSyncCommand command = createCommand(List.of(disk), + List.of(new VmwareCbtChangedBlockRangeTO("disk-1", 0, 1024))); + + Answer answer = wrapper.execute(command, libvirtComputingResource); + + Assert.assertFalse(answer.getResult()); + Assert.assertEquals(1024, ((VmwareCbtMigrationAnswer) answer).getChangedBytes()); + Assert.assertTrue(answer.getDetails().contains("validated 1 changed block range")); + } + + @Test + public void testExecuteUsesCommandVddkLibDirOverrideForSupportCheck() { + VmwareCbtDiskTO disk = createDisk("disk-1", "/var/lib/libvirt/images/disk-1.qcow2", 8192); + VmwareCbtSyncCommand command = createCommand(List.of(disk), + List.of(new VmwareCbtChangedBlockRangeTO("disk-1", 0, 1024))); + command.setVddkLibDir("/opt/vmware-vddk/override"); + Mockito.when(libvirtComputingResource.hostSupportsVmwareCbtMigration("/opt/vmware-vddk/override")) + .thenReturn(true); + + Answer answer = wrapper.execute(command, libvirtComputingResource); + + Assert.assertFalse(answer.getResult()); + Assert.assertTrue(answer.getDetails().contains("validated 1 changed block range")); + Mockito.verify(libvirtComputingResource).hostSupportsVmwareCbtMigration("/opt/vmware-vddk/override"); + } + + private VmwareCbtSyncCommand createCommand(List disks, List changedBlocks) { + return new VmwareCbtSyncCommand("migration-uuid", null, disks, changedBlocks, 1, "snapshot-1", false); + } + + private VmwareCbtDiskTO createDisk(String diskId, String targetPath, long capacityBytes) { + return new VmwareCbtDiskTO(diskId, 2000, String.format("[%s] vm/%s.vmdk", diskId, diskId), + "datastore1", targetPath, "qcow2", "*", null, capacityBytes); + } +} diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/VmwareCbtSyncPlanTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/VmwareCbtSyncPlanTest.java new file mode 100644 index 000000000000..16cc6c4d2530 --- /dev/null +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/VmwareCbtSyncPlanTest.java @@ -0,0 +1,98 @@ +// 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.hypervisor.kvm.resource.wrapper; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import com.cloud.agent.api.to.VmwareCbtChangedBlockRangeTO; +import com.cloud.agent.api.to.VmwareCbtDiskTO; + +public class VmwareCbtSyncPlanTest { + + @Test + public void testCreateGroupsChangedBlocksByDisk() { + VmwareCbtDiskTO disk1 = createDisk("disk-1", "/var/lib/libvirt/images/disk-1.qcow2", 8192); + VmwareCbtDiskTO disk2 = createDisk("disk-2", "/var/lib/libvirt/images/disk-2.qcow2", 8192); + + VmwareCbtSyncPlan syncPlan = VmwareCbtSyncPlan.create(List.of(disk1, disk2), List.of( + new VmwareCbtChangedBlockRangeTO("disk-1", 0, 1024), + new VmwareCbtChangedBlockRangeTO("disk-1", 4096, 512), + new VmwareCbtChangedBlockRangeTO("disk-2", 2048, 2048))); + + Assert.assertTrue(syncPlan.isValid()); + Assert.assertEquals(3, syncPlan.getChangedRangeCount()); + Assert.assertEquals(3, syncPlan.getCopyRangeCount()); + Assert.assertEquals(3584, syncPlan.getChangedBytes()); + Assert.assertEquals(2, syncPlan.getDiskPlans().size()); + Assert.assertEquals(1536, syncPlan.getDiskPlans().get(0).getChangedBytes()); + Assert.assertEquals(2048, syncPlan.getDiskPlans().get(1).getChangedBytes()); + } + + @Test + public void testCreateCoalescesAdjacentAndOverlappingRanges() { + VmwareCbtSyncPlan syncPlan = VmwareCbtSyncPlan.create(List.of(createDisk("disk-1", "/target", 8192)), List.of( + new VmwareCbtChangedBlockRangeTO("disk-1", 4096, 512), + new VmwareCbtChangedBlockRangeTO("disk-1", 0, 1024), + new VmwareCbtChangedBlockRangeTO("disk-1", 1024, 1024), + new VmwareCbtChangedBlockRangeTO("disk-1", 4352, 1024))); + + Assert.assertTrue(syncPlan.isValid()); + Assert.assertEquals(4, syncPlan.getChangedRangeCount()); + Assert.assertEquals(2, syncPlan.getCopyRangeCount()); + Assert.assertEquals(3328, syncPlan.getChangedBytes()); + Assert.assertEquals(2, syncPlan.getDiskPlans().get(0).getChangedBlocks().size()); + Assert.assertEquals(0, syncPlan.getDiskPlans().get(0).getChangedBlocks().get(0).getStartOffset()); + Assert.assertEquals(2048, syncPlan.getDiskPlans().get(0).getChangedBlocks().get(0).getLength()); + Assert.assertEquals(4096, syncPlan.getDiskPlans().get(0).getChangedBlocks().get(1).getStartOffset()); + Assert.assertEquals(1280, syncPlan.getDiskPlans().get(0).getChangedBlocks().get(1).getLength()); + } + + @Test + public void testCreateRejectsUnknownDisk() { + VmwareCbtSyncPlan syncPlan = VmwareCbtSyncPlan.create(List.of(createDisk("disk-1", "/target", 8192)), + List.of(new VmwareCbtChangedBlockRangeTO("disk-2", 0, 1024))); + + Assert.assertFalse(syncPlan.isValid()); + Assert.assertTrue(syncPlan.getValidationError().contains("unknown disk disk-2")); + } + + @Test + public void testCreateRejectsMissingTargetPath() { + VmwareCbtSyncPlan syncPlan = VmwareCbtSyncPlan.create(List.of(createDisk("disk-1", null, 8192)), + List.of(new VmwareCbtChangedBlockRangeTO("disk-1", 0, 1024))); + + Assert.assertFalse(syncPlan.isValid()); + Assert.assertTrue(syncPlan.getValidationError().contains("no target path")); + } + + @Test + public void testCreateRejectsOutOfBoundsRange() { + VmwareCbtSyncPlan syncPlan = VmwareCbtSyncPlan.create(List.of(createDisk("disk-1", "/target", 1024)), + List.of(new VmwareCbtChangedBlockRangeTO("disk-1", 512, 1024))); + + Assert.assertFalse(syncPlan.isValid()); + Assert.assertTrue(syncPlan.getValidationError().contains("exceeds disk capacity")); + } + + private VmwareCbtDiskTO createDisk(String diskId, String targetPath, long capacityBytes) { + return new VmwareCbtDiskTO(diskId, 2000, String.format("[%s] vm/%s.vmdk", diskId, diskId), + "datastore1", targetPath, "qcow2", "*", null, capacityBytes); + } +} diff --git a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/manager/VmwareCbtMigrationServiceImpl.java b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/manager/VmwareCbtMigrationServiceImpl.java new file mode 100644 index 000000000000..05843cbe76a9 --- /dev/null +++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/manager/VmwareCbtMigrationServiceImpl.java @@ -0,0 +1,356 @@ +// 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.hypervisor.vmware.manager; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.vm.UnmanagedInstanceTO; +import org.apache.cloudstack.vm.VmwareCbtChangedBlockInfo; +import org.apache.cloudstack.vm.VmwareCbtChangedDiskInfo; +import org.apache.cloudstack.vm.VmwareCbtDiskInfo; +import org.apache.cloudstack.vm.VmwareCbtMigrationService; +import org.apache.cloudstack.vm.VmwareCbtSnapshotInfo; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.cloud.hypervisor.vmware.mo.DatacenterMO; +import com.cloud.hypervisor.vmware.mo.HostMO; +import com.cloud.hypervisor.vmware.mo.VirtualMachineMO; +import com.cloud.hypervisor.vmware.mo.VmwareHypervisorHost; +import com.cloud.hypervisor.vmware.resource.VmwareContextFactory; +import com.cloud.hypervisor.vmware.util.VmwareContext; +import com.cloud.hypervisor.vmware.util.VmwareHelper; +import com.cloud.utils.exception.CloudRuntimeException; +import com.vmware.vim25.ManagedObjectReference; +import com.vmware.vim25.VirtualDevice; +import com.vmware.vim25.VirtualDisk; + +public class VmwareCbtMigrationServiceImpl implements VmwareCbtMigrationService { + + private static final Logger LOGGER = LogManager.getLogger(VmwareCbtMigrationServiceImpl.class); + + @Override + public List listSourceDisks(String vcenter, String datacenterName, String username, String password, + String sourceHost, String sourceVmName) { + try { + VmwareContext context = VmwareContextFactory.getContext(vcenter, username, password); + VmwareVmLookup lookup = lookupVirtualMachine(context, vcenter, datacenterName, sourceHost, sourceVmName); + UnmanagedInstanceTO unmanagedInstance = VmwareHelper.getUnmanagedInstance(lookup.hyperHost, lookup.vmMO); + return toVmwareCbtDiskInfo(unmanagedInstance, collectDiskDeviceInfo(lookup.vmMO)); + } catch (Exception e) { + String message = String.format("Unable to discover VMware CBT source disks for VM %s in vCenter %s: %s", + sourceVmName, vcenter, e.getMessage()); + LOGGER.error(message, e); + throw new CloudRuntimeException(message, e); + } + } + + @Override + public VmwareCbtSnapshotInfo createSnapshot(String vcenter, String datacenterName, String username, String password, + String sourceHost, String sourceVmName, String snapshotName, + String snapshotDescription, boolean quiesce) { + try { + VmwareContext context = VmwareContextFactory.getContext(vcenter, username, password); + VmwareVmLookup lookup = lookupVirtualMachine(context, vcenter, datacenterName, sourceHost, sourceVmName); + ManagedObjectReference snapshot = lookup.vmMO.createSnapshotGetReference(snapshotName, + snapshotDescription, false, quiesce); + if (snapshot == null) { + throw new CloudRuntimeException(String.format("Unable to create VMware snapshot %s for VM %s", + snapshotName, sourceVmName)); + } + return new VmwareCbtSnapshotInfo(snapshotName, formatManagedObjectReference(snapshot)); + } catch (Exception e) { + String message = String.format("Unable to create VMware CBT snapshot for VM %s in vCenter %s: %s", + sourceVmName, vcenter, e.getMessage()); + LOGGER.error(message, e); + throw new CloudRuntimeException(message, e); + } + } + + @Override + public List queryChangedDiskAreas(String vcenter, String datacenterName, String username, + String password, String sourceHost, String sourceVmName, + List disks, String snapshotMor) { + try { + VmwareContext context = VmwareContextFactory.getContext(vcenter, username, password); + VmwareVmLookup lookup = lookupVirtualMachine(context, vcenter, datacenterName, sourceHost, sourceVmName); + ManagedObjectReference snapshot = toManagedObjectReference("VirtualMachineSnapshot", snapshotMor); + List changedDisks = new ArrayList<>(); + if (CollectionUtils.isEmpty(disks)) { + return changedDisks; + } + for (VmwareCbtDiskInfo disk : disks) { + changedDisks.add(queryChangedDiskAreas(context, lookup.vmMO, snapshot, disk)); + } + return changedDisks; + } catch (Exception e) { + String message = String.format("Unable to query VMware CBT changed areas for VM %s in vCenter %s: %s", + sourceVmName, vcenter, e.getMessage()); + LOGGER.error(message, e); + throw new CloudRuntimeException(message, e); + } + } + + @Override + public void removeSnapshot(String vcenter, String datacenterName, String username, String password, + String sourceHost, String sourceVmName, String snapshotMor) { + if (StringUtils.isBlank(snapshotMor)) { + return; + } + + try { + VmwareContext context = VmwareContextFactory.getContext(vcenter, username, password); + lookupVirtualMachine(context, vcenter, datacenterName, sourceHost, sourceVmName); + ManagedObjectReference snapshot = toManagedObjectReference("VirtualMachineSnapshot", snapshotMor); + ManagedObjectReference task = context.getService().removeSnapshotTask(snapshot, false, true); + if (!context.getVimClient().waitForTask(task)) { + throw new CloudRuntimeException(String.format("Unable to remove VMware snapshot %s for VM %s", + snapshotMor, sourceVmName)); + } + context.waitForTaskProgressDone(task); + } catch (Exception e) { + String message = String.format("Unable to remove VMware CBT snapshot %s for VM %s in vCenter %s: %s", + snapshotMor, sourceVmName, vcenter, e.getMessage()); + LOGGER.error(message, e); + throw new CloudRuntimeException(message, e); + } + } + + private VmwareVmLookup lookupVirtualMachine(VmwareContext context, String vcenter, String datacenterName, + String sourceHost, String sourceVmName) throws Exception { + DatacenterMO datacenterMO = new DatacenterMO(context, datacenterName); + if (datacenterMO.getMor() == null) { + throw new CloudRuntimeException(String.format("Unable to find VMware datacenter %s in vCenter %s", + datacenterName, vcenter)); + } + + VmwareHypervisorHost hyperHost; + VirtualMachineMO vmMO; + if (StringUtils.isNotBlank(sourceHost)) { + ManagedObjectReference hostMor = datacenterMO.findHost(sourceHost); + if (hostMor == null) { + throw new CloudRuntimeException(String.format("Unable to find VMware host %s in vCenter %s", + sourceHost, vcenter)); + } + HostMO hostMO = new HostMO(context, hostMor); + vmMO = hostMO.findVmOnHyperHost(sourceVmName); + hyperHost = hostMO; + } else { + vmMO = datacenterMO.findVm(sourceVmName); + hyperHost = vmMO != null ? vmMO.getRunningHost() : null; + } + + if (vmMO == null) { + throw new CloudRuntimeException(String.format("Unable to find VMware VM %s in datacenter %s", + sourceVmName, datacenterName)); + } + return new VmwareVmLookup(hyperHost, vmMO); + } + + private List toVmwareCbtDiskInfo(UnmanagedInstanceTO unmanagedInstance, + Map diskDeviceInfo) { + List disks = new ArrayList<>(); + if (unmanagedInstance == null || CollectionUtils.isEmpty(unmanagedInstance.getDisks())) { + return disks; + } + + for (UnmanagedInstanceTO.Disk disk : unmanagedInstance.getDisks()) { + String sourceDiskId = StringUtils.defaultIfBlank(disk.getDiskId(), disk.getLabel()); + String sourceDiskPath = StringUtils.defaultIfBlank(disk.getImagePath(), disk.getFileBaseName()); + DiskDeviceInfo deviceInfo = diskDeviceInfo.get(sourceDiskId); + if (deviceInfo == null) { + deviceInfo = diskDeviceInfo.get(sourceDiskPath); + } + disks.add(new VmwareCbtDiskInfo(sourceDiskId, deviceInfo != null ? deviceInfo.deviceKey : null, + disk.getLabel(), sourceDiskPath, disk.getDatastoreName(), disk.getCapacity(), + deviceInfo != null ? deviceInfo.changeId : null)); + } + return disks; + } + + private Map collectDiskDeviceInfo(VirtualMachineMO vmMO) throws Exception { + Map diskDeviceInfo = new HashMap<>(); + VirtualDisk[] disks = vmMO.getAllDiskDevice(); + if (disks == null) { + return diskDeviceInfo; + } + + for (VirtualDevice device : disks) { + if (!(device instanceof VirtualDisk)) { + continue; + } + VirtualDisk disk = (VirtualDisk) device; + DiskDeviceInfo deviceInfo = new DiskDeviceInfo(disk.getKey(), + getBackingStringValue(disk.getBacking(), "getChangeId")); + if (StringUtils.isNotBlank(disk.getDiskObjectId())) { + diskDeviceInfo.put(disk.getDiskObjectId(), deviceInfo); + } + String fileName = getBackingStringValue(disk.getBacking(), "getFileName"); + if (StringUtils.isNotBlank(fileName)) { + diskDeviceInfo.put(fileName, deviceInfo); + } + } + return diskDeviceInfo; + } + + private VmwareCbtChangedDiskInfo queryChangedDiskAreas(VmwareContext context, VirtualMachineMO vmMO, + ManagedObjectReference snapshot, VmwareCbtDiskInfo disk) + throws ReflectiveOperationException { + if (disk.getSourceDiskDeviceKey() == null) { + throw new CloudRuntimeException(String.format("VMware disk device key is missing for source disk %s", + disk.getSourceDiskId())); + } + if (StringUtils.isBlank(disk.getChangeId())) { + throw new CloudRuntimeException(String.format("VMware CBT change ID is missing for source disk %s", + disk.getSourceDiskId())); + } + + List changedBlocks = new ArrayList<>(); + long startOffset = 0L; + long capacityBytes = disk.getCapacityBytes() == null ? 0L : disk.getCapacityBytes(); + String nextChangeId = null; + + do { + Object diskChangeInfo = invokeQueryChangedDiskAreas(context, vmMO, snapshot, disk, startOffset); + nextChangeId = StringUtils.defaultIfBlank(getObjectStringValue(diskChangeInfo, "getChangeId"), + nextChangeId); + for (Object changedArea : getObjectListValue(diskChangeInfo, "getChangedArea")) { + Long areaStart = getObjectLongValue(changedArea, "getStart"); + Long areaLength = getObjectLongValue(changedArea, "getLength"); + if (areaStart != null && areaLength != null && areaLength > 0L) { + changedBlocks.add(new VmwareCbtChangedBlockInfo(areaStart, areaLength)); + } + } + + Long responseStart = getObjectLongValue(diskChangeInfo, "getStartOffset"); + Long responseLength = getObjectLongValue(diskChangeInfo, "getLength"); + if (responseStart == null || responseLength == null || responseLength <= 0L) { + break; + } + startOffset = responseStart + responseLength; + } while (capacityBytes > 0L && startOffset < capacityBytes); + + return new VmwareCbtChangedDiskInfo(disk.getSourceDiskId(), nextChangeId, changedBlocks); + } + + private Object invokeQueryChangedDiskAreas(VmwareContext context, VirtualMachineMO vmMO, + ManagedObjectReference snapshot, VmwareCbtDiskInfo disk, + long startOffset) throws ReflectiveOperationException { + Method method = context.getService().getClass().getMethod("queryChangedDiskAreas", + ManagedObjectReference.class, ManagedObjectReference.class, int.class, long.class, String.class); + return method.invoke(context.getService(), vmMO.getMor(), snapshot, disk.getSourceDiskDeviceKey(), + startOffset, disk.getChangeId()); + } + + private ManagedObjectReference toManagedObjectReference(String defaultType, String mor) { + if (StringUtils.isBlank(mor)) { + return null; + } + + ManagedObjectReference reference = new ManagedObjectReference(); + if (mor.contains(":")) { + String[] parts = mor.split(":", 2); + reference.setType(parts[0]); + reference.setValue(parts[1]); + } else { + reference.setType(defaultType); + reference.setValue(mor); + } + return reference; + } + + private String formatManagedObjectReference(ManagedObjectReference mor) { + if (mor == null) { + return null; + } + return String.format("%s:%s", mor.getType(), mor.getValue()); + } + + private String getBackingStringValue(Object backing, String methodName) { + if (backing == null) { + return null; + } + + try { + Method method = backing.getClass().getMethod(methodName); + Object value = method.invoke(backing); + return value != null ? value.toString() : null; + } catch (ReflectiveOperationException e) { + return null; + } + } + + private String getObjectStringValue(Object object, String methodName) { + Object value = invokeGetter(object, methodName); + return value != null ? value.toString() : null; + } + + private Long getObjectLongValue(Object object, String methodName) { + Object value = invokeGetter(object, methodName); + if (value instanceof Number) { + return ((Number)value).longValue(); + } + return null; + } + + private List getObjectListValue(Object object, String methodName) { + Object value = invokeGetter(object, methodName); + if (value instanceof List) { + return (List)value; + } + return new ArrayList<>(); + } + + private Object invokeGetter(Object object, String methodName) { + if (object == null) { + return null; + } + + try { + Method method = object.getClass().getMethod(methodName); + return method.invoke(object); + } catch (ReflectiveOperationException e) { + return null; + } + } + + private static class DiskDeviceInfo { + private final Integer deviceKey; + private final String changeId; + + private DiskDeviceInfo(Integer deviceKey, String changeId) { + this.deviceKey = deviceKey; + this.changeId = changeId; + } + } + + private static class VmwareVmLookup { + private final VmwareHypervisorHost hyperHost; + private final VirtualMachineMO vmMO; + + private VmwareVmLookup(VmwareHypervisorHost hyperHost, VirtualMachineMO vmMO) { + this.hyperHost = hyperHost; + this.vmMO = vmMO; + } + } +} diff --git a/plugins/hypervisors/vmware/src/main/resources/META-INF/cloudstack/core/spring-vmware-core-context.xml b/plugins/hypervisors/vmware/src/main/resources/META-INF/cloudstack/core/spring-vmware-core-context.xml index d955ede30254..28825e987dd8 100644 --- a/plugins/hypervisors/vmware/src/main/resources/META-INF/cloudstack/core/spring-vmware-core-context.xml +++ b/plugins/hypervisors/vmware/src/main/resources/META-INF/cloudstack/core/spring-vmware-core-context.xml @@ -29,6 +29,8 @@ + = maxCycles) { + return Decision.READY_FOR_CUTOVER_MAX_CYCLES; + } + if (completedCycles < minCycles) { + return Decision.CONTINUE; + } + if (lastChangedBytes == 0) { + return Decision.READY_FOR_CUTOVER; + } + int updatedQuietCycles = isQuietCycle(lastChangedBytes, lastCycleDurationSeconds) ? quietCycles + 1 : 0; + if (updatedQuietCycles >= quietCyclesRequired) { + return Decision.READY_FOR_CUTOVER; + } + return Decision.CONTINUE; + } + + public boolean isQuietCycle(long changedBytes, long cycleDurationSeconds) { + boolean withinChangedBytes = quietDirtyBytesThreshold <= 0 || changedBytes <= quietDirtyBytesThreshold; + boolean withinDirtyRate = quietDirtyRateBytesPerSecondThreshold <= 0 || + getDirtyRateBytesPerSecond(changedBytes, cycleDurationSeconds) <= quietDirtyRateBytesPerSecondThreshold; + return withinChangedBytes && withinDirtyRate; + } + + protected long getDirtyRateBytesPerSecond(long changedBytes, long cycleDurationSeconds) { + if (cycleDurationSeconds <= 0) { + return changedBytes; + } + return changedBytes / cycleDurationSeconds; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigrationManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigrationManagerImpl.java new file mode 100644 index 000000000000..95b0309145d5 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/VmwareCbtMigrationManagerImpl.java @@ -0,0 +1,1017 @@ +// 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.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.vm.CancelVmwareCbtMigrationCmd; +import org.apache.cloudstack.api.command.admin.vm.CutoverVmwareCbtMigrationCmd; +import org.apache.cloudstack.api.command.admin.vm.ListVmwareCbtMigrationsCmd; +import org.apache.cloudstack.api.command.admin.vm.RegisterVmwareCbtMigrationTargetCmd; +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.VmwareCbtMigrationCycleResponse; +import org.apache.cloudstack.api.response.VmwareCbtMigrationDiskResponse; +import org.apache.cloudstack.api.response.VmwareCbtMigrationResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.VmwareCbtCleanupCommand; +import com.cloud.agent.api.VmwareCbtCutoverCommand; +import com.cloud.agent.api.VmwareCbtMigrationAnswer; +import com.cloud.agent.api.VmwareCbtSyncCommand; +import com.cloud.agent.api.to.RemoteInstanceTO; +import com.cloud.agent.api.to.VmwareCbtChangedBlockRangeTO; +import com.cloud.agent.api.to.VmwareCbtDiskSyncResultTO; +import com.cloud.agent.api.to.VmwareCbtDiskTO; +import com.cloud.dc.ClusterVO; +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.VmwareDatacenterVO; +import com.cloud.dc.dao.ClusterDao; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.dc.dao.VmwareDatacenterDao; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.resource.ResourceState; +import com.cloud.user.Account; +import com.cloud.user.AccountService; +import com.cloud.utils.Pair; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VmwareCbtMigrationCycleVO; +import com.cloud.vm.VmwareCbtMigrationDiskVO; +import com.cloud.vm.VmwareCbtMigrationVO; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.VmwareCbtMigrationCycleDao; +import com.cloud.vm.dao.VmwareCbtMigrationDao; +import com.cloud.vm.dao.VmwareCbtMigrationDiskDao; + +public class VmwareCbtMigrationManagerImpl implements VmwareCbtMigrationManager, Configurable { + + private static final String OBJECT_NAME = "vmwarecbtmigration"; + private static final String DETAIL_VDDK_TRANSPORTS = "vddk.transports"; + private static final String DETAIL_VDDK_THUMBPRINT = "vddk.thumbprint"; + private static final Logger LOGGER = LogManager.getLogger(VmwareCbtMigrationManagerImpl.class); + + static final ConfigKey VmwareCbtMigrationMinCycles = new ConfigKey<>(Integer.class, + "vmware.cbt.migration.min.cycles", + "Advanced", + "1", + "Minimum number of CBT delta synchronization cycles to run before CloudStack can recommend final VMware to KVM cutover", + true, + ConfigKey.Scope.Global, + null); + + static final ConfigKey VmwareCbtMigrationMaxCycles = new ConfigKey<>(Integer.class, + "vmware.cbt.migration.max.cycles", + "Advanced", + "5", + "Maximum number of CBT delta synchronization cycles to run before CloudStack recommends final VMware to KVM cutover", + true, + ConfigKey.Scope.Global, + null); + + static final ConfigKey VmwareCbtMigrationQuietCycles = new ConfigKey<>(Integer.class, + "vmware.cbt.migration.quiet.cycles", + "Advanced", + "2", + "Number of consecutive quiet CBT delta synchronization cycles required before CloudStack recommends final VMware to KVM cutover", + true, + ConfigKey.Scope.Global, + null); + + static final ConfigKey VmwareCbtMigrationQuietBytes = new ConfigKey<>(Long.class, + "vmware.cbt.migration.quiet.bytes", + "Advanced", + "1073741824", + "Maximum changed bytes in a CBT delta synchronization cycle for the cycle to be considered quiet", + true, + ConfigKey.Scope.Global, + null); + + static final ConfigKey VmwareCbtMigrationQuietDirtyRate = new ConfigKey<>(Long.class, + "vmware.cbt.migration.quiet.dirty.rate", + "Advanced", + "16777216", + "Maximum changed bytes per second in a CBT delta synchronization cycle for the cycle to be considered quiet", + true, + ConfigKey.Scope.Global, + null); + + @Inject + private VmwareCbtMigrationDao vmwareCbtMigrationDao; + @Inject + private DataCenterDao dataCenterDao; + @Inject + private ClusterDao clusterDao; + @Inject + private HostDao hostDao; + @Inject + private PrimaryDataStoreDao primaryDataStoreDao; + @Inject + private VmwareDatacenterDao vmwareDatacenterDao; + @Inject + private AccountService accountService; + @Inject + private UserVmDao userVmDao; + @Inject + private VmwareCbtMigrationDiskDao vmwareCbtMigrationDiskDao; + @Inject + private VmwareCbtMigrationCycleDao vmwareCbtMigrationCycleDao; + @Inject + private AgentManager agentManager; + @Autowired(required = false) + private VmwareCbtMigrationService vmwareCbtMigrationService; + + @Override + public List> getCommands() { + final List> cmdList = new ArrayList<>(); + cmdList.add(StartVmwareCbtMigrationCmd.class); + cmdList.add(ListVmwareCbtMigrationsCmd.class); + cmdList.add(SyncVmwareCbtMigrationCmd.class); + cmdList.add(RegisterVmwareCbtMigrationTargetCmd.class); + cmdList.add(CutoverVmwareCbtMigrationCmd.class); + cmdList.add(CancelVmwareCbtMigrationCmd.class); + return cmdList; + } + + @Override + public VmwareCbtMigrationResponse startVmwareCbtMigration(StartVmwareCbtMigrationCmd cmd) { + Account caller = CallContext.current().getCallingAccount(); + if (caller == null) { + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, "Unable to determine calling account"); + } + + DataCenterVO zone = getZone(cmd.getZoneId()); + ClusterVO destinationCluster = getDestinationCluster(cmd.getClusterId(), zone.getId()); + HostVO convertHost = selectCbtHost(cmd.getConvertInstanceHostId(), destinationCluster); + StoragePoolVO storagePool = getStoragePool(cmd.getStoragePoolId(), zone, destinationCluster); + + String sourceVmName = StringUtils.trimToNull(cmd.getSourceVmName()); + if (sourceVmName == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Source VM name is required"); + } + + VmwareSource source = resolveVmwareSource(cmd); + List sourceDisks = discoverSourceDisks(source, sourceVmName); + String displayName = StringUtils.defaultIfBlank(cmd.getDisplayName(), sourceVmName); + + VmwareCbtMigrationVO migration = new VmwareCbtMigrationVO(zone.getId(), caller.getAccountId(), CallContext.current().getCallingUserId(), + destinationCluster.getId(), displayName, source.vcenter, source.datacenterName, cmd.getSourceHost(), cmd.getSourceCluster(), sourceVmName); + migration.setExistingVcenterId(source.existingVcenterId); + if (convertHost != null) { + migration.setConvertHostId(convertHost.getId()); + } + if (storagePool != null) { + migration.setStoragePoolId(storagePool.getId()); + } + applyVddkDetails(migration, cmd.getDetails()); + migration.setState(VmwareCbtMigration.State.InitialSync); + migration.setCurrentStep(String.format("Discovered %s source disk(s); waiting for initial VDDK full sync", sourceDisks.size())); + migration.setUpdated(new Date()); + migration = vmwareCbtMigrationDao.persist(migration); + persistSourceDisks(migration, sourceDisks); + return createVmwareCbtMigrationResponse(migration); + } + + @Override + public ListResponse listVmwareCbtMigrations(ListVmwareCbtMigrationsCmd cmd) { + VmwareCbtMigration.State state = parseState(cmd.getState()); + Pair, Integer> result = vmwareCbtMigrationDao.listMigrations(cmd.getId(), cmd.getZoneId(), cmd.getAccountId(), + cmd.getVcenter(), cmd.getSourceVmName(), state, cmd.getStartIndex(), cmd.getPageSizeVal()); + + List responses = new ArrayList<>(); + for (VmwareCbtMigrationVO migration : result.first()) { + responses.add(createVmwareCbtMigrationResponse(migration)); + } + ListResponse listResponse = new ListResponse<>(); + listResponse.setResponses(responses, result.second()); + return listResponse; + } + + @Override + public VmwareCbtMigrationResponse syncVmwareCbtMigration(SyncVmwareCbtMigrationCmd cmd) { + VmwareCbtMigrationVO migration = getMigration(cmd.getId()); + rejectTerminalMigration(migration, "synchronize"); + requireMigrationState(migration, "synchronize", VmwareCbtMigration.State.Replicating, + VmwareCbtMigration.State.ReadyForCutover); + VmwareSource source = resolveVmwareSource(migration, cmd.getUsername(), cmd.getPassword()); + validateInitialSyncTargetDisks(migration); + HostVO cbtHost = getCbtHostForMigration(migration); + int cycleNumber = migration.getCompletedCycles() + 1; + VmwareCbtMigrationCycleVO cycle = new VmwareCbtMigrationCycleVO(migration.getId(), cycleNumber); + cycle.setState(VmwareCbtMigrationCycle.State.CopyingChangedBlocks); + cycle.setDescription("Dispatching CBT delta synchronization to KVM agent"); + cycle.setUpdated(new Date()); + cycle = vmwareCbtMigrationCycleDao.persist(cycle); + + migration.setState(VmwareCbtMigration.State.Replicating); + migration.setCurrentStep(String.format("Running CBT delta synchronization cycle %s", cycleNumber)); + migration.setLastError(null); + migration.setUpdated(new Date()); + vmwareCbtMigrationDao.update(migration.getId(), migration); + + VmwareCbtSnapshotInfo snapshot = null; + try { + snapshot = createDeltaSnapshot(source, migration, cycleNumber); + cycle.setState(VmwareCbtMigrationCycle.State.QueryingChangedAreas); + cycle.setSnapshotMor(snapshot.getSnapshotMor()); + cycle.setDescription("Querying VMware CBT changed disk areas"); + cycle.setUpdated(new Date()); + vmwareCbtMigrationCycleDao.update(cycle.getId(), cycle); + + VmwareCbtChangedBlockQueryResult changedBlockQuery = queryChangedBlocks(source, migration, + snapshot.getSnapshotMor()); + + cycle.setState(VmwareCbtMigrationCycle.State.CopyingChangedBlocks); + cycle.setDescription(String.format("Dispatching %s VMware CBT changed block range(s) to KVM agent", + changedBlockQuery.changedBlocks.size())); + cycle.setUpdated(new Date()); + vmwareCbtMigrationCycleDao.update(cycle.getId(), cycle); + + VmwareCbtSyncCommand syncCommand = new VmwareCbtSyncCommand(migration.getUuid(), + createRemoteInstance(source, migration), getDiskTransferObjects(migration), + changedBlockQuery.changedBlocks, cycleNumber, snapshot.getSnapshotMor(), false); + applyVddkDetails(syncCommand, migration); + syncCommand.setWait(3600); + + VmwareCbtMigrationAnswer answer = sendVmwareCbtCommand(cbtHost, syncCommand, "synchronize", + migration.getUuid()); + if (!answer.getResult()) { + markCycleFailed(cycle, answer.getDetails()); + markMigrationFailed(migration, "CBT delta synchronization failed", answer.getDetails()); + return createVmwareCbtMigrationResponse(vmwareCbtMigrationDao.findById(migration.getId())); + } + + applyDiskResults(migration, answer.getDiskResults()); + updateDiskChangeIds(migration, changedBlockQuery.changedDisks); + long changedBytes = answer.getChangedBytes(); + long durationSeconds = answer.getDurationSeconds(); + long dirtyRate = getDirtyRateBytesPerSecond(changedBytes, durationSeconds, answer.getDirtyRateBytesPerSecond()); + VmwareCbtMigrationCutoverPolicy cutoverPolicy = getCutoverPolicy(); + VmwareCbtMigrationCutoverPolicy.Decision cutoverDecision = cutoverPolicy.decide(cycleNumber, + migration.getQuietCycles(), changedBytes, durationSeconds); + int quietCycles = cutoverPolicy.isQuietCycle(changedBytes, durationSeconds) ? + migration.getQuietCycles() + 1 : 0; + + cycle.setState(VmwareCbtMigrationCycle.State.Completed); + cycle.setChangedBytes(changedBytes); + cycle.setDirtyRate(dirtyRate); + cycle.setDuration(durationSeconds * 1000); + cycle.setDescription(answer.getDetails()); + cycle.setUpdated(new Date()); + vmwareCbtMigrationCycleDao.update(cycle.getId(), cycle); + + migration.setCompletedCycles(cycleNumber); + migration.setQuietCycles(quietCycles); + migration.setTotalChangedBytes(migration.getTotalChangedBytes() + changedBytes); + migration.setLastChangedBytes(changedBytes); + migration.setLastDirtyRate(dirtyRate); + migration.setState(cutoverDecision == VmwareCbtMigrationCutoverPolicy.Decision.CONTINUE ? + VmwareCbtMigration.State.Replicating : VmwareCbtMigration.State.ReadyForCutover); + migration.setCurrentStep(getCutoverDecisionStep(cutoverDecision)); + migration.setUpdated(new Date()); + vmwareCbtMigrationDao.update(migration.getId(), migration); + return createVmwareCbtMigrationResponse(migration); + } catch (RuntimeException e) { + String error = StringUtils.defaultIfBlank(e.getMessage(), e.getClass().getSimpleName()); + markCycleFailed(cycle, error); + markMigrationFailed(migration, "CBT delta synchronization failed", error); + return createVmwareCbtMigrationResponse(vmwareCbtMigrationDao.findById(migration.getId())); + } finally { + removeDeltaSnapshotIfPossible(source, migration, snapshot); + } + } + + @Override + public VmwareCbtMigrationResponse registerVmwareCbtMigrationTarget(RegisterVmwareCbtMigrationTargetCmd cmd) { + VmwareCbtMigrationVO migration = getMigration(cmd.getId()); + rejectTerminalMigration(migration, "register target disks for"); + requireMigrationState(migration, "register target disks for", VmwareCbtMigration.State.Created, + VmwareCbtMigration.State.InitialSync); + + int registeredDiskCount = registerTargetDisks(migration, cmd.getTargetDisks()); + int diskCount = vmwareCbtMigrationDiskDao.listByMigrationId(migration.getId()).size(); + boolean allTargetDisksRegistered = registeredDiskCount == diskCount; + + migration.setState(allTargetDisksRegistered ? VmwareCbtMigration.State.Replicating : + VmwareCbtMigration.State.InitialSync); + migration.setCurrentStep(allTargetDisksRegistered ? + "Initial full sync target disks registered; ready for CBT delta synchronization" : + String.format("Registered %s of %s initial full sync target disk(s)", registeredDiskCount, diskCount)); + migration.setLastError(null); + migration.setUpdated(new Date()); + vmwareCbtMigrationDao.update(migration.getId(), migration); + return createVmwareCbtMigrationResponse(migration); + } + + @Override + public VmwareCbtMigrationResponse cutoverVmwareCbtMigration(CutoverVmwareCbtMigrationCmd cmd) { + VmwareCbtMigrationVO migration = getMigration(cmd.getId()); + rejectTerminalMigration(migration, "cut over"); + requireMigrationState(migration, "cut over", VmwareCbtMigration.State.ReadyForCutover); + VmwareSource source = resolveVmwareSource(migration, cmd.getUsername(), cmd.getPassword()); + validateInitialSyncTargetDisks(migration); + HostVO cbtHost = getCbtHostForMigration(migration); + + migration.setState(VmwareCbtMigration.State.CuttingOver); + migration.setCurrentStep("Running final CBT cutover"); + migration.setLastError(null); + migration.setUpdated(new Date()); + vmwareCbtMigrationDao.update(migration.getId(), migration); + + VmwareCbtCutoverCommand cutoverCommand = new VmwareCbtCutoverCommand(migration.getUuid(), createRemoteInstance(source, migration), + getDiskTransferObjects(migration), migration.getCompletedCycles() + 1, true); + applyVddkDetails(cutoverCommand, migration); + cutoverCommand.setWait(3600); + + VmwareCbtMigrationAnswer answer = sendVmwareCbtCommand(cbtHost, cutoverCommand, "cut over", migration.getUuid()); + if (!answer.getResult()) { + markMigrationFailed(migration, "CBT cutover failed", answer.getDetails()); + return createVmwareCbtMigrationResponse(vmwareCbtMigrationDao.findById(migration.getId())); + } + + applyDiskResults(migration, answer.getDiskResults()); + migration.setState(VmwareCbtMigration.State.Completed); + migration.setCurrentStep("Completed"); + migration.setUpdated(new Date()); + vmwareCbtMigrationDao.update(migration.getId(), migration); + return createVmwareCbtMigrationResponse(migration); + } + + @Override + public VmwareCbtMigrationResponse cancelVmwareCbtMigration(CancelVmwareCbtMigrationCmd cmd) { + VmwareCbtMigrationVO migration = getMigration(cmd.getId()); + if (!migration.getState().isTerminal()) { + sendCleanupCommandIfPossible(migration); + migration.setState(VmwareCbtMigration.State.Cancelled); + migration.setCurrentStep("Cancelled"); + migration.setUpdated(new Date()); + vmwareCbtMigrationDao.update(migration.getId(), migration); + } + return createVmwareCbtMigrationResponse(migration); + } + + private DataCenterVO getZone(Long zoneId) { + DataCenterVO zone = dataCenterDao.findById(zoneId); + if (zone == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Unable to find zone with ID %s", zoneId)); + } + return zone; + } + + private ClusterVO getDestinationCluster(Long clusterId, long zoneId) { + ClusterVO cluster = clusterDao.findById(clusterId); + if (cluster == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Unable to find cluster with ID %s", clusterId)); + } + if (cluster.getDataCenterId() != zoneId) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Destination cluster does not belong to the requested zone"); + } + if (Hypervisor.HypervisorType.KVM != cluster.getHypervisorType()) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Destination cluster must be a KVM cluster for VMware CBT migration"); + } + return cluster; + } + + private HostVO selectCbtHost(Long hostId, ClusterVO destinationCluster) { + if (hostId == null) { + List hosts = hostDao.listByClusterHypervisorTypeAndHostCapability(destinationCluster.getId(), + destinationCluster.getHypervisorType(), Host.HOST_VMWARE_CBT_SUPPORT); + if (CollectionUtils.isEmpty(hosts)) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + String.format("Could not find an enabled KVM host in cluster %s that reports VMware CBT migration support", destinationCluster.getName())); + } + return hosts.get(0); + } + HostVO host = hostDao.findById(hostId); + if (host == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Unable to find CBT migration host with ID %s", hostId)); + } + if (host.getClusterId() == null || host.getClusterId() != destinationCluster.getId()) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "CBT migration host must belong to the destination KVM cluster"); + } + if (Hypervisor.HypervisorType.KVM != host.getHypervisorType()) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "CBT migration host must be a KVM host"); + } + if (host.getType() != Host.Type.Routing || host.getStatus() != Status.Up || host.getResourceState() != ResourceState.Enabled) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "CBT migration host must be an enabled, running routing host"); + } + hostDao.loadDetails(host); + if (!Boolean.parseBoolean(host.getDetail(Host.HOST_VMWARE_CBT_SUPPORT))) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + String.format("CBT migration host %s does not report VMware CBT migration support", host.getName())); + } + return host; + } + + private StoragePoolVO getStoragePool(Long storagePoolId, DataCenterVO zone, ClusterVO destinationCluster) { + if (storagePoolId == null) { + return null; + } + StoragePoolVO pool = primaryDataStoreDao.findById(storagePoolId); + if (pool == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Unable to find storage pool with ID %s", storagePoolId)); + } + if (pool.getDataCenterId() != zone.getId()) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Storage pool does not belong to the requested zone"); + } + if (pool.getClusterId() != null && !pool.getClusterId().equals(destinationCluster.getId())) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Cluster-scoped storage pool must belong to the destination KVM cluster"); + } + return pool; + } + + private VmwareSource resolveVmwareSource(StartVmwareCbtMigrationCmd cmd) { + if ((cmd.getExistingVcenterId() == null && StringUtils.isBlank(cmd.getVcenter())) || + (cmd.getExistingVcenterId() != null && StringUtils.isNotBlank(cmd.getVcenter()))) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Please provide an existing vCenter ID or a vCenter IP/Name, parameters are mutually exclusive"); + } + + if (cmd.getExistingVcenterId() != null) { + VmwareDatacenterVO existingDc = vmwareDatacenterDao.findById(cmd.getExistingVcenterId()); + if (existingDc == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + String.format("Cannot find any existing VMware datacenter with ID %s", cmd.getExistingVcenterId())); + } + return new VmwareSource(existingDc.getId(), existingDc.getVcenterHost(), existingDc.getVmwareDatacenterName(), + existingDc.getUser(), existingDc.getPassword(), cmd.getSourceHost()); + } + + if (StringUtils.isAnyBlank(cmd.getVcenter(), cmd.getDatacenterName(), cmd.getUsername(), cmd.getPassword())) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + "Please set all the information for a vCenter IP/Name, datacenter, username and password"); + } + return new VmwareSource(null, cmd.getVcenter(), cmd.getDatacenterName(), cmd.getUsername(), cmd.getPassword(), + cmd.getSourceHost()); + } + + private VmwareSource resolveVmwareSource(VmwareCbtMigrationVO migration, String username, String password) { + if (migration.getExistingVcenterId() != null) { + VmwareDatacenterVO existingDc = vmwareDatacenterDao.findById(migration.getExistingVcenterId()); + if (existingDc == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + String.format("Cannot find any existing VMware datacenter with ID %s", + migration.getExistingVcenterId())); + } + return new VmwareSource(existingDc.getId(), existingDc.getVcenterHost(), existingDc.getVmwareDatacenterName(), + existingDc.getUser(), existingDc.getPassword(), migration.getSourceHost()); + } + + if (StringUtils.isAnyBlank(migration.getVcenter(), migration.getDatacenter(), username, password)) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + "Please provide source vCenter username and password for this VMware CBT migration"); + } + return new VmwareSource(null, migration.getVcenter(), migration.getDatacenter(), username, password, + migration.getSourceHost()); + } + + private VmwareCbtMigration.State parseState(String state) { + if (StringUtils.isBlank(state)) { + return null; + } + try { + return VmwareCbtMigration.State.getValue(state); + } catch (IllegalArgumentException e) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, e.getMessage()); + } + } + + private VmwareCbtMigrationVO getMigration(Long id) { + VmwareCbtMigrationVO migration = vmwareCbtMigrationDao.findById(id); + if (migration == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Unable to find VMware CBT migration with ID %s", id)); + } + return migration; + } + + private void rejectTerminalMigration(VmwareCbtMigrationVO migration, String action) { + if (migration.getState().isTerminal()) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + String.format("Cannot %s VMware CBT migration %s because it is already in state %s", + action, migration.getUuid(), migration.getState())); + } + } + + private void requireMigrationState(VmwareCbtMigrationVO migration, String action, + VmwareCbtMigration.State... allowedStates) { + for (VmwareCbtMigration.State allowedState : allowedStates) { + if (migration.getState() == allowedState) { + return; + } + } + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + String.format("Cannot %s VMware CBT migration %s while it is in state %s. Expected state: %s", + action, migration.getUuid(), migration.getState(), StringUtils.join(allowedStates, ", "))); + } + + private List discoverSourceDisks(VmwareSource source, String sourceVmName) { + List sourceDisks = getVmwareCbtMigrationService().listSourceDisks(source.vcenter, source.datacenterName, + source.username, source.password, source.sourceHost, sourceVmName); + if (CollectionUtils.isEmpty(sourceDisks)) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, + String.format("No source disks were discovered for VMware VM %s", sourceVmName)); + } + return sourceDisks; + } + + private void persistSourceDisks(VmwareCbtMigrationVO migration, List sourceDisks) { + for (VmwareCbtDiskInfo sourceDisk : sourceDisks) { + VmwareCbtMigrationDiskVO disk = new VmwareCbtMigrationDiskVO(migration.getId(), + StringUtils.defaultIfBlank(sourceDisk.getSourceDiskId(), sourceDisk.getLabel()), + sourceDisk.getSourceDiskDeviceKey(), sourceDisk.getSourceDiskPath(), sourceDisk.getDatastoreName(), + sourceDisk.getCapacityBytes()); + disk.setChangeId(sourceDisk.getChangeId()); + disk.setTargetFormat("qcow2"); + disk.setUpdated(new Date()); + vmwareCbtMigrationDiskDao.persist(disk); + } + } + + private void validateInitialSyncTargetDisks(VmwareCbtMigrationVO migration) { + List disks = vmwareCbtMigrationDiskDao.listByMigrationId(migration.getId()); + if (CollectionUtils.isEmpty(disks)) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + String.format("VMware CBT migration %s has no discovered source disks", migration.getUuid())); + } + for (VmwareCbtMigrationDiskVO disk : disks) { + if (StringUtils.isBlank(disk.getTargetPath())) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + String.format("VMware CBT migration %s cannot run delta sync before initial full sync registers target disk path for source disk %s", + migration.getUuid(), disk.getSourceDiskId())); + } + } + } + + private int registerTargetDisks(VmwareCbtMigrationVO migration, List targetDisks) { + if (CollectionUtils.isEmpty(targetDisks)) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "At least one target disk must be provided"); + } + + List disks = vmwareCbtMigrationDiskDao.listByMigrationId(migration.getId()); + Map disksBySourceDiskId = new HashMap<>(); + for (VmwareCbtMigrationDiskVO disk : disks) { + disksBySourceDiskId.put(disk.getSourceDiskId(), disk); + } + + for (VmwareCbtTargetDiskInfo targetDisk : targetDisks) { + VmwareCbtMigrationDiskVO disk = disksBySourceDiskId.get(targetDisk.getSourceDiskId()); + if (disk == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + String.format("Source disk %s is not tracked by VMware CBT migration %s", + targetDisk.getSourceDiskId(), migration.getUuid())); + } + disk.setTargetPath(targetDisk.getTargetPath()); + disk.setTargetFormat(StringUtils.defaultIfBlank(targetDisk.getTargetFormat(), "qcow2")); + if (StringUtils.isNotBlank(targetDisk.getChangeId())) { + disk.setChangeId(targetDisk.getChangeId()); + } + if (StringUtils.isNotBlank(targetDisk.getSnapshotMor())) { + disk.setSnapshotMor(targetDisk.getSnapshotMor()); + } + disk.setState(VmwareCbtMigrationDisk.State.Ready); + disk.setUpdated(new Date()); + vmwareCbtMigrationDiskDao.update(disk.getId(), disk); + } + + int registeredDiskCount = 0; + for (VmwareCbtMigrationDiskVO disk : vmwareCbtMigrationDiskDao.listByMigrationId(migration.getId())) { + if (StringUtils.isNotBlank(disk.getTargetPath())) { + registeredDiskCount++; + } + } + return registeredDiskCount; + } + + private VmwareCbtSnapshotInfo createDeltaSnapshot(VmwareSource source, VmwareCbtMigrationVO migration, + int cycleNumber) { + String snapshotName = String.format("cloudstack-cbt-%s-%s", migration.getUuid(), cycleNumber); + String description = String.format("CloudStack VMware CBT migration %s cycle %s", migration.getUuid(), + cycleNumber); + return getVmwareCbtMigrationService().createSnapshot(source.vcenter, source.datacenterName, source.username, + source.password, source.sourceHost, migration.getSourceVmName(), snapshotName, description, false); + } + + private VmwareCbtChangedBlockQueryResult queryChangedBlocks(VmwareSource source, VmwareCbtMigrationVO migration, + String snapshotMor) { + List disks = vmwareCbtMigrationDiskDao.listByMigrationId(migration.getId()); + List sourceDisks = new ArrayList<>(); + for (VmwareCbtMigrationDiskVO disk : disks) { + sourceDisks.add(new VmwareCbtDiskInfo(disk.getSourceDiskId(), disk.getSourceDiskDeviceKey(), null, + disk.getSourceDiskPath(), disk.getDatastoreName(), disk.getCapacityBytes(), disk.getChangeId())); + disk.setSnapshotMor(snapshotMor); + disk.setUpdated(new Date()); + vmwareCbtMigrationDiskDao.update(disk.getId(), disk); + } + + List changedDisks = getVmwareCbtMigrationService().queryChangedDiskAreas(source.vcenter, + source.datacenterName, source.username, source.password, source.sourceHost, + migration.getSourceVmName(), sourceDisks, snapshotMor); + List changedBlocks = new ArrayList<>(); + for (VmwareCbtChangedDiskInfo changedDisk : changedDisks) { + for (VmwareCbtChangedBlockInfo changedBlock : changedDisk.getChangedBlocks()) { + changedBlocks.add(new VmwareCbtChangedBlockRangeTO(changedDisk.getSourceDiskId(), + changedBlock.getStartOffset(), changedBlock.getLength())); + } + } + return new VmwareCbtChangedBlockQueryResult(changedBlocks, changedDisks); + } + + private void updateDiskChangeIds(VmwareCbtMigrationVO migration, List changedDisks) { + List disks = vmwareCbtMigrationDiskDao.listByMigrationId(migration.getId()); + for (VmwareCbtMigrationDiskVO disk : disks) { + for (VmwareCbtChangedDiskInfo changedDisk : changedDisks) { + if (StringUtils.isNotBlank(changedDisk.getNextChangeId()) && + StringUtils.equals(disk.getSourceDiskId(), changedDisk.getSourceDiskId())) { + disk.setChangeId(changedDisk.getNextChangeId()); + disk.setUpdated(new Date()); + vmwareCbtMigrationDiskDao.update(disk.getId(), disk); + break; + } + } + } + } + + private VmwareCbtMigrationCutoverPolicy getCutoverPolicy() { + return new VmwareCbtMigrationCutoverPolicy(VmwareCbtMigrationMinCycles.value(), + VmwareCbtMigrationMaxCycles.value(), VmwareCbtMigrationQuietCycles.value(), + VmwareCbtMigrationQuietBytes.value(), VmwareCbtMigrationQuietDirtyRate.value()); + } + + private long getDirtyRateBytesPerSecond(long changedBytes, long durationSeconds, long reportedDirtyRate) { + if (reportedDirtyRate > 0) { + return reportedDirtyRate; + } + if (durationSeconds <= 0) { + return changedBytes; + } + return changedBytes / durationSeconds; + } + + private String getCutoverDecisionStep(VmwareCbtMigrationCutoverPolicy.Decision cutoverDecision) { + switch (cutoverDecision) { + case READY_FOR_CUTOVER: + return "Ready for final cutover"; + case READY_FOR_CUTOVER_MAX_CYCLES: + return "Ready for final cutover after reaching maximum CBT delta cycles"; + case CONTINUE: + default: + return "CBT delta synchronization completed; continue replication cycles"; + } + } + + private void applyDiskResults(VmwareCbtMigrationVO migration, List diskResults) { + if (CollectionUtils.isEmpty(diskResults)) { + return; + } + + List disks = vmwareCbtMigrationDiskDao.listByMigrationId(migration.getId()); + for (VmwareCbtDiskSyncResultTO diskResult : diskResults) { + for (VmwareCbtMigrationDiskVO disk : disks) { + if (StringUtils.equals(disk.getSourceDiskId(), diskResult.getDiskId())) { + applyDiskResult(disk, diskResult); + break; + } + } + } + } + + private void applyDiskResult(VmwareCbtMigrationDiskVO disk, VmwareCbtDiskSyncResultTO diskResult) { + if (StringUtils.isNotBlank(diskResult.getTargetPath())) { + disk.setTargetPath(diskResult.getTargetPath()); + } + if (StringUtils.isNotBlank(diskResult.getChangeId())) { + disk.setChangeId(diskResult.getChangeId()); + } + if (StringUtils.isNotBlank(diskResult.getSnapshotMor())) { + disk.setSnapshotMor(diskResult.getSnapshotMor()); + } + disk.setState(diskResult.getResult() ? VmwareCbtMigrationDisk.State.Ready : VmwareCbtMigrationDisk.State.Failed); + disk.setUpdated(new Date()); + vmwareCbtMigrationDiskDao.update(disk.getId(), disk); + } + + private void removeDeltaSnapshotIfPossible(VmwareSource source, VmwareCbtMigrationVO migration, + VmwareCbtSnapshotInfo snapshot) { + if (source == null || snapshot == null || StringUtils.isBlank(snapshot.getSnapshotMor())) { + return; + } + + try { + getVmwareCbtMigrationService().removeSnapshot(source.vcenter, source.datacenterName, source.username, + source.password, source.sourceHost, migration.getSourceVmName(), snapshot.getSnapshotMor()); + clearDiskSnapshotMor(migration, snapshot.getSnapshotMor()); + } catch (RuntimeException e) { + LOGGER.warn(String.format("Unable to remove VMware CBT snapshot %s for migration %s: %s", + snapshot.getSnapshotMor(), migration.getUuid(), e.getMessage()), e); + } + } + + private void clearDiskSnapshotMor(VmwareCbtMigrationVO migration, String snapshotMor) { + List disks = vmwareCbtMigrationDiskDao.listByMigrationId(migration.getId()); + for (VmwareCbtMigrationDiskVO disk : disks) { + if (StringUtils.equals(disk.getSnapshotMor(), snapshotMor)) { + disk.setSnapshotMor(null); + disk.setUpdated(new Date()); + vmwareCbtMigrationDiskDao.update(disk.getId(), disk); + } + } + } + + private VmwareCbtMigrationService getVmwareCbtMigrationService() { + if (vmwareCbtMigrationService == null) { + throw new ServerApiException(ApiErrorCode.UNSUPPORTED_ACTION_ERROR, + "VMware CBT migration service is unavailable. Please enable the VMware hypervisor plugin."); + } + return vmwareCbtMigrationService; + } + + private HostVO getCbtHostForMigration(VmwareCbtMigrationVO migration) { + ClusterVO destinationCluster = clusterDao.findById(migration.getDestinationClusterId()); + if (destinationCluster == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to find destination cluster for VMware CBT migration"); + } + if (migration.getConvertHostId() != null) { + return selectCbtHost(migration.getConvertHostId(), destinationCluster); + } + return selectCbtHost(null, destinationCluster); + } + + private RemoteInstanceTO createRemoteInstance(VmwareSource source, VmwareCbtMigrationVO migration) { + return new RemoteInstanceTO(migration.getSourceVmName(), null, source.vcenter, source.username, source.password, + source.datacenterName, migration.getSourceCluster(), migration.getSourceHost()); + } + + private void applyVddkDetails(VmwareCbtMigrationVO migration, Map details) { + if (details == null) { + return; + } + migration.setVddkLibDir(StringUtils.trimToNull(details.get(Host.HOST_VDDK_LIB_DIR))); + migration.setVddkTransports(StringUtils.trimToNull(details.get(DETAIL_VDDK_TRANSPORTS))); + migration.setVddkThumbprint(StringUtils.trimToNull(details.get(DETAIL_VDDK_THUMBPRINT))); + } + + private void applyVddkDetails(VmwareCbtSyncCommand command, VmwareCbtMigrationVO migration) { + command.setVddkLibDir(migration.getVddkLibDir()); + command.setVddkTransports(migration.getVddkTransports()); + command.setVddkThumbprint(migration.getVddkThumbprint()); + } + + private void applyVddkDetails(VmwareCbtCutoverCommand command, VmwareCbtMigrationVO migration) { + command.setVddkLibDir(migration.getVddkLibDir()); + command.setVddkTransports(migration.getVddkTransports()); + command.setVddkThumbprint(migration.getVddkThumbprint()); + } + + private List getDiskTransferObjects(VmwareCbtMigrationVO migration) { + List disks = vmwareCbtMigrationDiskDao.listByMigrationId(migration.getId()); + List diskTOs = new ArrayList<>(); + for (VmwareCbtMigrationDiskVO disk : disks) { + diskTOs.add(new VmwareCbtDiskTO(disk.getSourceDiskId(), disk.getSourceDiskDeviceKey(), + disk.getSourceDiskPath(), disk.getDatastoreName(), disk.getTargetPath(), disk.getTargetFormat(), + disk.getChangeId(), disk.getSnapshotMor(), disk.getCapacityBytes() == null ? 0L : disk.getCapacityBytes())); + } + return diskTOs; + } + + private VmwareCbtMigrationAnswer sendVmwareCbtCommand(HostVO host, Command command, String action, String migrationUuid) { + try { + Answer answer = agentManager.send(host.getId(), command); + if (answer instanceof VmwareCbtMigrationAnswer) { + return (VmwareCbtMigrationAnswer) answer; + } + return new VmwareCbtMigrationAnswer(command, answer.getResult(), answer.getDetails(), migrationUuid); + } catch (AgentUnavailableException | OperationTimedoutException e) { + String message = String.format("Failed to %s VMware CBT migration %s on host %s due to: %s", + action, migrationUuid, host.getName(), e.getMessage()); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, message, e); + } + } + + private void markCycleFailed(VmwareCbtMigrationCycleVO cycle, String details) { + cycle.setState(VmwareCbtMigrationCycle.State.Failed); + cycle.setDescription(details); + cycle.setUpdated(new Date()); + vmwareCbtMigrationCycleDao.update(cycle.getId(), cycle); + } + + private void markMigrationFailed(VmwareCbtMigrationVO migration, String currentStep, String details) { + migration.setState(VmwareCbtMigration.State.Failed); + migration.setCurrentStep(currentStep); + migration.setLastError(details); + migration.setUpdated(new Date()); + vmwareCbtMigrationDao.update(migration.getId(), migration); + } + + private void sendCleanupCommandIfPossible(VmwareCbtMigrationVO migration) { + try { + HostVO cbtHost = getCbtHostForMigration(migration); + VmwareCbtCleanupCommand cleanupCommand = new VmwareCbtCleanupCommand(migration.getUuid(), getDiskTransferObjects(migration), + true, true, true); + cleanupCommand.setWait(300); + sendVmwareCbtCommand(cbtHost, cleanupCommand, "clean up", migration.getUuid()); + } catch (ServerApiException e) { + migration.setLastError(e.getDescription()); + } + } + + private VmwareCbtMigrationResponse createVmwareCbtMigrationResponse(VmwareCbtMigrationVO migration) { + VmwareCbtMigrationResponse response = new VmwareCbtMigrationResponse(); + response.setId(migration.getUuid()); + + DataCenterVO zone = dataCenterDao.findById(migration.getZoneId()); + if (zone != null) { + response.setZoneId(zone.getUuid()); + response.setZoneName(zone.getName()); + } + + Account account = accountService.getAccount(migration.getAccountId()); + if (account != null) { + response.setAccountId(account.getUuid()); + response.setAccountName(account.getAccountName()); + } + + ClusterVO cluster = clusterDao.findById(migration.getDestinationClusterId()); + if (cluster != null) { + response.setClusterId(cluster.getUuid()); + response.setClusterName(cluster.getName()); + } + + if (migration.getConvertHostId() != null) { + HostVO host = hostDao.findById(migration.getConvertHostId()); + if (host != null) { + response.setConvertInstanceHostId(host.getUuid()); + response.setConvertInstanceHostName(host.getName()); + } + } + + if (migration.getStoragePoolId() != null) { + StoragePoolVO pool = primaryDataStoreDao.findById(migration.getStoragePoolId()); + if (pool != null) { + response.setStoragePoolId(pool.getUuid()); + response.setStoragePoolName(pool.getName()); + } + } + + if (migration.getVmId() != null) { + UserVmVO vm = userVmDao.findById(migration.getVmId()); + if (vm != null) { + response.setVirtualMachineId(vm.getUuid()); + } + } + + response.setDisplayName(migration.getDisplayName()); + response.setVcenter(migration.getVcenter()); + if (migration.getExistingVcenterId() != null) { + VmwareDatacenterVO existingDc = vmwareDatacenterDao.findById(migration.getExistingVcenterId()); + if (existingDc != null) { + response.setExistingVcenterId(existingDc.getUuid()); + } + } + response.setDatacenterName(migration.getDatacenter()); + response.setSourceHost(migration.getSourceHost()); + response.setSourceCluster(migration.getSourceCluster()); + response.setSourceVmName(migration.getSourceVmName()); + response.setState(migration.getState().name()); + response.setCurrentStep(migration.getCurrentStep()); + response.setLastError(migration.getLastError()); + response.setCompletedCycles(migration.getCompletedCycles()); + response.setQuietCycles(migration.getQuietCycles()); + response.setTotalChangedBytes(migration.getTotalChangedBytes()); + response.setLastChangedBytes(migration.getLastChangedBytes()); + response.setLastDirtyRate(migration.getLastDirtyRate()); + response.setDisks(createVmwareCbtMigrationDiskResponses(migration)); + response.setCycles(createVmwareCbtMigrationCycleResponses(migration)); + response.setCreated(migration.getCreated()); + response.setLastUpdated(migration.getUpdated()); + response.setObjectName(OBJECT_NAME); + return response; + } + + private List createVmwareCbtMigrationDiskResponses(VmwareCbtMigrationVO migration) { + List diskResponses = new ArrayList<>(); + List disks = vmwareCbtMigrationDiskDao.listByMigrationId(migration.getId()); + for (VmwareCbtMigrationDiskVO disk : disks) { + VmwareCbtMigrationDiskResponse diskResponse = new VmwareCbtMigrationDiskResponse(); + diskResponse.setId(disk.getUuid()); + diskResponse.setSourceDiskId(disk.getSourceDiskId()); + diskResponse.setSourceDiskDeviceKey(disk.getSourceDiskDeviceKey()); + diskResponse.setSourceDiskPath(disk.getSourceDiskPath()); + diskResponse.setDatastoreName(disk.getDatastoreName()); + diskResponse.setCapacityBytes(disk.getCapacityBytes()); + diskResponse.setTargetPath(disk.getTargetPath()); + diskResponse.setTargetFormat(disk.getTargetFormat()); + diskResponse.setChangeId(disk.getChangeId()); + diskResponse.setSnapshotMor(disk.getSnapshotMor()); + diskResponse.setState(disk.getState().name()); + diskResponse.setObjectName("vmwarecbtmigrationdisk"); + diskResponses.add(diskResponse); + } + return diskResponses; + } + + private List createVmwareCbtMigrationCycleResponses(VmwareCbtMigrationVO migration) { + List cycleResponses = new ArrayList<>(); + List cycles = vmwareCbtMigrationCycleDao.listByMigrationId(migration.getId()); + for (VmwareCbtMigrationCycleVO cycle : cycles) { + VmwareCbtMigrationCycleResponse cycleResponse = new VmwareCbtMigrationCycleResponse(); + cycleResponse.setId(cycle.getUuid()); + cycleResponse.setCycleNumber(cycle.getCycleNumber()); + cycleResponse.setSnapshotMor(cycle.getSnapshotMor()); + cycleResponse.setChangedBytes(cycle.getChangedBytes()); + cycleResponse.setDirtyRate(cycle.getDirtyRate()); + cycleResponse.setDuration(cycle.getDuration()); + cycleResponse.setState(cycle.getState().name()); + cycleResponse.setDescription(cycle.getDescription()); + cycleResponse.setCreated(cycle.getCreated()); + cycleResponse.setLastUpdated(cycle.getUpdated()); + cycleResponse.setObjectName("vmwarecbtmigrationcycle"); + cycleResponses.add(cycleResponse); + } + return cycleResponses; + } + + @Override + public String getConfigComponentName() { + return VmwareCbtMigrationManagerImpl.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] { + VmwareCbtMigrationMinCycles, + VmwareCbtMigrationMaxCycles, + VmwareCbtMigrationQuietCycles, + VmwareCbtMigrationQuietBytes, + VmwareCbtMigrationQuietDirtyRate + }; + } + + private static class VmwareSource { + private final Long existingVcenterId; + private final String vcenter; + private final String datacenterName; + private final String username; + private final String password; + private final String sourceHost; + + private VmwareSource(Long existingVcenterId, String vcenter, String datacenterName, String username, + String password, String sourceHost) { + this.existingVcenterId = existingVcenterId; + this.vcenter = vcenter; + this.datacenterName = datacenterName; + this.username = username; + this.password = password; + this.sourceHost = sourceHost; + } + } + + private static class VmwareCbtChangedBlockQueryResult { + private final List changedBlocks; + private final List changedDisks; + + private VmwareCbtChangedBlockQueryResult(List changedBlocks, + List changedDisks) { + this.changedBlocks = changedBlocks; + this.changedDisks = changedDisks; + } + } +} diff --git a/server/src/main/resources/META-INF/cloudstack/server-compute/spring-server-compute-context.xml b/server/src/main/resources/META-INF/cloudstack/server-compute/spring-server-compute-context.xml index 3afae7676b7b..adca6ac7b001 100644 --- a/server/src/main/resources/META-INF/cloudstack/server-compute/spring-server-compute-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/server-compute/spring-server-compute-context.xml @@ -39,4 +39,6 @@ + + diff --git a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java index bee6c4ad257f..8ef067c6da07 100644 --- a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java @@ -730,6 +730,27 @@ public void testGetTemplateForImportInstanceDefaultTemplate() { Assert.assertEquals(defaultTemplateName, templateForImportInstance.getName()); } + @Test + public void testGetVmwareMigrationModeFallsBackToUseVddk() { + ImportVmCmd cmd = Mockito.mock(ImportVmCmd.class); + Assert.assertEquals(ImportVmCmd.VmwareMigrationMode.OVF, unmanagedVMsManager.getVmwareMigrationMode(cmd, false)); + Assert.assertEquals(ImportVmCmd.VmwareMigrationMode.VDDK, unmanagedVMsManager.getVmwareMigrationMode(cmd, true)); + } + + @Test + public void testGetVmwareMigrationModeParsesCbt() { + ImportVmCmd cmd = Mockito.mock(ImportVmCmd.class); + when(cmd.getVmwareMigrationMode()).thenReturn("cbt"); + Assert.assertEquals(ImportVmCmd.VmwareMigrationMode.CBT, unmanagedVMsManager.getVmwareMigrationMode(cmd, false)); + } + + @Test(expected = ServerApiException.class) + public void testGetVmwareMigrationModeRejectsUnknownMode() { + ImportVmCmd cmd = Mockito.mock(ImportVmCmd.class); + when(cmd.getVmwareMigrationMode()).thenReturn("not-a-mode"); + unmanagedVMsManager.getVmwareMigrationMode(cmd, false); + } + private enum VcenterParameter { EXISTING, EXTERNAL, diff --git a/server/src/test/java/org/apache/cloudstack/vm/VmwareCbtMigrationCutoverPolicyTest.java b/server/src/test/java/org/apache/cloudstack/vm/VmwareCbtMigrationCutoverPolicyTest.java new file mode 100644 index 000000000000..9baca363f3c8 --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/vm/VmwareCbtMigrationCutoverPolicyTest.java @@ -0,0 +1,70 @@ +// 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.junit.Assert; +import org.junit.Test; + +public class VmwareCbtMigrationCutoverPolicyTest { + + @Test + public void testContinuesUntilMinimumCyclesAreReached() { + VmwareCbtMigrationCutoverPolicy policy = new VmwareCbtMigrationCutoverPolicy(2, 5, 1, 1024, 0); + + Assert.assertEquals(VmwareCbtMigrationCutoverPolicy.Decision.CONTINUE, + policy.decide(1, 0, 512, 10)); + } + + @Test + public void testReadyForCutoverAfterRequiredQuietCycles() { + VmwareCbtMigrationCutoverPolicy policy = new VmwareCbtMigrationCutoverPolicy(1, 5, 2, 1024, 0); + + Assert.assertEquals(VmwareCbtMigrationCutoverPolicy.Decision.READY_FOR_CUTOVER, + policy.decide(3, 1, 512, 10)); + } + + @Test + public void testReadyForCutoverAfterMinimumCyclesWhenNoBlocksChanged() { + VmwareCbtMigrationCutoverPolicy policy = new VmwareCbtMigrationCutoverPolicy(2, 5, 2, 1024, 0); + + Assert.assertEquals(VmwareCbtMigrationCutoverPolicy.Decision.CONTINUE, + policy.decide(1, 0, 0, 10)); + Assert.assertEquals(VmwareCbtMigrationCutoverPolicy.Decision.READY_FOR_CUTOVER, + policy.decide(2, 0, 0, 10)); + } + + @Test + public void testDirtyRateCanKeepReplicationRunning() { + VmwareCbtMigrationCutoverPolicy policy = new VmwareCbtMigrationCutoverPolicy(1, 5, 1, 0, 1024); + + Assert.assertFalse(policy.isQuietCycle(2048, 1)); + Assert.assertTrue(policy.isQuietCycle(2048, 2)); + } + + @Test + public void testReadyForCutoverWhenMaxCyclesReached() { + VmwareCbtMigrationCutoverPolicy policy = new VmwareCbtMigrationCutoverPolicy(1, 5, 2, 1024, 1024); + + Assert.assertEquals(VmwareCbtMigrationCutoverPolicy.Decision.READY_FOR_CUTOVER_MAX_CYCLES, + policy.decide(5, 0, 4096, 1)); + } + + @Test(expected = IllegalArgumentException.class) + public void testRejectsMaxCyclesBelowMinCycles() { + new VmwareCbtMigrationCutoverPolicy(3, 2, 1, 1024, 1024); + } +} diff --git a/tools/cloudstack-build.ps1 b/tools/cloudstack-build.ps1 new file mode 100644 index 000000000000..1c11c4690459 --- /dev/null +++ b/tools/cloudstack-build.ps1 @@ -0,0 +1,199 @@ +# +# 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. +# + +<# +.SYNOPSIS +Runs a Windows-safe Apache CloudStack Maven build. + +.DESCRIPTION +Uses the local Codex Java/Maven toolchain when present, verifies that Java 17+ +is available, keeps Bash scripts on LF line endings, and promotes a server +compile request to package so cloud-api test classes are produced for the +cloud-server dependency graph. + +.EXAMPLE +powershell -NoProfile -ExecutionPolicy Bypass -File tools\cloudstack-build.ps1 -Modules server -Phase compile + +.EXAMPLE +powershell -NoProfile -ExecutionPolicy Bypass -File tools\cloudstack-build.ps1 -Modules server -Test VmwareCbtMigrationCutoverPolicyTest +#> + +[CmdletBinding()] +param( + [string[]] $Modules = @("server"), + + [ValidateSet("validate", "compile", "test-compile", "test", "package", "verify", "install")] + [string] $Phase = "package", + + [string] $Test, + + [switch] $RunTests, + + [switch] $NoAutoPackage, + + [string] $JavaHome = "$env:USERPROFILE\.codex\toolchains\jdk-17", + + [string] $MavenHome = "$env:USERPROFILE\.codex\toolchains\apache-maven-3.9.9" +) + +$ErrorActionPreference = "Stop" + +function Get-ExistingFile([string[]] $Candidates) { + foreach ($candidate in $Candidates) { + if (Test-Path -LiteralPath $candidate -PathType Leaf) { + return $candidate + } + } + return $null +} + +function Join-ModuleList([string[]] $ModuleValues) { + $values = @() + foreach ($moduleValue in $ModuleValues) { + if ([string]::IsNullOrWhiteSpace($moduleValue)) { + continue + } + $values += ($moduleValue -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + } + return ($values -join ",") +} + +function Convert-FileToLf([string] $Path) { + if (-not (Test-Path -LiteralPath $Path)) { + return + } + + $utf8NoBom = New-Object System.Text.UTF8Encoding $false + $content = [System.IO.File]::ReadAllText($Path, $utf8NoBom) + $normalized = $content.Replace("`r`n", "`n") + if ($normalized -ne $content) { + [System.IO.File]::WriteAllText($Path, $normalized, $utf8NoBom) + Write-Host "Normalized LF line endings for $Path" + } +} + +function Get-JavaMajorVersion([string] $JavaExecutable) { + $processInfo = New-Object System.Diagnostics.ProcessStartInfo + $processInfo.FileName = $JavaExecutable + $processInfo.Arguments = "-version" + $processInfo.RedirectStandardError = $true + $processInfo.RedirectStandardOutput = $true + $processInfo.UseShellExecute = $false + + $process = [System.Diagnostics.Process]::Start($processInfo) + $standardError = $process.StandardError.ReadToEnd() + $standardOutput = $process.StandardOutput.ReadToEnd() + $process.WaitForExit() + + $combinedOutput = "$standardError`n$standardOutput" + $versionLine = $combinedOutput -split "`r?`n" | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | + Select-Object -First 1 + + if ($versionLine -notmatch 'version "([^"]+)"') { + throw "Could not determine Java version from: $versionLine" + } + + $versionParts = $Matches[1].Split(".") + if ($versionParts[0] -eq "1" -and $versionParts.Length -gt 1) { + return [int] $versionParts[1] + } + return [int] $versionParts[0] +} + +function Add-PathPrefix([string] $PathValue) { + if (-not [string]::IsNullOrWhiteSpace($PathValue)) { + $env:Path = "$PathValue;$env:Path" + } +} + +$moduleList = Join-ModuleList $Modules +if ([string]::IsNullOrWhiteSpace($moduleList)) { + throw "At least one Maven module must be provided." +} + +$requestedPhase = $Phase +$serverModules = @("server", ":cloud-server", "cloud-server") +if (-not $NoAutoPackage -and $Phase -eq "compile") { + $selectedModules = $moduleList -split "," + foreach ($serverModule in $serverModules) { + if ($selectedModules -contains $serverModule) { + $Phase = "package" + Write-Host "Using Maven phase 'package' instead of 'compile' because cloud-server depends on cloud-api:tests." + break + } + } +} + +if ($env:OS -eq "Windows_NT") { + Convert-FileToLf (Join-Path (Get-Location) "engine\schema\templateConfig.sh") +} + +$javaExe = Get-ExistingFile @((Join-Path $JavaHome "bin\java.exe")) +$mvnCmd = Get-ExistingFile @((Join-Path $MavenHome "bin\mvn.cmd")) + +if ($javaExe -ne $null) { + $env:JAVA_HOME = $JavaHome + Add-PathPrefix (Join-Path $JavaHome "bin") +} else { + $javaCommand = Get-Command java -ErrorAction SilentlyContinue + if ($javaCommand -eq $null) { + throw "Java was not found. Install Java 17+ or pass -JavaHome." + } + $javaExe = $javaCommand.Source +} + +$javaMajorVersion = Get-JavaMajorVersion $javaExe +if ($javaMajorVersion -lt 17) { + throw "Java 17+ is required for this CloudStack build. Found Java major version $javaMajorVersion at $javaExe." +} + +if ($mvnCmd -ne $null) { + $env:MAVEN_HOME = $MavenHome + Add-PathPrefix (Join-Path $MavenHome "bin") +} else { + $mvnCommand = Get-Command mvn -ErrorAction SilentlyContinue + if ($mvnCommand -eq $null) { + throw "Maven was not found. Install Maven or pass -MavenHome." + } + $mvnCmd = $mvnCommand.Source +} + +$mavenArgs = @("-pl", $moduleList, "-am") + +if ([string]::IsNullOrWhiteSpace($Test)) { + if (-not $RunTests) { + $mavenArgs += @("-DskipTests", "-DskipITs") + } +} else { + $mavenArgs += @("-Dtest=$Test", "-DfailIfNoTests=false", "-Dsurefire.failIfNoSpecifiedTests=false", "-DskipITs") +} + +$mavenArgs += @("-Dcheckstyle.skip=true", "-Dspotbugs.skip=true", $Phase) + +Write-Host "JAVA_HOME=$env:JAVA_HOME" +Write-Host "MAVEN_HOME=$env:MAVEN_HOME" +Write-Host "Java executable: $javaExe" +Write-Host "Java major version: $javaMajorVersion" +Write-Host "Requested phase: $requestedPhase" +Write-Host "Effective phase: $Phase" +Write-Host "Running: mvn $($mavenArgs -join ' ')" + +& $mvnCmd @mavenArgs +exit $LASTEXITCODE diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 1187b3e62b40..b865802e83e4 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -734,6 +734,7 @@ "label.current": "Current", "label.currentstep": "Current step", "label.currentstep.duration": "Current step duration", +"label.completedcycles": "Completed cycles", "label.current.storage": "Current storage", "label.currentpassword": "Current password", "label.custom": "Custom", @@ -2017,6 +2018,7 @@ "label.quickview": "Quick view", "label.quiescevm": "Quiesce Instance", "label.quiettime": "Quiet time (in sec)", +"label.quietcycles": "Quiet cycles", "label.quota": "Quota", "label.quota.add.credits": "Add credits", "label.quota.configuration": "Quota configuration", @@ -2363,6 +2365,7 @@ "label.sourcecidr": "Source CIDR", "label.sourcecidrlist": "Source CIDR list", "label.sourcehost": "Source host", +"label.sourcecluster": "Source cluster", "label.sourceipaddress": "Source IP address", "label.sourceipaddressnetworkid": "Network ID of source IP address", "label.sourcenat": "Source NAT", @@ -2412,6 +2415,27 @@ "label.user.data": "User Data", "label.user.data.library": "User Data Library", "label.use.vddk": "Use VDDK", +"label.vmware.migration.mode": "VMware migration mode", +"label.vmware.migration.mode.ovf": "OVF", +"label.vmware.migration.mode.vddk": "VDDK", +"label.vmware.migration.mode.cbt": "CBT", +"label.vmware.cbt.migrations": "VMware CBT Migrations", +"label.sync.delta": "Sync delta", +"label.cutover": "Cutover", +"label.cyclenumber": "Cycle", +"label.changedbytes": "Changed bytes", +"label.dirtyrate": "Dirty rate", +"label.lastchangedbytes": "Last changed bytes", +"label.lastdirtyrate": "Last dirty rate", +"label.totalchangedbytes": "Total changed bytes", +"label.lasterror": "Last error", +"label.register.targets": "Register targets", +"label.source.disk": "Source disk", +"label.source.disk.path": "Source disk path", +"label.target.path": "Target path", +"label.target.format": "Target format", +"label.change.id": "Change ID", +"label.snapshot.mor": "Snapshot MOR", "label.ssh.port": "SSH port", "label.sshkeypair": "New SSH key pair", "label.sshkeypairs": "SSH key pairs", @@ -3078,6 +3102,7 @@ "message.action.unmanage.volumes": "Please confirm that you want to unmanage the Volumes.", "message.action.vmsnapshot.delete": "Please confirm that you want to delete this Instance Snapshot.
Please notice that the Instance will be paused before the Snapshot deletion, and resumed after deletion, if it runs on KVM.", "message.activate.project": "Are you sure you want to activate this project?", +"message.api.not.available": "API is not available.", "message.add.custom.action.parameters": "Parameters to be made available while running the custom action.", "message.add.egress.rule.failed": "Adding new egress rule failed.", "message.add.egress.rule.processing": "Adding new egress rule...", @@ -3221,6 +3246,9 @@ "message.confirm.archive.selected.alerts": "Please confirm you would like to archive the selected alerts", "message.confirm.archive.selected.events": "Please confirm you would like to archive the selected events", "message.confirm.attach.disk": "Are you sure you want to attach disk?", +"message.confirm.cancel.vmware.cbt.migration": "Please confirm that you want to cancel this VMware CBT migration.", +"message.confirm.sync.vmware.cbt.migration": "Please confirm that you want to run the next VMware CBT delta synchronization cycle.", +"message.confirm.cutover.vmware.cbt.migration": "Please confirm that you want to run final cutover for this VMware CBT migration.", "message.confirm.change.disk.offering.for.sharedfs": "Please confirm that you want to change the disk offering for the Shared FileSystem. This might migrate the underlying volume to a different storage pool if required.", "message.confirm.change.offering.for.volume": "Please confirm that you want to change disk offering for the volume", "message.confirm.change.service.offering.for.sharedfs": "Please confirm that you want to change the service offering for the Shared FileSystem.", @@ -3589,6 +3617,7 @@ "message.error.vcenter.password": "Please enter vCenter password.", "message.error.vcenter.username": "Please enter vCenter username.", "message.error.version.for.cluster": "Please select Kubernetes version for Kubernetes Cluster.", +"message.error.vmware.cbt.target.path": "Enter a target path for every VMware CBT disk.", "message.error.vlan.range": "Please enter a valid VLAN/VNI range.", "message.error.volume.name": "Please enter volume name.", "message.error.volume": "Please enter volume.", @@ -3622,6 +3651,7 @@ "message.host.external.datadisk": "Usage of data disks for the selected template is not applicable", "message.import.running.instance.warning": "The selected VM is powered-on on the VMware Datacenter. The recommended state to convert a VMware VM into KVM is powered-off after a graceful shutdown of the guest OS.", "message.import.vm.tasks": "Import from VMware to KVM tasks", +"message.vmware.cbt.migrations": "CBT incremental migration sessions from VMware to KVM", "message.import.volume": "Please specify the domain, account or project name.
If not set, the volume will be imported for the caller.", "message.info.cloudian.console": "Cloudian Management Console should open in another window.", "message.installwizard.cloudstack.helptext.website": " * Project website:\t ", diff --git a/ui/src/views/tools/ImportUnmanagedInstance.vue b/ui/src/views/tools/ImportUnmanagedInstance.vue index ffa0d9344335..3580efd23301 100644 --- a/ui/src/views/tools/ImportUnmanagedInstance.vue +++ b/ui/src/views/tools/ImportUnmanagedInstance.vue @@ -152,11 +152,18 @@ - + - + + {{ $t('label.vmware.migration.mode.ovf') }} + {{ $t('label.vmware.migration.mode.vddk') }} + {{ $t('label.vmware.migration.mode.cbt') }} +