diff --git a/api/src/main/java/com/cloud/vm/VirtualMachine.java b/api/src/main/java/com/cloud/vm/VirtualMachine.java index e2ea408e7b8c..80cb885f0e33 100644 --- a/api/src/main/java/com/cloud/vm/VirtualMachine.java +++ b/api/src/main/java/com/cloud/vm/VirtualMachine.java @@ -345,6 +345,8 @@ public boolean isUsedBySystem() { Type getType(); + String getBackupName(); + HypervisorType getHypervisorType(); Map getDetails(); @@ -355,5 +357,4 @@ public boolean isUsedBySystem() { boolean isDisplay(); boolean isDynamicallyScalable(); - } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java index 2fddcace84dd..370272f2ae38 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java @@ -69,14 +69,26 @@ public Long getId() { return id; } + public void setId(Long id) { + this.id = id; + } + public Long getDeviceId() { return deviceId; } + public void setDeviceId(Long deviceId) { + this.deviceId = deviceId; + } + public Long getVirtualMachineId() { return virtualMachineId; } + public void setVirtualMachineId(Long virtualMachineId) { + this.virtualMachineId = virtualMachineId; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/backup/Backup.java b/api/src/main/java/org/apache/cloudstack/backup/Backup.java index dffe8a032134..6f001b1fab75 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/Backup.java +++ b/api/src/main/java/org/apache/cloudstack/backup/Backup.java @@ -23,7 +23,7 @@ import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.api.Identity; import org.apache.cloudstack.api.InternalIdentity; -import org.apache.commons.lang3.StringUtils; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import com.cloud.storage.Volume; @@ -85,6 +85,14 @@ class RestorePoint { private String id; private Date created; private String type; + private List paths; + + public RestorePoint(String id, Date created, String type, List paths) { + this.id = id; + this.created = created; + this.type = type; + this.paths = paths; + } public RestorePoint(String id, Date created, String type) { this.id = id; @@ -115,6 +123,14 @@ public String getType() { public void setType(String type) { this.type = type; } + + public void setPaths(List paths) { + this.paths = paths; + } + + public List getPaths() { + return paths; + } } class VolumeInfo { @@ -122,12 +138,14 @@ class VolumeInfo { private Volume.Type type; private Long size; private String path; + private Long deviceId; - public VolumeInfo(String uuid, String path, Volume.Type type, Long size) { + public VolumeInfo(String uuid, String path, Volume.Type type, Long size, Long deviceId) { this.uuid = uuid; this.type = type; this.size = size; this.path = path; + this.deviceId = deviceId; } public String getUuid() { @@ -150,14 +168,21 @@ public Long getSize() { return size; } + public void setDeviceId(Long deviceId) { + this.deviceId = deviceId; + } + + public Long getDeviceId() { + return deviceId; + } + @Override public String toString() { - return StringUtils.join(":", uuid, path, type, size); + return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "uuid", "path", "type", "size", "deviceId"); } } long getVmId(); - long getBackupOfferingId(); String getExternalId(); String getType(); Date getDate(); @@ -166,4 +191,5 @@ public String toString() { Long getProtectedSize(); List getBackedUpVolumes(); long getZoneId(); + long getBackupOfferingId(); } diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java b/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java index 39582b0e423c..69552a4e8d26 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java @@ -60,7 +60,7 @@ public interface BackupProvider { * @param vm the machine to stop backing up * @return succeeded? */ - boolean removeVMFromBackupOffering(VirtualMachine vm); + boolean removeVMFromBackupOffering(VirtualMachine vm, boolean removeBackups); /** * Whether the provider will delete backups on removal of VM from the offering diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java index e6ffca06f9e0..bac5d3ff1bff 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java @@ -102,6 +102,7 @@ public interface VolumeDao extends GenericDao, StateDao findIncludingRemovedByZone(long zoneId); + VolumeVO findByPath(String path); /** * Lists all volumes using a given passphrase ID * @param passphraseId diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java index 750dbf2bee0f..426d30706419 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java @@ -159,6 +159,13 @@ public VolumeVO findByPoolIdName(long poolId, String name) { return findOneBy(sc); } + @Override + public VolumeVO findByPath(String path) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("path", path); + return findOneBy(sc); + } + @Override public List findByPoolId(long poolId, Volume.Type volumeType) { SearchCriteria sc = AllFieldsSearch.create(); @@ -396,6 +403,7 @@ public VolumeDaoImpl() { AllFieldsSearch.and("updatedCount", AllFieldsSearch.entity().getUpdatedCount(), Op.EQ); AllFieldsSearch.and("name", AllFieldsSearch.entity().getName(), Op.EQ); AllFieldsSearch.and("passphraseId", AllFieldsSearch.entity().getPassphraseId(), Op.EQ); + AllFieldsSearch.and("path", AllFieldsSearch.entity().getPath(), Op.EQ); AllFieldsSearch.and("iScsiName", AllFieldsSearch.entity().get_iScsiName(), Op.EQ); AllFieldsSearch.done(); diff --git a/engine/schema/src/main/java/com/cloud/vm/VMInstanceVO.java b/engine/schema/src/main/java/com/cloud/vm/VMInstanceVO.java index a1d9f4a8089a..b559107436de 100644 --- a/engine/schema/src/main/java/com/cloud/vm/VMInstanceVO.java +++ b/engine/schema/src/main/java/com/cloud/vm/VMInstanceVO.java @@ -202,6 +202,9 @@ public class VMInstanceVO implements VirtualMachine, FiniteStateObject getBackupVolumeList() { public void setBackupVolumes(String backupVolumes) { this.backupVolumes = backupVolumes; } + + public String getBackupName() { + return backupName; + } + + public void setBackupName(String backupName) { + this.backupName = backupName; + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDaoImpl.java index b4e1a7602825..93269c76f43c 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDaoImpl.java @@ -44,7 +44,6 @@ import com.google.gson.Gson; public class BackupDaoImpl extends GenericDaoBase implements BackupDao { - @Inject AccountDao accountDao; @@ -194,43 +193,48 @@ public List listBackupsByVMandIntervalType(Long vmId, Backup.Type back @Override public BackupResponse newBackupResponse(Backup backup) { - VMInstanceVO vm = vmInstanceDao.findByIdIncludingRemoved(backup.getVmId()); - AccountVO account = accountDao.findByIdIncludingRemoved(vm.getAccountId()); - DomainVO domain = domainDao.findByIdIncludingRemoved(vm.getDomainId()); - DataCenterVO zone = dataCenterDao.findByIdIncludingRemoved(vm.getDataCenterId()); - Long offeringId = backup.getBackupOfferingId(); - if (offeringId == null) { - offeringId = vm.getBackupOfferingId(); - } - BackupOffering offering = backupOfferingDao.findByIdIncludingRemoved(offeringId); - - BackupResponse response = new BackupResponse(); - response.setId(backup.getUuid()); - response.setVmId(vm.getUuid()); - response.setVmName(vm.getHostName()); - response.setExternalId(backup.getExternalId()); - response.setType(backup.getType()); - response.setDate(backup.getDate()); - response.setSize(backup.getSize()); - response.setProtectedSize(backup.getProtectedSize()); - response.setStatus(backup.getStatus()); - // ACS 4.20: For backups taken prior this release the backup.backed_volumes column would be empty hence use vm_instance.backup_volumes - String backedUpVolumes; - if (Objects.isNull(backup.getBackedUpVolumes())) { - backedUpVolumes = new Gson().toJson(vm.getBackupVolumeList().toArray(), Backup.VolumeInfo[].class); - } else { - backedUpVolumes = new Gson().toJson(backup.getBackedUpVolumes().toArray(), Backup.VolumeInfo[].class); + try { + VMInstanceVO vm = vmInstanceDao.findByIdIncludingRemoved(backup.getVmId()); + AccountVO account = accountDao.findByIdIncludingRemoved(vm.getAccountId()); + DomainVO domain = domainDao.findByIdIncludingRemoved(vm.getDomainId()); + DataCenterVO zone = dataCenterDao.findByIdIncludingRemoved(vm.getDataCenterId()); + Long offeringId = backup.getBackupOfferingId(); + if (offeringId == null) { + offeringId = vm.getBackupOfferingId(); + } + BackupOffering offering = backupOfferingDao.findByIdIncludingRemoved(offeringId); + + BackupResponse response = new BackupResponse(); + response.setId(backup.getUuid()); + response.setVmId(vm.getUuid()); + response.setVmName(vm.getHostName()); + response.setExternalId(backup.getExternalId()); + response.setType(backup.getType()); + response.setDate(backup.getDate()); + response.setSize(backup.getSize()); + response.setProtectedSize(backup.getProtectedSize()); + response.setStatus(backup.getStatus()); + // ACS 4.20: For backups taken prior this release the backup.backed_volumes column would be empty hence use vm_instance.backup_volumes + String backedUpVolumes; + if (Objects.isNull(backup.getBackedUpVolumes())) { + backedUpVolumes = new Gson().toJson(vm.getBackupVolumeList().toArray(), Backup.VolumeInfo[].class); + } else { + backedUpVolumes = new Gson().toJson(backup.getBackedUpVolumes().toArray(), Backup.VolumeInfo[].class); + } + response.setVolumes(backedUpVolumes); + response.setBackupOfferingId(offering.getUuid()); + response.setBackupOffering(offering.getName()); + response.setAccountId(account.getUuid()); + response.setAccount(account.getAccountName()); + response.setDomainId(domain.getUuid()); + response.setDomain(domain.getName()); + response.setZoneId(zone.getUuid()); + response.setZone(zone.getName()); + response.setObjectName("backup"); + return response; + } catch (Exception e) { + logger.error("Failed to create backup response from Backup [id: {}, vmId: {}] due to: [{}].", backup.getId(), backup.getVmId(), e.getMessage(), e); + return null; } - response.setVolumes(backedUpVolumes); - response.setBackupOfferingId(offering.getUuid()); - response.setBackupOffering(offering.getName()); - response.setAccountId(account.getUuid()); - response.setAccount(account.getAccountName()); - response.setDomainId(domain.getUuid()); - response.setDomain(domain.getName()); - response.setZoneId(zone.getUuid()); - response.setZone(zone.getName()); - response.setObjectName("backup"); - return response; } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/engine/cloud/entity/api/db/VMEntityVO.java b/engine/schema/src/main/java/org/apache/cloudstack/engine/cloud/entity/api/db/VMEntityVO.java index 917f8bb800a2..8043c90528b6 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/engine/cloud/entity/api/db/VMEntityVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/engine/cloud/entity/api/db/VMEntityVO.java @@ -189,6 +189,9 @@ public class VMEntityVO implements VirtualMachine, FiniteStateObject getBackupVolumeList() { return Arrays.asList(new Gson().fromJson(this.backupVolumes, Backup.VolumeInfo[].class)); } + + @Override + public String getBackupName() { + return backupName; + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/PrimaryDataStoreDao.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/PrimaryDataStoreDao.java index f0c235e842c1..bc890af5bdf5 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/PrimaryDataStoreDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/PrimaryDataStoreDao.java @@ -149,6 +149,18 @@ public interface PrimaryDataStoreDao extends GenericDao { List listStoragePoolsWithActiveVolumesByOfferingId(long offeringid); + /** + * Find storage pool by path like. If clusterId is informed, try to find by path like in specified zone and cluster. + * If clusterId is not informed, try to find by path like in specified zone and with clusterId null. + */ + StoragePoolVO findPoolByPathLike(String datastore, Long datacenterId, Long clusterId); + + /** + * Find storage pool by name in specified datacenter. If clusterId is informed, try to find by name in specified zone and cluster. + * If clusterId is not informed, try to find by name in specified zone. + */ + StoragePoolVO findPoolByName(String datastore, Long datacenterId, Long clusterId); + Pair, Integer> searchForIdsAndCount(Long storagePoolId, String storagePoolName, Long zoneId, String path, Long podId, Long clusterId, String address, ScopeType scopeType, StoragePoolStatus status, String keyword, Filter searchFilter); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/PrimaryDataStoreDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/PrimaryDataStoreDaoImpl.java index cb7313954dc7..c51ec464c62f 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/PrimaryDataStoreDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/PrimaryDataStoreDaoImpl.java @@ -62,6 +62,10 @@ public class PrimaryDataStoreDaoImpl extends GenericDaoBase private final SearchBuilder DcLocalStorageSearch; private final GenericSearchBuilder StatusCountSearch; private final SearchBuilder ClustersSearch; + private final SearchBuilder PathLikeSearchClusterWide; + private final SearchBuilder PathLikeSearchZoneWide; + private final SearchBuilder NameSearchClusterWide; + private final SearchBuilder NameSearchZoneWide; private final SearchBuilder IdsSearch; @Inject @@ -155,6 +159,26 @@ public PrimaryDataStoreDaoImpl() { IdsSearch.and("ids", IdsSearch.entity().getId(), SearchCriteria.Op.IN); IdsSearch.done(); + PathLikeSearchClusterWide = createSearchBuilder(); + PathLikeSearchClusterWide.and("path", PathLikeSearchClusterWide.entity().getPath(), Op.LIKE); + PathLikeSearchClusterWide.and("datacenterId", PathLikeSearchClusterWide.entity().getDataCenterId(), Op.EQ); + PathLikeSearchClusterWide.and("clusterId", PathLikeSearchClusterWide.entity().getClusterId(), Op.EQ); + PathLikeSearchClusterWide.done(); + + PathLikeSearchZoneWide = createSearchBuilder(); + PathLikeSearchZoneWide.and("path", PathLikeSearchZoneWide.entity().getPath(), Op.LIKE); + PathLikeSearchZoneWide.and("datacenterId", PathLikeSearchZoneWide.entity().getDataCenterId(), Op.EQ); + PathLikeSearchZoneWide.and("clusterId", PathLikeSearchZoneWide.entity().getClusterId(), Op.NULL); + + NameSearchClusterWide = createSearchBuilder(); + NameSearchClusterWide.and("name", NameSearchClusterWide.entity().getName(), Op.EQ); + NameSearchClusterWide.and("datacenterId", NameSearchClusterWide.entity().getDataCenterId(), Op.EQ); + NameSearchClusterWide.and("clusterId", NameSearchClusterWide.entity().getClusterId(), Op.EQ); + + NameSearchZoneWide = createSearchBuilder(); + NameSearchZoneWide.and("name", NameSearchZoneWide.entity().getName(), Op.EQ); + NameSearchZoneWide.and("datacenterId", NameSearchZoneWide.entity().getDataCenterId(), Op.EQ); + NameSearchZoneWide.and("clusterId", NameSearchZoneWide.entity().getClusterId(), Op.NULL); } @Override @@ -713,6 +737,34 @@ public List listStoragePoolsWithActiveVolumesByOfferingId(long of } } + @Override + public StoragePoolVO findPoolByPathLike(String datastore, Long datacenterId, Long clusterId) { + SearchCriteria sc = null; + if (clusterId != null) { + sc = PathLikeSearchClusterWide.create(); + sc.setParameters("clusterId", clusterId); + } else { + sc = PathLikeSearchZoneWide.create(); + } + sc.setParameters("path", "%/" + datastore); + sc.setParameters("datacenterId", datacenterId); + return findOneBy(sc); + } + + @Override + public StoragePoolVO findPoolByName(String datastore, Long datacenterId, Long clusterId) { + SearchCriteria sc = null; + if (clusterId != null) { + sc = NameSearchClusterWide.create(); + sc.setParameters("clusterId", clusterId); + } else { + sc = NameSearchZoneWide.create(); + } + sc.setParameters("name", datastore); + sc.setParameters("datacenterId", datacenterId); + return findOneBy(sc); + } + @Override public Pair, Integer> searchForIdsAndCount(Long storagePoolId, String storagePoolName, Long zoneId, String path, Long podId, Long clusterId, String address, ScopeType scopeType, StoragePoolStatus status, diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql b/engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql index c36b71c2f250..f0985618ace6 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql @@ -381,6 +381,7 @@ CALL `cloud`.`IDEMPOTENT_MODIFY_COLUMN_CHAR_SET`('vpc_offerings', 'name', 'VARCH CALL `cloud`.`IDEMPOTENT_MODIFY_COLUMN_CHAR_SET`('vpc_offerings', 'unique_name', 'VARCHAR(64)', 'DEFAULT NULL COMMENT \'unique name of the vpc offering\''); CALL `cloud`.`IDEMPOTENT_MODIFY_COLUMN_CHAR_SET`('vpc_offerings', 'display_text', 'VARCHAR(255)', 'DEFAULT NULL COMMENT \'display text\''); +-- CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.roles','state', 'varchar(10) NOT NULL default "enabled" COMMENT "role state"'); -- Multi-Arch Zones diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql index b01243ad989d..61ae823708c5 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql @@ -37,3 +37,17 @@ WHERE rp.rule = 'quotaStatement' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'quotaCreditsList'); CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.host', 'last_mgmt_server_id', 'bigint unsigned DEFAULT NULL COMMENT "last management server this host is connected to" AFTER `mgmt_server_id`'); + +-- PR #6589 - [Veeam] disable jobs but keep backups +-- Populate column backup_volumes in table backups with a GSON +-- formed by concatenating the UUID, type, size, path and deviceId +-- of the volumes of VMs that have some backup offering. +-- Required for the restore process of a backup using Veeam +-- The Gson result can be in one of this formats: +-- When VM has only ROOT disk: [{"uuid":"","type":"","size":,"path":"","deviceId":}] +-- When VM has more tha one disk: [{"uuid":"","type":"","size":,"path":"","deviceId":}, {"uuid":"","type":"","size":,"path":"","deviceId":}, <>] +UPDATE `cloud`.`backups` b INNER JOIN `cloud`.`vm_instance` vm ON b.vm_id = vm.id SET b.backed_volumes = (SELECT CONCAT("[", GROUP_CONCAT( CONCAT("{\"uuid\":\"", v.uuid, "\",\"type\":\"", v.volume_type, "\",\"size\":", v.`size`, ",\"path\":\"", v.path, "\",\"deviceId\":", v.device_id, "}") SEPARATOR ","), "]") FROM `cloud`.`volumes` v WHERE v.instance_id = vm.id); + +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vm_instance', 'backup_name', 'varchar(255) NULL COMMENT "backup job name when using Veeam provider"'); + +UPDATE `cloud`.`vm_instance` vm INNER JOIN `cloud`.`backup_offering` bo ON vm.backup_offering_id = bo.id SET vm.backup_name = CONCAT(vm.instance_name, "-CSBKP-", vm.uuid); diff --git a/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java b/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java index 2d43f9e8d3c8..56b3c843a067 100644 --- a/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java +++ b/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java @@ -111,7 +111,7 @@ public Backup createNewBackupEntryForRestorePoint(Backup.RestorePoint restorePoi } @Override - public boolean removeVMFromBackupOffering(VirtualMachine vm) { + public boolean removeVMFromBackupOffering(VirtualMachine vm, boolean removeBackups) { logger.debug("Removing VM {} from backup offering by the Dummy Backup Provider", vm); return true; } diff --git a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java index 1e3b5aaa8e5e..5857ad3bae38 100644 --- a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java +++ b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java @@ -404,7 +404,7 @@ public boolean assignVMToBackupOffering(VirtualMachine vm, BackupOffering backup } @Override - public boolean removeVMFromBackupOffering(VirtualMachine vm) { + public boolean removeVMFromBackupOffering(VirtualMachine vm, boolean removeBackups) { return true; } diff --git a/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java b/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java index 504a551bb30b..04ce7c014ba9 100644 --- a/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java +++ b/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java @@ -307,7 +307,7 @@ public boolean isValidProviderOffering(Long zoneId, String uuid) { public boolean assignVMToBackupOffering(VirtualMachine vm, BackupOffering backupOffering) { return true; } @Override - public boolean removeVMFromBackupOffering(VirtualMachine vm) { + public boolean removeVMFromBackupOffering(VirtualMachine vm, boolean removeBackups) { LOG.debug("Removing VirtualMachine from Backup offering and Deleting any existing backups"); List backupsTaken = getClient(vm.getDataCenterId()).getBackupsForVm(vm); diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java index 93278e808513..96a7fcc4d437 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java @@ -27,6 +27,7 @@ import java.util.Objects; import java.util.stream.Collectors; +import com.cloud.storage.dao.VolumeDao; import javax.inject.Inject; import org.apache.cloudstack.backup.Backup.Metric; @@ -45,7 +46,6 @@ import com.cloud.hypervisor.vmware.VmwareDatacenterZoneMap; import com.cloud.dc.dao.VmwareDatacenterDao; import com.cloud.hypervisor.vmware.dao.VmwareDatacenterZoneMapDao; -import com.cloud.storage.dao.VolumeDao; import com.cloud.utils.Pair; import com.cloud.utils.component.AdapterBase; import com.cloud.utils.exception.CloudRuntimeException; @@ -164,7 +164,7 @@ private String getGuestBackupName(final String instanceName, final String uuid) public boolean assignVMToBackupOffering(final VirtualMachine vm, final BackupOffering backupOffering) { final VeeamClient client = getClient(vm.getDataCenterId()); final Job parentJob = client.listJob(backupOffering.getExternalId()); - final String clonedJobName = getGuestBackupName(vm.getInstanceName(), vm.getUuid()); + final String clonedJobName = getGuestBackupName(vm.getInstanceName(), backupOffering.getUuid()); if (!client.cloneVeeamJob(parentJob, clonedJobName)) { logger.error("Failed to clone pre-defined Veeam job (backup offering) for backup offering [id: {}, name: {}] but will check the list of jobs again if it was eventually succeeded.", backupOffering.getExternalId(), backupOffering.getName()); @@ -180,6 +180,7 @@ public boolean assignVMToBackupOffering(final VirtualMachine vm, final BackupOff final VmwareDatacenter vmwareDC = findVmwareDatacenterForVM(vm); if (client.addVMToVeeamJob(job.getExternalId(), vm.getInstanceName(), vmwareDC.getVcenterHost())) { ((VMInstanceVO) vm).setBackupExternalId(job.getExternalId()); + ((VMInstanceVO) vm).setBackupName(clonedJobName); return true; } } @@ -188,24 +189,26 @@ public boolean assignVMToBackupOffering(final VirtualMachine vm, final BackupOff } @Override - public boolean removeVMFromBackupOffering(final VirtualMachine vm) { + public boolean removeVMFromBackupOffering(final VirtualMachine vm, boolean removeBackups) { final VeeamClient client = getClient(vm.getDataCenterId()); final VmwareDatacenter vmwareDC = findVmwareDatacenterForVM(vm); + if (vm.getBackupExternalId() == null) { throw new CloudRuntimeException("The VM does not have a backup job assigned."); } - try { - if (!client.removeVMFromVeeamJob(vm.getBackupExternalId(), vm.getInstanceName(), vmwareDC.getVcenterHost())) { - logger.warn("Failed to remove VM from Veeam Job id: " + vm.getBackupExternalId()); - } - } catch (Exception e) { - logger.debug("VM was removed from the job so could not remove again, trying to delete the veeam job now.", e); + + final String clonedJobName = vm.getBackupName(); + boolean result = false; + + if (removeBackups) { + result = client.deleteJobAndBackup(clonedJobName); + } else { + result = client.disableJob(clonedJobName); } - final String clonedJobName = getGuestBackupName(vm.getInstanceName(), vm.getUuid()); - if (!client.deleteJobAndBackup(clonedJobName)) { - logger.warn("Failed to remove Veeam job and backup for job: " + clonedJobName); - throw new CloudRuntimeException("Failed to delete Veeam B&R job and backup, an operation may be in progress. Please try again after some time."); + if (!result) { + logger.warn("Failed to remove Veeam {} for job: [name: {}].", removeBackups ? "job and backup" : "job", clonedJobName); + throw new CloudRuntimeException("Failed to delete Veeam B&R job, an operation may be in progress. Please try again after some time."); } client.syncBackupRepository(); return true; @@ -213,7 +216,7 @@ public boolean removeVMFromBackupOffering(final VirtualMachine vm) { @Override public boolean willDeleteBackupsOnOfferingRemoval() { - return true; + return false; } @Override @@ -308,11 +311,11 @@ public Map getBackupMetrics(final Long zoneId, fi final Map backendMetrics = getClient(zoneId).getBackupMetrics(); for (final VirtualMachine vm : vms) { - if (vm == null || !backendMetrics.containsKey(vm.getUuid())) { + if (vm == null || !backendMetrics.containsKey(vm.getInstanceName())) { continue; } - Metric metric = backendMetrics.get(vm.getUuid()); + Metric metric = backendMetrics.get(vm.getInstanceName()); logger.debug("Metrics for VM [{}] is [backup size: {}, data size: {}].", vm, metric.getBackupSize(), metric.getDataSize()); metrics.put(vm, metric); @@ -320,6 +323,10 @@ public Map getBackupMetrics(final Long zoneId, fi return metrics; } + public List listRestorePoints(VirtualMachine vm) { + return getClient(vm.getDataCenterId()).listRestorePoints(vm.getBackupName(), vm.getInstanceName()); + } + @Override public Backup createNewBackupEntryForRestorePoint(Backup.RestorePoint restorePoint, VirtualMachine vm, Backup.Metric metric) { BackupVO backup = new BackupVO(); @@ -341,12 +348,6 @@ public Backup createNewBackupEntryForRestorePoint(Backup.RestorePoint restorePoi return backup; } - @Override - public List listRestorePoints(VirtualMachine vm) { - String backupName = getGuestBackupName(vm.getInstanceName(), vm.getUuid()); - return getClient(vm.getDataCenterId()).listRestorePoints(backupName, vm.getInstanceName()); - } - @Override public String getConfigComponentName() { return BackupService.class.getSimpleName(); diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java index 9accc0714de9..d667ebf51a59 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java @@ -437,7 +437,7 @@ protected String getRepositoryNameFromJob(String backupName) { throw new CloudRuntimeException(String.format("Failed to get Repository Name from Job [name: %s].", backupName)); } - for (String block : result.second().split("\r\n")) { + for (String block : result.second().split("\r\n\r\n")) { if (block.matches("Name(\\s)+:(.)*")) { return block.split(":")[1].trim(); } @@ -534,6 +534,10 @@ public boolean cloneVeeamJob(final Job parentJob, final String clonedJobName) { + "repository associated with backup job [id: %s, uid: %s, backupServerId: %s, name: %s].", parentJob.getId(), parentJob.getUid(), parentJob.getBackupServerId(), parentJob.getName())); } + if (checkIfBackupAlreadyExistsAndIsDisabled(clonedJobName)) { + logger.debug("Job with name [{}] already exists in Veeam, but it is disable. Enabling it now.", clonedJobName); + return enableJob(clonedJobName); + } final BackupJobCloneInfo cloneInfo = new BackupJobCloneInfo(); cloneInfo.setJobName(clonedJobName); cloneInfo.setFolderName(clonedJobName); @@ -547,11 +551,36 @@ public boolean cloneVeeamJob(final Job parentJob, final String clonedJobName) { return false; } + private boolean enableJob(String clonedJobName) { + String action = "if ($job) { Enable-VBRJob -Job $job } else { Write-Output \"Failed\" }"; + return checkJobAndDoAction(clonedJobName, action); + } + + private boolean checkIfBackupAlreadyExistsAndIsDisabled(String clonedJobName) { + String action = "if ($job) { $job.IsScheduleEnabled -eq $False -or $job.Options.JobOptions.RunManually -eq $True } else { Write-Output \"Failed\" }"; + return checkJobAndDoAction(clonedJobName, action); + } + + private boolean checkJobAndDoAction(String clonedJobName, String action) { + List cmds = Arrays.asList( + String.format("$job = Get-VBRJob -Name \"%s\"", clonedJobName), + action + ); + Pair result = executePowerShellCommands(cmds); + return result.first() && !result.second().contains("Failed"); + } + public boolean addVMToVeeamJob(final String jobId, final String vmwareInstanceName, final String vmwareDcName) { logger.debug("Trying to add VM to backup offering that is Veeam job: " + jobId); try { final String heirarchyId = findDCHierarchy(vmwareDcName); final String veeamVmRefId = lookupVM(heirarchyId, vmwareInstanceName); + if (checkIfVmAlreadyExistsInJob(jobId, vmwareInstanceName)) { + logger.debug("VM [name: {}] is already assigned to the Backup Job [{}].", vmwareInstanceName, jobId); + return true; + } + + logger.debug("Trying to add VM [name: {}] to job [{}].", vmwareInstanceName, jobId); final CreateObjectInJobSpec vmToBackupJob = new CreateObjectInJobSpec(); vmToBackupJob.setObjName(vmwareInstanceName); vmToBackupJob.setObjRef(veeamVmRefId); @@ -564,6 +593,20 @@ public boolean addVMToVeeamJob(final String jobId, final String vmwareInstanceNa throw new CloudRuntimeException("Failed to add VM to backup offering likely due to timeout, please check Veeam tasks"); } + private boolean checkIfVmAlreadyExistsInJob(String jobId, String vmwareInstanceName) { + jobId = jobId.replace("urn:veeam:Job:", ""); + logger.debug("Checking if VM [name: {}] is already assigned to the Backup Job [name: {}].", vmwareInstanceName, jobId); + List cmds = Arrays.asList( + String.format("$job = (Get-VBRJob ^| Where-Object { $_.Id -eq '%s' })", jobId), + "if ($job) { ", + String.format("$vm = Get-VBRJobObject -Job $job -Name '%s'", vmwareInstanceName), + "if ($vm) { Write-Output \"VM has already in Job\" } else { Write-Output \"False\" }", + "} else { Write-Output \"False\" }" + ); + Pair result = executePowerShellCommands(cmds); + return result.first() && !result.second().contains("False"); + } + public boolean removeVMFromVeeamJob(final String jobId, final String vmwareInstanceName, final String vmwareDcName) { logger.debug("Trying to remove VM from backup offering that is a Veeam job: " + jobId); try { @@ -656,6 +699,22 @@ public boolean setJobSchedule(final String jobName) { return result != null && result.first() && !result.second().isEmpty() && !result.second().contains(FAILED_TO_DELETE); } + public boolean disableJob(final String jobName) { + String separator = ";"; + String action = String.join(separator, + "if ($job) {", + "if ($job.Options.JobOptions.RunManually -eq $False) { ", + "Disable-VBRJob -Job $job", + "$repo = Get-VBRBackupRepository", + "Sync-VBRBackupRepository -Repository $repo", + "}", + "} else {", + "Write-Output \"Failed\"", + "Exit 1", + "}"); + return checkJobAndDoAction(jobName, action); + } + public boolean deleteJobAndBackup(final String jobName) { Pair result = executePowerShellCommands(Arrays.asList( String.format("$job = Get-VBRJob -Name '%s'", jobName), @@ -721,32 +780,15 @@ protected Map processHttpResponseForBackupMetrics(final I throw new CloudRuntimeException("Could not get backup metrics via Veeam B&R API"); } for (final BackupFile backupFile : backupFiles.getBackupFiles()) { - String vmUuid = null; - String backupName = null; - List links = backupFile.getLink(); - for (Link link : links) { - if (BACKUP_REFERENCE.equals(link.getType())) { - backupName = link.getName(); - break; - } - } - if (backupName != null && backupName.contains(BACKUP_IDENTIFIER)) { - final String[] names = backupName.split(BACKUP_IDENTIFIER); - if (names.length > 1) { - vmUuid = names[1]; - } - } - if (vmUuid == null) { + String vmInstanceName = getVmInstanceNameFromBackupFile(backupFile); + if (vmInstanceName == null) { continue; } - if (vmUuid.contains(" - ")) { - vmUuid = vmUuid.split(" - ")[0]; - } Long usedSize = 0L; Long dataSize = 0L; - if (metrics.containsKey(vmUuid)) { - usedSize = metrics.get(vmUuid).getBackupSize(); - dataSize = metrics.get(vmUuid).getDataSize(); + if (metrics.containsKey(vmInstanceName)) { + usedSize = metrics.get(vmInstanceName).getBackupSize(); + dataSize = metrics.get(vmInstanceName).getDataSize(); } if (backupFile.getBackupSize() != null) { usedSize += Long.valueOf(backupFile.getBackupSize()); @@ -754,7 +796,7 @@ protected Map processHttpResponseForBackupMetrics(final I if (backupFile.getDataSize() != null) { dataSize += Long.valueOf(backupFile.getDataSize()); } - metrics.put(vmUuid, new Backup.Metric(usedSize, dataSize)); + metrics.put(vmInstanceName, new Backup.Metric(usedSize, dataSize)); } } catch (final IOException e) { logger.error("Failed to process response to get backup metrics via Veeam B&R API due to:", e); @@ -763,6 +805,25 @@ protected Map processHttpResponseForBackupMetrics(final I return metrics; } + private String getVmInstanceNameFromBackupFile(BackupFile backupFile) { + String vmInstanceName = null; + String backupName = null; + List links = backupFile.getLink(); + for (Link link : links) { + if (BACKUP_REFERENCE.equals(link.getType())) { + backupName = link.getName(); + break; + } + } + if (backupName != null && backupName.contains(BACKUP_IDENTIFIER)) { + final String[] names = backupName.split(BACKUP_IDENTIFIER); + if (names.length > 0) { + vmInstanceName = names[0]; + } + } + return vmInstanceName; + } + public Map getBackupMetricsLegacy() { final String separator = "====="; final List cmds = Arrays.asList( @@ -807,7 +868,7 @@ protected Map processPowerShellResultForBackupMetrics(fin final String backupName = parts[0]; if (backupName != null && backupName.contains(BACKUP_IDENTIFIER)) { final String[] names = backupName.split(BACKUP_IDENTIFIER); - sizes.put(names[names.length - 1], new Backup.Metric(Long.valueOf(parts[1]), Long.valueOf(parts[2]))); + sizes.put(names[0], new Backup.Metric(Long.valueOf(parts[1]), Long.valueOf(parts[2]))); } } return sizes; @@ -815,6 +876,7 @@ protected Map processPowerShellResultForBackupMetrics(fin private Backup.RestorePoint getRestorePointFromBlock(String[] parts) { logger.debug(String.format("Processing block of restore points: [%s].", StringUtils.join(parts, ", "))); + List paths = new ArrayList<>(); String id = null; Date created = null; String type = null; @@ -832,16 +894,31 @@ private Backup.RestorePoint getRestorePointFromBlock(String[] parts) { } else if (part.matches("Type(\\s)+:(.)*")) { String [] split = part.split(":"); type = split[1].trim(); + } else if (part.matches("Path(\\s)+:(.)*")) { + String diskPath = part.split(":")[1].trim(); + String path = diskPath.split("/")[1].replace(".vmdk", ""); + paths.add(path); } } - return new Backup.RestorePoint(id, created, type); + return new Backup.RestorePoint(id, created, type, paths); } public List listRestorePointsLegacy(String backupName, String vmInternalName) { final List cmds = Arrays.asList( - String.format("$backup = Get-VBRBackup -Name '%s'", backupName), - String.format("if ($backup) { $restore = (Get-VBRRestorePoint -Backup:$backup -Name \"%s\" ^| Where-Object {$_.IsConsistent -eq $true})", vmInternalName), - "if ($restore) { $restore ^| Format-List } }" + String.format("$backup = Get-VBRBackup -Name \"%s\"", backupName), + "if ($backup) {", + String.format("$restorePoints = (Get-VBRRestorePoint -Backup:$backup -Name \"%s\" ^| Where-Object {$_.IsConsistent -eq $true})", vmInternalName), + "if ($restorePoints) {", + "ForEach ($restore in $restorePoints) {", + "$restoreId = 'Id : ' + $restore.Id.Guid", + "$creationTime = 'CreationTime : ' + $restore.CreationTime.ToString('MM/dd/yyyy HH:mm:ss')", + "$type = 'Type : ' + $restore.Type", + "$path = 'Path : '", + "$paths = $restore.AuxData.Disks.Path ^| ForEach-Object { $path + $_ }", + "Write-Output $restoreId $creationTime $type $paths `r`n", + "}", + "}", + "}" ); Pair response = executePowerShellCommands(cmds); if (response == null || !response.first()) { diff --git a/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java b/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java index 63d6896bb85c..7f688250a107 100644 --- a/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java +++ b/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java @@ -148,7 +148,7 @@ public void getRepositoryNameFromJobTestExceptionWhenResultIsInWrongFormat() { @Test public void getRepositoryNameFromJobTestSuccess() throws Exception { String backupName = "TEST-BACKUP3"; - Pair response = new Pair(Boolean.TRUE, "\r\nName : test"); + Pair response = new Pair(Boolean.TRUE, "\r\n\r\nName : test"); Mockito.doReturn(response).when(mockClient).executePowerShellCommands(Mockito.anyList()); String repositoryNameFromJob = mockClient.getRepositoryNameFromJob(backupName); Assert.assertEquals("test", repositoryNameFromJob); @@ -239,7 +239,17 @@ public void testProcessPowerShellResultForBackupMetrics() { Map metrics = client.processPowerShellResultForBackupMetrics(result); - verifyBackupMetrics(metrics); + Assert.assertEquals(2, metrics.size()); + + String vmName1 = "i-2-3-VM"; + Assert.assertTrue(metrics.containsKey(vmName1)); + Assert.assertEquals(537776128L, (long) metrics.get(vmName1).getBackupSize()); + Assert.assertEquals(2147506644L, (long) metrics.get(vmName1).getDataSize()); + + String vmName2 = "i-2-5-VM"; + Assert.assertTrue(metrics.containsKey(vmName2)); + Assert.assertEquals(1268682752L, (long) metrics.get(vmName2).getBackupSize()); + Assert.assertEquals(15624049921L, (long) metrics.get(vmName2).getDataSize()); } @Test @@ -427,7 +437,17 @@ public void testProcessHttpResponseForBackupMetricsForV11() { InputStream inputStream = new ByteArrayInputStream(xmlResponse.getBytes()); Map metrics = client.processHttpResponseForBackupMetrics(inputStream); - verifyBackupMetrics(metrics); + Assert.assertEquals(2, metrics.size()); + + String vmName1 = "i-2-3-VM"; + Assert.assertTrue(metrics.containsKey(vmName1)); + Assert.assertEquals(537776128L, (long) metrics.get(vmName1).getBackupSize()); + Assert.assertEquals(2147506644L, (long) metrics.get(vmName1).getDataSize()); + + String vmName2 = "i-2-5-VM"; + Assert.assertTrue(metrics.containsKey(vmName2)); + Assert.assertEquals(1268682752L, (long) metrics.get(vmName2).getBackupSize()); + Assert.assertEquals(15624049921L, (long) metrics.get(vmName2).getDataSize()); } @Test @@ -460,12 +480,14 @@ public void testGetBackupMetricsViaVeeamAPI() { .withHeader("content-type", "application/xml") .withStatus(200) .withBody(xmlResponse))); + + String vmName = "i-2-4-VM"; Map metrics = client.getBackupMetricsViaVeeamAPI(); Assert.assertEquals(1, metrics.size()); - Assert.assertTrue(metrics.containsKey("506760dc-ed77-40d6-a91d-e0914e7a1ad8")); - Assert.assertEquals(535875584L, (long) metrics.get("506760dc-ed77-40d6-a91d-e0914e7a1ad8").getBackupSize()); - Assert.assertEquals(2147507235L, (long) metrics.get("506760dc-ed77-40d6-a91d-e0914e7a1ad8").getDataSize()); + Assert.assertTrue(metrics.containsKey(vmName)); + Assert.assertEquals(535875584L, (long) metrics.get(vmName).getBackupSize()); + Assert.assertEquals(2147507235L, (long) metrics.get(vmName).getDataSize()); } @Test diff --git a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java index c02513f4889b..08aed58d6fe1 100644 --- a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java +++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java @@ -27,9 +27,12 @@ import java.util.Map; import java.util.UUID; +import com.cloud.storage.VolumeApiService; +import com.cloud.utils.LogUtils; import com.cloud.agent.api.CleanupVMCommand; import javax.inject.Inject; +import com.google.common.collect.Lists; import com.cloud.agent.api.to.NfsTO; import com.cloud.cpu.CPU; import com.cloud.hypervisor.vmware.mo.DatastoreMO; @@ -40,6 +43,7 @@ import com.cloud.vm.VmDetailConstants; import com.vmware.vim25.VirtualMachinePowerState; import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.command.user.volume.DetachVolumeCmd; import org.apache.cloudstack.backup.Backup; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; @@ -200,6 +204,8 @@ public class VMwareGuru extends HypervisorGuruBase implements HypervisorGuru, Co @Inject DiskOfferingDao diskOfferingDao; @Inject PhysicalNetworkDao physicalNetworkDao; @Inject StoragePoolHostDao storagePoolHostDao; + @Inject + protected VolumeApiService volumeService; @Inject NfsMountManager mountManager; protected VMwareGuru() { @@ -527,7 +533,7 @@ private boolean isRootDisk(VirtualDisk disk, Map disksMap if (vm == null) { throw new CloudRuntimeException("Failed to find the volumes details from the VM backup"); } - List backedUpVolumes = vm.getBackupVolumeList(); + List backedUpVolumes = backup.getBackedUpVolumes(); for (Backup.VolumeInfo backedUpVolume : backedUpVolumes) { if (backedUpVolume.getSize().equals(disk.getCapacityInBytes())) { return backedUpVolume.getType().equals(Volume.Type.ROOT); @@ -583,13 +589,33 @@ private Long getPoolIdFromDatastoreUuid(long zoneId, String datastoreUuid) { /** * Get pool ID for disk */ - private Long getPoolId(long zoneId, VirtualDisk disk) { + protected Long getPoolId(VirtualDisk disk, long datacenterId, long clusterId) { VirtualDeviceBackingInfo backing = disk.getBacking(); checkBackingInfo(backing); VirtualDiskFlatVer2BackingInfo info = (VirtualDiskFlatVer2BackingInfo)backing; String[] fileNameParts = info.getFileName().split(" "); - String datastoreUuid = StringUtils.substringBetween(fileNameParts[0], "[", "]"); - return getPoolIdFromDatastoreUuid(zoneId, datastoreUuid); + String datastore = StringUtils.substringBetween(fileNameParts[0], "[", "]"); + if (UuidUtils.isUuidWithoutHyphens(datastore)) { + return getPoolIdFromDatastoreUuid(datacenterId, datastore); + } + return getPoolIdFromDatastoreNameOrPath(datastore, datacenterId, clusterId); + } + + protected Long getPoolIdFromDatastoreNameOrPath(String datastore, long datacenterId, long clusterId) { + logger.debug("Trying to find pool Id for datastore: [{}].", datastore); + + String errorMessage = String.format("Could not find storage pool with name or path [%s].", datastore); + StoragePoolVO poolVO = _storagePoolDao.findPoolByName(datastore, datacenterId, clusterId); + if (poolVO != null) { + return poolVO.getId(); + } + logger.debug("Could not find storage pool with name [{}]. Trying to search by path [{}] in datacenter [{}] and cluster [{}].", datastore, datastore, datacenterId, clusterId); + + poolVO = _storagePoolDao.findPoolByPathLike(datastore, datacenterId, clusterId); + if (poolVO == null) { + throw new CloudRuntimeException(errorMessage); + } + return poolVO.getId(); } /** @@ -821,19 +847,47 @@ private void syncVMVolumes(VMInstanceVO vmInstanceVO, List virtualD String operation = ""; for (VirtualDisk disk : virtualDisks) { - Long poolId = getPoolId(zoneId, disk); + Long clusterId = vmManager.findClusterAndHostIdForVm(instanceId).first(); + Long poolId = getPoolId(disk, zoneId, clusterId); Volume volume = null; - if (disksMapping.containsKey(disk) && disksMapping.get(disk) != null) { - volume = updateVolume(disk, disksMapping, vmToImport, poolId, vmInstanceVO); - operation = "updated"; + if (disksMapping.containsKey(disk)) { + if (disksMapping.get(disk) != null) { + volume = updateVolume(disk, disksMapping, vmToImport, poolId, vmInstanceVO); + operation = "updated"; + } else { + volume = createVolume(disk, vmToImport, domainId, zoneId, accountId, instanceId, poolId, templateId, backup, true); + operation = "created"; + } } else { - volume = createVolume(disk, vmToImport, domainId, zoneId, accountId, instanceId, poolId, templateId, backup, true); - operation = "created"; + volume = detachVolume(vmInstanceVO, disk, backup); + operation = "detached"; } logger.debug(String.format("Sync volumes to %s in backup restore operation: %s volume [id: %s].", vmInstanceVO, operation, volume.getUuid())); } } + protected VolumeVO detachVolume(VMInstanceVO vmInstanceVO, VirtualDisk disk, Backup backup) { + VolumeVO volume = null; + logger.debug(() -> LogUtils.logGsonWithoutException("Disk [%s] of VM [uuid: %s, name: %s] does not exist in the metadata of backup [uuid: %s]. Therefore, we need to detach it.", + disk, vmInstanceVO.getUuid(), vmInstanceVO.getInstanceName(), backup.getUuid())); + String volumeFullPath = getVolumeFullPath(disk); + volume = _volumeDao.findByPath(getVolumeNameFromFileName(volumeFullPath)); + if (volume != null && vmInstanceVO.getId() == volume.getInstanceId() && volume.getRemoved() == null) { + DetachVolumeCmd detachVolumeCmd = new DetachVolumeCmd(); + detachVolumeCmd.setId(volume.getId()); + Volume result = volumeService.detachVolumeFromVM(detachVolumeCmd); + if (result != null) { + logger.debug("Volume [uuid: {}] detached with success from VM [uuid: {}, name: {}], during the backup restore process (as this volume does not exist in the metadata of backup [uuid: {}]).", + result.getUuid(), vmInstanceVO.getUuid(), vmInstanceVO.getInstanceName(), backup.getUuid()); + } else { + logger.warn("Failed to detach volume [uuid: {}] from VM [uuid: {}, name: {}], during the backup restore process (as this volume does not exist in the metadata of backup [uuid: {}]).", + volume.getUuid(), vmInstanceVO.getUuid(), vmInstanceVO.getInstanceName(), backup.getUuid()); + } + } + return volume; + } + + private VirtualMachineDiskInfo getDiskInfo(VirtualMachineMO vmMo, Long poolId, String volumeName) throws Exception { VirtualMachineDiskInfoBuilder diskInfoBuilder = vmMo.getDiskInfoBuilder(); String poolName = _storagePoolDao.findById(poolId).getUuid().replace("-", ""); @@ -845,7 +899,7 @@ private VolumeVO createVolume(VirtualDisk disk, VirtualMachineMO vmToImport, lon if (vm == null) { throw new CloudRuntimeException("Failed to find the backup volume information from the VM backup"); } - List backedUpVolumes = vm.getBackupVolumeList(); + List backedUpVolumes = backup.getBackedUpVolumes(); Volume.Type type = Volume.Type.DATADISK; Long size = disk.getCapacityInBytes(); if (isImport) { @@ -870,7 +924,7 @@ protected String createVolumeInfoFromVolumes(List vmVolumes) { try { List list = new ArrayList<>(); for (VolumeVO vol : vmVolumes) { - list.add(new Backup.VolumeInfo(vol.getUuid(), vol.getPath(), vol.getVolumeType(), vol.getSize())); + list.add(new Backup.VolumeInfo(vol.getUuid(), vol.getPath(), vol.getVolumeType(), vol.getSize(), vol.getDeviceId())); } return GSON.toJson(list.toArray(), Backup.VolumeInfo[].class); } catch (Exception e) { @@ -1066,28 +1120,16 @@ private Map getDisksMapping(Backup backup, List backedUpVolumes = vm.getBackupVolumeList(); + List backedUpVolumes = backup.getBackedUpVolumes(); Map usedVols = new HashMap<>(); Map map = new HashMap<>(); for (Backup.VolumeInfo backedUpVol : backedUpVolumes) { - VolumeVO volumeExtra = _volumeDao.findByUuid(backedUpVol.getUuid()); - if (volumeExtra != null) { - logger.debug(String.format("Marking volume [id: %s] of VM [%s] as removed for the backup process.", backedUpVol.getUuid(), ReflectionToStringBuilderUtils.reflectOnlySelectedFields(vm, "uuid", "instanceName"))); - _volumeDao.remove(volumeExtra.getId()); - - if (vm.getType() == Type.User) { - UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_DETACH, volumeExtra.getAccountId(), volumeExtra.getDataCenterId(), volumeExtra.getId(), - volumeExtra.getName(), volumeExtra.getDiskOfferingId(), null, volumeExtra.getSize(), Volume.class.getName(), - volumeExtra.getUuid(), volumeExtra.isDisplayVolume()); - _resourceLimitService.decrementResourceCount(vm.getAccountId(), Resource.ResourceType.volume, volumeExtra.isDisplayVolume()); - _resourceLimitService.decrementResourceCount(vm.getAccountId(), Resource.ResourceType.primary_storage, volumeExtra.isDisplayVolume(), volumeExtra.getSize()); - } - } for (VirtualDisk disk : virtualDisks) { if (!map.containsKey(disk) && backedUpVol.getSize().equals(disk.getCapacityInBytes()) && !usedVols.containsKey(backedUpVol.getUuid())) { String volId = backedUpVol.getUuid(); VolumeVO vol = _volumeDao.findByUuidIncludingRemoved(volId); + vol.setDeviceId(backedUpVol.getDeviceId()); usedVols.put(backedUpVol.getUuid(), true); map.put(disk, vol); logger.debug("VM restore mapping for disk " + disk.getBacking() + " (capacity: " + toHumanReadableSize(disk.getCapacityInBytes()) + ") with volume ID" + vol.getId()); @@ -1111,16 +1153,23 @@ private VirtualMachineMO findVM(DatacenterMO dcMo, String path) throws Exception /** * Find restored volume based on volume info */ - private VirtualDisk findRestoredVolume(Backup.VolumeInfo volumeInfo, VirtualMachineMO vm) throws Exception { - List virtualDisks = vm.getVirtualDisks(); + protected VirtualDisk findRestoredVolume(Backup.VolumeInfo volumeInfo, VirtualMachineMO vm, String volumeName, int deviceId) throws Exception { + List virtualDisks = Lists.reverse(vm.getVirtualDisks()); + logger.debug(LogUtils.logGsonWithoutException("Trying to find restored volume with size [%s], name [%s] and deviceId (unitNumber in VMWare) [%s] " + + "in VM [%s] disks [%s].", volumeInfo.getSize(), volumeName, deviceId, vm.getVmName(), virtualDisks)); for (VirtualDisk disk : virtualDisks) { - if (disk.getCapacityInBytes().equals(volumeInfo.getSize())) { - return disk; + VirtualDeviceBackingInfo backingInfo = disk.getBacking(); + if (backingInfo instanceof VirtualDiskFlatVer2BackingInfo) { + VirtualDiskFlatVer2BackingInfo diskBackingInfo = (VirtualDiskFlatVer2BackingInfo)backingInfo; + if (disk.getCapacityInBytes().equals(volumeInfo.getSize()) && diskBackingInfo.getFileName().contains(volumeName) && disk.getUnitNumber() == deviceId) { + return disk; + } } } throw new CloudRuntimeException("Volume to restore could not be found"); } + /** * Get volume full path */ @@ -1187,8 +1236,9 @@ public boolean attachRestoredVolumeToVirtualMachine(long zoneId, String location throws Exception { DatacenterMO dcMo = getDatacenterMO(zoneId); VirtualMachineMO vmRestored = findVM(dcMo, location); + int newDeviceId = (int) (_volumeDao.findByInstance(vm.getId()).stream().mapToLong(VolumeVO::getDeviceId).max().orElse(0L) + 1); VirtualMachineMO vmMo = findVM(dcMo, vm.getInstanceName()); - VirtualDisk restoredDisk = findRestoredVolume(volumeInfo, vmRestored); + VirtualDisk restoredDisk = findRestoredVolume(volumeInfo, vmRestored, location.split(".vmdk")[0], newDeviceId); String diskPath = vmRestored.getVmdkFileBaseName(restoredDisk); logger.debug("Restored disk size=" + toHumanReadableSize(restoredDisk.getCapacityInKB() * Resource.ResourceType.bytesToKiB) + " path=" + diskPath); diff --git a/plugins/hypervisors/vmware/src/test/java/com/cloud/hypervisor/guru/VMwareGuruTest.java b/plugins/hypervisors/vmware/src/test/java/com/cloud/hypervisor/guru/VMwareGuruTest.java index 4da513db3e4e..b98456af4136 100644 --- a/plugins/hypervisors/vmware/src/test/java/com/cloud/hypervisor/guru/VMwareGuruTest.java +++ b/plugins/hypervisors/vmware/src/test/java/com/cloud/hypervisor/guru/VMwareGuruTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -29,10 +30,18 @@ import static org.mockito.Mockito.withSettings; import java.util.ArrayList; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import com.cloud.hypervisor.vmware.mo.VirtualMachineMO; +import com.cloud.storage.VolumeApiService; +import com.cloud.storage.dao.VolumeDao; +import com.vmware.vim25.VirtualDisk; +import com.vmware.vim25.VirtualDiskFlatVer2BackingInfo; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupVO; import org.apache.cloudstack.storage.NfsMountManager; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; @@ -65,7 +74,6 @@ import com.cloud.hypervisor.vmware.mo.DatacenterMO; import com.cloud.hypervisor.vmware.mo.DatastoreMO; import com.cloud.hypervisor.vmware.mo.HostMO; -import com.cloud.hypervisor.vmware.mo.VirtualMachineMO; import com.cloud.hypervisor.vmware.util.VmwareClient; import com.cloud.hypervisor.vmware.util.VmwareContext; import com.cloud.hypervisor.vmware.util.VmwareHelper; @@ -77,6 +85,7 @@ import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.StoragePoolHostDao; import com.cloud.utils.Pair; +import com.cloud.vm.VMInstanceVO; import com.cloud.utils.UuidUtils; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VirtualMachine; @@ -110,6 +119,12 @@ public class VMwareGuruTest { @Mock ClusterDetailsDao _clusterDetailsDao; + @Mock + VolumeDao volumeDao; + + @Mock + VolumeApiService volumeService; + AutoCloseable closeable; @Mock @@ -174,6 +189,103 @@ public void finalizeMigrateForLocalStorageToHaveTargetHostGuid(){ Assert.assertEquals("HostSystem:host-a@x.x.x.x", migrateVmToPoolCommand.getHostGuidInTargetCluster()); } + @Test + public void detachVolumeTestWhenVolumePathDontExistsInDb() { + VirtualDisk virtualDisk = Mockito.mock(VirtualDisk.class); + VirtualDiskFlatVer2BackingInfo info = Mockito.mock(VirtualDiskFlatVer2BackingInfo.class); + Mockito.when(virtualDisk.getBacking()).thenReturn(info); + Mockito.when(info.getFileName()).thenReturn("[ae4e2064cdbf3587908f726a23f9a5a3] i-2-444-VM/6b10e0316c5e441dbaeb23a806679c8d.vmdk"); + Mockito.when(volumeDao.findByPath(Mockito.eq("6b10e0316c5e441dbaeb23a806679c8d"))).thenReturn(null); + VMInstanceVO vmInstanceVO = new VMInstanceVO(); + BackupVO backupVO = new BackupVO(); + + VolumeVO detachVolume = vMwareGuru.detachVolume(vmInstanceVO, virtualDisk, backupVO); + Assert.assertEquals(null, detachVolume); + } + + @Test + public void detachVolumeTestWhenVolumeExistsButIsOwnedByAnotherInstance() { + VirtualDisk virtualDisk = Mockito.mock(VirtualDisk.class); + VirtualDiskFlatVer2BackingInfo info = Mockito.mock(VirtualDiskFlatVer2BackingInfo.class); + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + VMInstanceVO vmInstanceVO = Mockito.mock(VMInstanceVO.class); + + Mockito.when(virtualDisk.getBacking()).thenReturn(info); + Mockito.when(info.getFileName()).thenReturn("[ae4e2064cdbf3587908f726a23f9a5a3] i-2-444-VM/6b10e0316c5e441dbaeb23a806679c8d.vmdk"); + Mockito.when(volumeDao.findByPath(Mockito.eq("6b10e0316c5e441dbaeb23a806679c8d"))).thenReturn(volumeVO); + Mockito.when(volumeVO.getInstanceId()).thenReturn(2L); + Mockito.when(vmInstanceVO.getId()).thenReturn(1L); + BackupVO backupVO = new BackupVO(); + + Mockito.verify(volumeService, Mockito.never()).detachVolumeFromVM(Mockito.any()); + vMwareGuru.detachVolume(vmInstanceVO, virtualDisk, backupVO); + } + + @Test + public void detachVolumeTestWhenVolumeExistsButIsRemoved() { + VirtualDisk virtualDisk = Mockito.mock(VirtualDisk.class); + VirtualDiskFlatVer2BackingInfo info = Mockito.mock(VirtualDiskFlatVer2BackingInfo.class); + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + VMInstanceVO vmInstanceVO = Mockito.mock(VMInstanceVO.class); + + Mockito.when(volumeVO.getInstanceId()).thenReturn(1L); + Mockito.when(volumeVO.getRemoved()).thenReturn(new Date()); + Mockito.when(virtualDisk.getBacking()).thenReturn(info); + Mockito.when(info.getFileName()).thenReturn("[ae4e2064cdbf3587908f726a23f9a5a3] i-2-444-VM/6b10e0316c5e441dbaeb23a806679c8d.vmdk"); + Mockito.when(volumeDao.findByPath(Mockito.eq("6b10e0316c5e441dbaeb23a806679c8d"))).thenReturn(volumeVO); + Mockito.when(vmInstanceVO.getId()).thenReturn(1L); + BackupVO backupVO = new BackupVO(); + + Mockito.verify(volumeService, Mockito.never()).detachVolumeFromVM(Mockito.any()); + vMwareGuru.detachVolume(vmInstanceVO, virtualDisk, backupVO); + } + + @Test + public void detachVolumeTestWhenVolumeExistsButDetachFail() { + VirtualDisk virtualDisk = Mockito.mock(VirtualDisk.class); + VirtualDiskFlatVer2BackingInfo info = Mockito.mock(VirtualDiskFlatVer2BackingInfo.class); + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + VMInstanceVO vmInstanceVO = Mockito.mock(VMInstanceVO.class); + BackupVO backupVO = Mockito.mock(BackupVO.class); + + Mockito.when(volumeVO.getInstanceId()).thenReturn(1L); + Mockito.when(volumeVO.getUuid()).thenReturn("123"); + Mockito.when(backupVO.getUuid()).thenReturn("321"); + Mockito.when(vmInstanceVO.getInstanceName()).thenReturn("test1"); + Mockito.when(vmInstanceVO.getUuid()).thenReturn("1234"); + Mockito.when(virtualDisk.getBacking()).thenReturn(info); + Mockito.when(info.getFileName()).thenReturn("[ae4e2064cdbf3587908f726a23f9a5a3] i-2-444-VM/6b10e0316c5e441dbaeb23a806679c8d.vmdk"); + Mockito.when(volumeDao.findByPath(Mockito.eq("6b10e0316c5e441dbaeb23a806679c8d"))).thenReturn(volumeVO); + Mockito.when(vmInstanceVO.getId()).thenReturn(1L); + Mockito.when(volumeService.detachVolumeFromVM(Mockito.any())).thenReturn(null); + + vMwareGuru.detachVolume(vmInstanceVO, virtualDisk, backupVO); + Mockito.verify(volumeService, Mockito.times(1)).detachVolumeFromVM(Mockito.any()); + } + + @Test + public void detachVolumeTestWhenVolumeExistsAndDetachDontFail() { + VirtualDisk virtualDisk = Mockito.mock(VirtualDisk.class); + VirtualDiskFlatVer2BackingInfo info = Mockito.mock(VirtualDiskFlatVer2BackingInfo.class); + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + VMInstanceVO vmInstanceVO = Mockito.mock(VMInstanceVO.class); + BackupVO backupVO = Mockito.mock(BackupVO.class); + + Mockito.when(volumeVO.getInstanceId()).thenReturn(1L); + Mockito.when(volumeVO.getUuid()).thenReturn("123"); + Mockito.when(backupVO.getUuid()).thenReturn("321"); + Mockito.when(vmInstanceVO.getInstanceName()).thenReturn("test1"); + Mockito.when(vmInstanceVO.getUuid()).thenReturn("1234"); + Mockito.when(virtualDisk.getBacking()).thenReturn(info); + Mockito.when(info.getFileName()).thenReturn("[ae4e2064cdbf3587908f726a23f9a5a3] i-2-444-VM/6b10e0316c5e441dbaeb23a806679c8d.vmdk"); + Mockito.when(volumeDao.findByPath(Mockito.eq("6b10e0316c5e441dbaeb23a806679c8d"))).thenReturn(volumeVO); + Mockito.when(vmInstanceVO.getId()).thenReturn(1L); + Mockito.when(volumeService.detachVolumeFromVM(Mockito.any())).thenReturn(volumeVO); + + vMwareGuru.detachVolume(vmInstanceVO, virtualDisk, backupVO); + Mockito.verify(volumeService, Mockito.times(1)).detachVolumeFromVM(Mockito.any()); + } + @Test public void createVolumeInfoFromVolumesTestEmptyVolumeListReturnEmptyArray() { String volumeInfo = vMwareGuru.createVolumeInfoFromVolumes(new ArrayList<>()); @@ -204,6 +316,19 @@ public void createVolumeInfoFromVolumesTestCorrectlyConvertOfVolumes() { assertEquals(expected, result); } + @Test + public void findRestoredVolumeTestNotFindRestoredVolume() throws Exception { + VirtualMachineMO vmInstanceVO = Mockito.mock(VirtualMachineMO.class); + Backup.VolumeInfo volumeInfo = Mockito.mock(Backup.VolumeInfo.class); + Mockito.when(volumeInfo.getSize()).thenReturn(52l); + Mockito.when(vmInstanceVO.getVirtualDisks()).thenReturn(new ArrayList<>()); + try { + vMwareGuru.findRestoredVolume(volumeInfo, vmInstanceVO, null, 0); + } catch (Exception e) { + assertEquals("Volume to restore could not be found", e.getMessage()); + } + } + @Test(expected=CloudRuntimeException.class) public void testCloneHypervisorVM_NoExternalVM() throws Exception { String vCenterHost = "10.1.1.2"; @@ -386,6 +511,72 @@ public void testCloneHypervisorVM_CloneVMFailed() throws Exception { } } + @Test + public void findRestoredVolumeTestFindRestoredVolume() throws Exception { + Backup.VolumeInfo volumeInfo = Mockito.mock(Backup.VolumeInfo.class); + VirtualMachineMO vmInstanceVO = Mockito.mock(VirtualMachineMO.class); + VirtualDisk virtualDisk = Mockito.mock(VirtualDisk.class); + VirtualDiskFlatVer2BackingInfo info = Mockito.mock(VirtualDiskFlatVer2BackingInfo.class); + ArrayList disks = new ArrayList<>(); + disks.add(virtualDisk); + Mockito.when(volumeInfo.getSize()).thenReturn(52l); + Mockito.when(virtualDisk.getCapacityInBytes()).thenReturn(52l); + Mockito.when(info.getFileName()).thenReturn("test.vmdk"); + Mockito.when(virtualDisk.getBacking()).thenReturn(info); + Mockito.when(virtualDisk.getUnitNumber()).thenReturn(1); + Mockito.when(vmInstanceVO.getVirtualDisks()).thenReturn(disks); + VirtualDisk findRestoredVolume = vMwareGuru.findRestoredVolume(volumeInfo, vmInstanceVO, "test", 1); + assertNotNull(findRestoredVolume); + } + + @Test + public void getPoolIdTestDatastoreNameIsUuid() { + VirtualDisk virtualDisk = Mockito.mock(VirtualDisk.class); + VirtualDiskFlatVer2BackingInfo backingInfo = Mockito.mock(VirtualDiskFlatVer2BackingInfo.class); + StoragePoolVO poolVO = Mockito.mock(StoragePoolVO.class); + + Mockito.when(poolVO.getId()).thenReturn(10L); + Mockito.when(backingInfo.getFileName()).thenReturn("[5758578ed6a6454087a6c62f13df8ce2] test/test.vmdk"); + Mockito.when(virtualDisk.getBacking()).thenReturn(backingInfo); + Mockito.when(_storagePoolDao.findByUuid("5758578e-d6a6-4540-87a6-c62f13df8ce2")).thenReturn(poolVO); + + Long result = vMwareGuru.getPoolId(virtualDisk, 1L, 2L); + assertEquals(10l, (long)result); + } + + @Test + public void getPoolIdTestDatastoreNameIsNotUuid() { + VirtualDisk virtualDisk = Mockito.mock(VirtualDisk.class); + VirtualDiskFlatVer2BackingInfo backingInfo = Mockito.mock(VirtualDiskFlatVer2BackingInfo.class); + StoragePoolVO poolVO = Mockito.mock(StoragePoolVO.class); + + Mockito.when(poolVO.getId()).thenReturn(11L); + Mockito.when(backingInfo.getFileName()).thenReturn("[storage-name] test/test.vmdk"); + Mockito.when(virtualDisk.getBacking()).thenReturn(backingInfo); + Mockito.when(_storagePoolDao.findPoolByName("storage-name", 1L, 2L)).thenReturn(poolVO); + + Long result = vMwareGuru.getPoolId(virtualDisk, 1L, 2L); + assertEquals(11l, (long)result); + } + + @Test + public void getPoolIdFromDatastoreNameOrPathTestStorageDoesNotExist() { + VirtualDisk virtualDisk = Mockito.mock(VirtualDisk.class); + VirtualDiskFlatVer2BackingInfo backingInfo = Mockito.mock(VirtualDiskFlatVer2BackingInfo.class); + + Mockito.when(backingInfo.getFileName()).thenReturn("[storage-name] test/test.vmdk"); + Mockito.when(virtualDisk.getBacking()).thenReturn(backingInfo); + Mockito.when(_storagePoolDao.findPoolByPathLike("storage-name", 1L, 2L)).thenReturn(null); + + try { + vMwareGuru.getPoolId(virtualDisk, 1L, 2L); + fail(); + } catch (Exception e) { + assertEquals("Could not find storage pool with name or path [storage-name].", e.getMessage()); + } + } + + @Test public void testCloneHypervisorVM() throws Exception { String vCenterHost = "10.1.1.2"; @@ -485,6 +676,37 @@ public void testCreateVMTemplateFileOutOfBand_NoClonedVM() throws Exception { } } + @Test + public void getPoolIdFromDatastoreNameOrPathTestStorageNameExist() { + VirtualDisk virtualDisk = Mockito.mock(VirtualDisk.class); + VirtualDiskFlatVer2BackingInfo backingInfo = Mockito.mock(VirtualDiskFlatVer2BackingInfo.class); + StoragePoolVO poolVO = Mockito.mock(StoragePoolVO.class); + + Mockito.when(poolVO.getId()).thenReturn(13l); + Mockito.when(_storagePoolDao.findPoolByName("storage-name", 1L, 2L)).thenReturn(poolVO); + Mockito.when(backingInfo.getFileName()).thenReturn("[storage-name] test/test.vmdk"); + Mockito.when(virtualDisk.getBacking()).thenReturn(backingInfo); + + Long result = vMwareGuru.getPoolId(virtualDisk, 1L, 2L); + assertEquals(13l, (long)result); + } + + @Test + public void getPoolIdFromDatastoreNameOrPathTestStoragePathExist() { + VirtualDisk virtualDisk = Mockito.mock(VirtualDisk.class); + VirtualDiskFlatVer2BackingInfo backingInfo = Mockito.mock(VirtualDiskFlatVer2BackingInfo.class); + StoragePoolVO poolVO = Mockito.mock(StoragePoolVO.class); + + Mockito.when(poolVO.getId()).thenReturn(14l); + Mockito.when(_storagePoolDao.findPoolByPathLike("storage-name", 1l, 2L)).thenReturn(poolVO); + Mockito.when(backingInfo.getFileName()).thenReturn("[storage-name] test/test.vmdk"); + Mockito.when(virtualDisk.getBacking()).thenReturn(backingInfo); + + Long result = vMwareGuru.getPoolId(virtualDisk, 1L, 2L); + assertEquals(14l, (long)result); + } + + @Test public void testCreateVMTemplateFileOutOfBand() throws Exception { String vCenterHost = "10.1.1.2"; diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 1a25f36efe21..1b4187438433 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -2714,10 +2714,7 @@ private void checkForDevicesInCopies(Long vmId, UserVmVO vm) { } // if target VM has backups - List backups = backupDao.listByVmId(vm.getDataCenterId(), vm.getId()); - if (vm.getBackupOfferingId() != null && !backups.isEmpty()) { - throw new InvalidParameterValueException(String.format("Unable to attach volume to VM %s/%s, please specify a VM that does not have any backups", vm.getName(), vm.getUuid())); - } + validateIfVmHasBackups(vm, true); } /** @@ -2834,7 +2831,7 @@ protected String createVolumeInfoFromVolumes(List vmVolumes) { try { List list = new ArrayList<>(); for (VolumeVO vol : vmVolumes) { - list.add(new Backup.VolumeInfo(vol.getUuid(), vol.getPath(), vol.getVolumeType(), vol.getSize())); + list.add(new Backup.VolumeInfo(vol.getUuid(), vol.getPath(), vol.getVolumeType(), vol.getSize(), vol.getDeviceId())); } return GsonHelper.getGson().toJson(list.toArray(), Backup.VolumeInfo[].class); } catch (Exception e) { diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 021c6ff62267..83244ea1204d 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -133,6 +133,7 @@ import org.apache.cloudstack.storage.template.VnfTemplateManager; import org.apache.cloudstack.userdata.UserDataManager; import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.cloudstack.utils.security.ParserUtils; import org.apache.cloudstack.vm.schedule.VMScheduleManager; import org.apache.cloudstack.vm.UnmanagedVMsManager; @@ -2476,18 +2477,7 @@ public boolean expunge(UserVmVO vm) { return false; } try { - - if (vm.getBackupOfferingId() != null) { - List backupsForVm = backupDao.listByVmId(vm.getDataCenterId(), vm.getId()); - if (CollectionUtils.isEmpty(backupsForVm)) { - backupManager.removeVMFromBackupOffering(vm.getId(), true); - } else { - throw new CloudRuntimeException(String.format("This VM [uuid: %s, name: %s] has a " - + "Backup Offering [id: %s, external id: %s] with %s backups. Please, remove the backup offering " - + "before proceeding to VM exclusion!", vm.getUuid(), vm.getInstanceName(), vm.getBackupOfferingId(), - vm.getBackupExternalId(), backupsForVm.size())); - } - } + removeBackupOfferingBeforeDeleteVmIfNeeded(vm); autoScaleManager.removeVmFromVmGroup(vm.getId()); @@ -2533,6 +2523,32 @@ public boolean expunge(UserVmVO vm) { } } + protected void removeBackupOfferingBeforeDeleteVmIfNeeded(UserVmVO vm) { + if (vm.getBackupOfferingId() == null) { + logger.debug("VM [{}] does not have a Backup Offering. Don't need to remove then.", + () -> ReflectionToStringBuilderUtils.reflectOnlySelectedFields(vm, "uuid", "instanceName")); + return; + } + logger.debug("VM [{}] has backup offering with id [{}]. Trying to remove this backup offering.", + () -> ReflectionToStringBuilderUtils.reflectOnlySelectedFields(vm, "uuid", "instanceName"), vm::getBackupOfferingId); + List backupsForVm = backupDao.listByVmId(vm.getDataCenterId(), vm.getId()); + if (CollectionUtils.isEmpty(backupsForVm)) { + logger.debug("VM [{}] with backup offering [id: {}] does not have any backups. Trying to delete job.", + () -> ReflectionToStringBuilderUtils.reflectOnlySelectedFields(vm, "uuid", "instanceName"), vm::getBackupOfferingId); + backupManager.removeVMFromBackupOffering(vm.getId(), true); + } else if (backupManager.getName().equalsIgnoreCase("veeam")){ + logger.debug("VM [uuid: {}, name: {}] has a Backup Offering [id: {}, external id: {}] with {} backups. " + + "Trying to disable/remove only the job, but keeping the backups.", vm.getUuid(), vm.getInstanceName(), vm.getBackupOfferingId(), + vm.getBackupExternalId(), backupsForVm.size()); + backupManager.removeVMFromBackupOffering(vm.getId(), false); + } else { + throw new CloudRuntimeException(String.format("This VM [uuid: %s, name: %s] has a " + + "Backup Offering [id: %s, external id: %s] with %s backups. Please, remove the backup offering " + + "before proceeding to VM exclusion!", vm.getUuid(), vm.getInstanceName(), vm.getBackupOfferingId(), + vm.getBackupExternalId(), backupsForVm.size())); + } + } + /** * Release network resources, it was done on vm stop previously. * @param id vm id @@ -3440,9 +3456,11 @@ public UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, C stopVirtualMachine(vmId, VmDestroyForcestop.value()); - // Detach all data disks from VM - List dataVols = _volsDao.findByInstanceAndType(vmId, Volume.Type.DATADISK); - detachVolumesFromVm(vm, dataVols); + if (vm.getHypervisorType() == HypervisorType.VMware) { + removeBackupOfferingIfNeededAndDetachVolumes(vm); + } else { + detachVolumesFromVm(vm, volumesToBeDeleted); + } UserVm destroyedVm = destroyVm(vmId, expunge); if (expunge && !expunge(vm)) { @@ -3465,6 +3483,13 @@ public UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, C return destroyedVm; } + protected void removeBackupOfferingIfNeededAndDetachVolumes(UserVmVO vm) { + removeBackupOfferingBeforeDeleteVmIfNeeded(vm); + List allVolumes = _volsDao.findByInstance(vm.getId()); + allVolumes.removeIf(vol -> vol.getVolumeType() == Volume.Type.ROOT); + detachVolumesFromVm(vm, allVolumes); + } + private List getVolumesFromIds(DestroyVMCmd cmd) { List volumes = new ArrayList<>(); if (cmd.getVolumeIds() != null) { diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index 68b6a2ad0470..2340330c87f6 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -292,7 +292,7 @@ public boolean deleteBackupOffering(final Long offeringId) { public static String createVolumeInfoFromVolumes(List vmVolumes) { List list = new ArrayList<>(); for (VolumeVO vol : vmVolumes) { - list.add(new Backup.VolumeInfo(vol.getUuid(), vol.getPath(), vol.getVolumeType(), vol.getSize())); + list.add(new Backup.VolumeInfo(vol.getUuid(), vol.getPath(), vol.getVolumeType(), vol.getSize(), vol.getDeviceId())); } return new Gson().toJson(list.toArray(), Backup.VolumeInfo[].class); } @@ -341,15 +341,16 @@ public VMInstanceVO doInTransaction(final TransactionStatus status) { } if (!vmInstanceDao.update(vmId, vm)) { - backupProvider.removeVMFromBackupOffering(vm); + backupProvider.removeVMFromBackupOffering(vm, true); throw new CloudRuntimeException("Failed to update VM assignment to the backup offering in the DB, please try again."); } UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VM_BACKUP_OFFERING_ASSIGN, vm.getAccountId(), vm.getDataCenterId(), vmId, "Backup-" + vm.getHostName() + "-" + vm.getUuid(), vm.getBackupOfferingId(), null, null, Backup.class.getSimpleName(), vm.getUuid()); - logger.debug(String.format("VM [%s] successfully added to Backup Offering [%s].", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(vm, - "uuid", "instanceName", "backupOfferingId", "backupVolumes"), ReflectionToStringBuilderUtils.reflectOnlySelectedFields(offering, - "uuid", "name", "externalId", "provider"))); + logger.debug("VM [{}] successfully added to Backup Offering [{}].", + () -> ReflectionToStringBuilderUtils.reflectOnlySelectedFields(vm, "uuid", "instanceName", "backupOfferingId", "backupVolumes"), + () -> ReflectionToStringBuilderUtils.reflectOnlySelectedFields(offering, "uuid", "name", "externalId", "provider")); + return vm; } catch (Exception e) { String msg = String.format("Failed to assign VM [%s] to the Backup Offering [%s], using provider [name: %s, class: %s], due to: [%s].", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(vm, "uuid", "instanceName", "backupOfferingId", "backupVolumes"), @@ -359,7 +360,6 @@ public VMInstanceVO doInTransaction(final TransactionStatus status) { logger.debug(msg, e); return null; } - return vm; } }); } @@ -394,17 +394,17 @@ public boolean removeVMFromBackupOffering(final Long vmId, final boolean forced) boolean result = false; try { - result = backupProvider.removeVMFromBackupOffering(vm); vm.setBackupOfferingId(null); - vm.setBackupVolumes(null); vm.setBackupExternalId(null); - if (result && backupProvider.willDeleteBackupsOnOfferingRemoval()) { + vm.setBackupVolumes(null); + result = backupProvider.removeVMFromBackupOffering(vm, forced); + if (result && (backupProvider.willDeleteBackupsOnOfferingRemoval() || forced)) { final List backups = backupDao.listByVmId(null, vm.getId()); for (final Backup backup : backups) { backupDao.remove(backup.getId()); } } - if ((result || forced) && vmInstanceDao.update(vm.getId(), vm)) { + if (result && vmInstanceDao.update(vm.getId(), vm)) { UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VM_BACKUP_OFFERING_REMOVE, vm.getAccountId(), vm.getDataCenterId(), vm.getId(), "Backup-" + vm.getHostName() + "-" + vm.getUuid(), vm.getBackupOfferingId(), null, null, Backup.class.getSimpleName(), vm.getUuid()); @@ -887,7 +887,7 @@ private void tryToUpdateStateOfSpecifiedVolume(VolumeVO volume, Volume.Event eve logger.debug(String.format("Trying to update state of volume [%s] with event [%s].", volume, event)); try { if (!volumeApiService.stateTransitTo(volume, event)) { - throw new CloudRuntimeException(String.format("Unable to change state of volume [%s] to [%s].", volume, next)); + throw new CloudRuntimeException(String.format("Unable to change state of volume [%s] to [%s].", volume.getVolumeDescription(), next)); } } catch (NoTransitionException e) { String errMsg = String.format("Failed to update state of volume [%s] with event [%s] due to [%s].", volume, event, e.getMessage()); @@ -964,9 +964,11 @@ public boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid, throw new CloudRuntimeException(String.format("Error restoring volume [%s] of VM [%s] to host [%s] using backup provider [%s] due to: [%s].", backedUpVolumeUuid, vm.getUuid(), host.getUuid(), backupProvider.getName(), result.second())); } - if (!attachVolumeToVM(vm.getDataCenterId(), result.second(), vmFromBackup.getBackupVolumeList(), + + if (!attachVolumeToVM(vm.getDataCenterId(), result.second(), CollectionUtils.isNullOrEmpty(backup.getBackedUpVolumes()) ? + vm.getBackupVolumeList() : backup.getBackedUpVolumes(), backedUpVolumeUuid, vm, datastore.getUuid(), backup)) { - throw new CloudRuntimeException(String.format("Error attaching volume [%s] to VM [%s]." + backedUpVolumeUuid, vm.getUuid())); + throw new CloudRuntimeException(String.format("Error attaching volume [%s] to VM [%s].", backedUpVolumeUuid, vm.getUuid())); } return true; } diff --git a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java index 7dcf30c55e40..04fc17dd774a 100644 --- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java +++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java @@ -666,7 +666,6 @@ public void testResourceLimitCheckForUploadedVolume() throws NoSuchFieldExceptio when(vm.getState()).thenReturn(State.Running); when(vm.getDataCenterId()).thenReturn(34L); when(vm.getBackupOfferingId()).thenReturn(null); - when(backupDaoMock.listByVmId(anyLong(), anyLong())).thenReturn(Collections.emptyList()); when(volumeDaoMock.findByInstanceAndType(anyLong(), any(Volume.Type.class))).thenReturn(new ArrayList<>(10)); when(volumeDataFactoryMock.getVolume(9L)).thenReturn(volumeToAttach); when(volumeToAttach.getState()).thenReturn(Volume.State.Uploaded); @@ -1388,6 +1387,12 @@ public void isNotPossibleToResizeTestNoRootDiskSize() { prepareAndRunTestOfIsNotPossibleToResize(Type.ROOT, 0l, Storage.ImageFormat.QCOW2, false); } + public void validateIfVMHaveBackupsTestSucessWhenVMDontHaveBackupOffering() { + UserVmVO vm = Mockito.mock(UserVmVO.class); + when(vm.getBackupOfferingId()).thenReturn(null); + volumeApiServiceImpl.validateIfVmHasBackups(vm, true); + } + private void prepareAndRunTestOfIsNotPossibleToResize(Type volumeType, Long rootDisk, Storage.ImageFormat imageFormat, boolean expectedIsNotPossibleToResize) { VolumeVO volume = Mockito.mock(VolumeVO.class); when(volume.getVolumeType()).thenReturn(volumeType); diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java index f07d2af21af2..58d360e996e9 100644 --- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java +++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java @@ -50,6 +50,10 @@ import org.apache.cloudstack.api.command.user.vm.RestoreVMCmd; import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd; import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.BackupVO; +import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; @@ -239,6 +243,12 @@ public class UserVmManagerImplTest { @Mock ResourceLimitService resourceLimitMgr; + @Mock + BackupManager backupManager; + + @Mock + BackupDao backupDao; + @Mock VolumeApiService volumeApiService; @@ -802,6 +812,37 @@ public void prepareResizeVolumeCmdTestNewOfferingSmaller() { prepareAndRunResizeVolumeTest(2L, 10L, 20L, largerDisdkOffering, smallerDisdkOffering); } + @Test + public void removeBackupOfferingBeforeDeleteVmIfNeededTestWhenVmHaveBackupOfferingAndBackups() { + Mockito.when(userVmVoMock.getBackupOfferingId()).thenReturn(1l); + Mockito.when(userVmVoMock.getDataCenterId()).thenReturn(2l); + Mockito.when(userVmVoMock.getId()).thenReturn(2l); + Mockito.when(backupManager.getName()).thenReturn("veeam"); + + List backupsForVm = new ArrayList<>(); + backupsForVm.add(new BackupVO()); + Mockito.when(backupDao.listByVmId(Mockito.eq(2l), Mockito.eq(2l))).thenReturn(backupsForVm); + userVmManagerImpl.removeBackupOfferingBeforeDeleteVmIfNeeded(userVmVoMock); + Mockito.verify(backupManager).removeVMFromBackupOffering(Mockito.eq(2l), Mockito.eq(false)); + } + + @Test + public void removeBackupOfferingBeforeDeleteVmIfNeededTestRemoveAllIfVmHasNoBackups() { + Mockito.when(userVmVoMock.getBackupOfferingId()).thenReturn(1l); + Mockito.when(userVmVoMock.getDataCenterId()).thenReturn(2l); + Mockito.when(userVmVoMock.getId()).thenReturn(2l); + Mockito.when(backupDao.listByVmId(Mockito.eq(2l), Mockito.eq(2l))).thenReturn(new ArrayList<>()); + userVmManagerImpl.removeBackupOfferingBeforeDeleteVmIfNeeded(userVmVoMock); + Mockito.verify(backupManager).removeVMFromBackupOffering(Mockito.eq(2l), Mockito.eq(true)); + } + + @Test + public void removeBackupOfferingBeforeDeleteVmIfNeededTestDoNothingWhenVmHasNoBackupOffering() { + Mockito.when(userVmVoMock.getBackupOfferingId()).thenReturn(null); + userVmManagerImpl.removeBackupOfferingBeforeDeleteVmIfNeeded(userVmVoMock); + Mockito.verify(backupManager, Mockito.never()).removeVMFromBackupOffering(Mockito.anyLong(), Mockito.anyBoolean()); + } + private void prepareAndRunResizeVolumeTest(Long expectedOfferingId, long expectedMinIops, long expectedMaxIops, DiskOfferingVO currentRootDiskOffering, DiskOfferingVO newRootDiskOffering) { long rootVolumeId = 1l; VolumeVO rootVolumeOfVm = Mockito.mock(VolumeVO.class); diff --git a/utils/src/main/java/com/cloud/utils/UuidUtils.java b/utils/src/main/java/com/cloud/utils/UuidUtils.java index 42604e5c8a0d..b956ca6b9c42 100644 --- a/utils/src/main/java/com/cloud/utils/UuidUtils.java +++ b/utils/src/main/java/com/cloud/utils/UuidUtils.java @@ -40,6 +40,18 @@ public static boolean isUuid(String uuid) { return uuidRegex.matches(uuid); } + /** + * Checks if a string is a valid 32 characters UUID without hyphens, e.g. 24abcb8f4211374fa2e1e5c0b7e88a2d + */ + public static boolean isUuidWithoutHyphens(String uuid) { + try { + normalize(uuid); + return true; + } catch (Exception e) { + return false; + } + } + /** * Returns a valid UUID in string format from a 32 digit UUID string without hyphens. * Example: 24abcb8f4211374fa2e1e5c0b7e88a2d -> 24abcb8f-4211-374f-a2e1-e5c0b7e88a2d