package org.ovirt.engine.core.bll.snapshots; import static org.ovirt.engine.core.bll.storage.disk.image.DisksFilter.ONLY_ACTIVE; import static org.ovirt.engine.core.bll.storage.disk.image.DisksFilter.ONLY_SNAPABLE; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; import org.apache.commons.lang.StringUtils; import org.ovirt.engine.core.bll.VmHandler; import org.ovirt.engine.core.bll.context.CompensationContext; import org.ovirt.engine.core.bll.memory.MemoryUtils; import org.ovirt.engine.core.bll.network.VmInterfaceManager; import org.ovirt.engine.core.bll.network.vm.VnicProfileHelper; import org.ovirt.engine.core.bll.storage.disk.image.DisksFilter; import org.ovirt.engine.core.bll.storage.disk.image.ImagesHandler; import org.ovirt.engine.core.bll.utils.ClusterUtils; import org.ovirt.engine.core.bll.utils.IconUtils; import org.ovirt.engine.core.bll.utils.VmDeviceUtils; import org.ovirt.engine.core.common.AuditLogType; import org.ovirt.engine.core.common.businessentities.Cluster; import org.ovirt.engine.core.common.businessentities.Quota; import org.ovirt.engine.core.common.businessentities.Snapshot; import org.ovirt.engine.core.common.businessentities.Snapshot.SnapshotStatus; import org.ovirt.engine.core.common.businessentities.Snapshot.SnapshotType; import org.ovirt.engine.core.common.businessentities.VM; import org.ovirt.engine.core.common.businessentities.VmDevice; import org.ovirt.engine.core.common.businessentities.VmDeviceGeneralType; import org.ovirt.engine.core.common.businessentities.VmDeviceId; import org.ovirt.engine.core.common.businessentities.VmStatic; import org.ovirt.engine.core.common.businessentities.VmTemplate; import org.ovirt.engine.core.common.businessentities.aaa.DbUser; import org.ovirt.engine.core.common.businessentities.network.VmNetworkInterface; import org.ovirt.engine.core.common.businessentities.storage.Disk; import org.ovirt.engine.core.common.businessentities.storage.DiskImage; import org.ovirt.engine.core.common.businessentities.storage.DiskVmElement; import org.ovirt.engine.core.common.businessentities.storage.ImageStatus; import org.ovirt.engine.core.common.businessentities.storage.ImageStorageDomainMap; import org.ovirt.engine.core.common.utils.VmDeviceType; import org.ovirt.engine.core.compat.Guid; import org.ovirt.engine.core.dao.BaseDiskDao; import org.ovirt.engine.core.dao.ClusterDao; import org.ovirt.engine.core.dao.DiskDao; import org.ovirt.engine.core.dao.DiskImageDao; import org.ovirt.engine.core.dao.DiskVmElementDao; import org.ovirt.engine.core.dao.QuotaDao; import org.ovirt.engine.core.dao.SnapshotDao; import org.ovirt.engine.core.dao.VmDeviceDao; import org.ovirt.engine.core.dao.VmDynamicDao; import org.ovirt.engine.core.dao.VmStaticDao; import org.ovirt.engine.core.dao.VmTemplateDao; import org.ovirt.engine.core.dao.network.VmNetworkInterfaceDao; import org.ovirt.engine.core.utils.ovf.OvfManager; import org.ovirt.engine.core.utils.ovf.OvfReaderException; import org.ovirt.engine.core.utils.ovf.VMStaticOvfLogHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The {@link Snapshot} manager is used to easily add/update/remove snapshots. */ @Singleton public class SnapshotsManager { private static final Logger log = LoggerFactory.getLogger(SnapshotsManager.class); @Inject private VmDeviceUtils vmDeviceUtils; @Inject private VmDeviceDao vmDeviceDao; @Inject private BaseDiskDao baseDiskDao; @Inject private SnapshotDao snapshotDao; @Inject private VmDynamicDao vmDynamicDao; @Inject private VmStaticDao vmStaticDao; @Inject private VmNetworkInterfaceDao vmNetworkInterfaceDao; @Inject private VmTemplateDao vmTemplateDao; @Inject private ClusterDao clusterDao; @Inject private DiskVmElementDao diskVmElementDao; @Inject private DiskDao diskDao; @Inject private DiskImageDao diskImageDao; @Inject private QuotaDao quotaDao; @Inject private VmHandler vmHandler; @Inject private OvfManager ovfManager; /** * Save an active snapshot for the VM, without saving the configuration.<br> * The snapshot is created in status {@link SnapshotStatus#OK} by default. * * @param snapshotId * The ID for the snapshot. * @param vm * The VM to save the snapshot for. * @param compensationContext * Context for saving compensation details. * @return the newly created snapshot * @see #addActiveSnapshot(Guid, VM, SnapshotStatus, CompensationContext) */ public Snapshot addActiveSnapshot(Guid snapshotId, VM vm, final CompensationContext compensationContext) { return addActiveSnapshot(snapshotId, vm, SnapshotStatus.OK, "", null, compensationContext); } /** * Save an active snapshot for the VM, without saving the configuration.<br> * The snapshot is created in status {@link SnapshotStatus#OK} by default. * * @param snapshotId * The ID for the snapshot. * @param vm * The VM to save the snapshot for. * @param snapshotStatus * The initial status of the created snapshot * @param compensationContext * Context for saving compensation details. * @return the newly created snapshot * @see #addActiveSnapshot(Guid, VM, SnapshotStatus, CompensationContext) */ public Snapshot addActiveSnapshot(Guid snapshotId, VM vm, SnapshotStatus snapshotStatus, final CompensationContext compensationContext) { return addActiveSnapshot(snapshotId, vm, snapshotStatus, "", null, compensationContext); } /** * Save an active snapshot for the VM, without saving the configuration.<br> * The snapshot is created in status {@link SnapshotStatus#OK} by default. * * @param snapshotId * The ID for the snapshot. * @param vm * The VM to save the snapshot for. * @param memoryVolume * The memory state for the created snapshot * @param compensationContext * Context for saving compensation details. * @return the newly created snapshot * @see #addActiveSnapshot(Guid, VM, SnapshotStatus, CompensationContext) */ public Snapshot addActiveSnapshot(Guid snapshotId, VM vm, String memoryVolume, final CompensationContext compensationContext) { return addActiveSnapshot(snapshotId, vm, SnapshotStatus.OK, memoryVolume, null, compensationContext); } /** * Save an active snapshot for the VM, without saving the configuration.<br> * The snapshot is created in status {@link SnapshotStatus#OK} by default. * * @param snapshotId * The ID for the snapshot. * @param vm * The VM to save the snapshot for. * @param memoryVolume * The memory state for the created snapshot * @param disks * The disks contained in the snapshot * @param compensationContext * Context for saving compensation details. * @return the newly created snapshot */ public Snapshot addActiveSnapshot(Guid snapshotId, VM vm, String memoryVolume, List<DiskImage> disks, final CompensationContext compensationContext) { return addActiveSnapshot(snapshotId, vm, SnapshotStatus.OK, memoryVolume, disks, compensationContext); } /** * Save an active snapshot for the VM, without saving the configuration.<br> * The snapshot is created with the given status {@link SnapshotStatus}. * * @param snapshotId * The ID for the snapshot. * @param vm * The VM to save the snapshot for. * @param snapshotStatus * The initial status of the snapshot * @param compensationContext * Context for saving compensation details. */ public Snapshot addActiveSnapshot(Guid snapshotId, VM vm, SnapshotStatus snapshotStatus, String memoryVolume, final CompensationContext compensationContext) { return addActiveSnapshot(snapshotId, vm, snapshotStatus, memoryVolume, null, compensationContext); } /** * Save an active snapshot for the VM, without saving the configuration.<br> * The snapshot is created with the given status {@link SnapshotStatus}. * * @param snapshotId * The ID for the snapshot. * @param vm * The VM to save the snapshot for. * @param snapshotStatus * The initial status of the snapshot * @param disks * The disks contained in the snapshot * @param compensationContext * Context for saving compensation details. */ public Snapshot addActiveSnapshot(Guid snapshotId, VM vm, SnapshotStatus snapshotStatus, String memoryVolume, List<DiskImage> disks, final CompensationContext compensationContext) { return addSnapshot(snapshotId, "Active VM", snapshotStatus, SnapshotType.ACTIVE, vm, false, memoryVolume, disks, compensationContext); } /** * Add a new snapshot, saving it to the DB (with compensation). The VM's current configuration (including Disks & * NICs) will be saved in the snapshot.<br> * The snapshot is created in status {@link SnapshotStatus#LOCKED} by default. * * @param snapshotId * The ID for the snapshot. * @param description * The snapshot description. * @param snapshotType * The snapshot type. * @param vm * The VM to save in configuration. * @param memoryVolume * the volume in which the snapshot's memory is stored * @param compensationContext * Context for saving compensation details. * @return the added snapshot */ public Snapshot addSnapshot(Guid snapshotId, String description, SnapshotType snapshotType, VM vm, String memoryVolume, final CompensationContext compensationContext) { return addSnapshot(snapshotId, description, SnapshotStatus.LOCKED, snapshotType, vm, true, memoryVolume, null, compensationContext); } /**addSnapshot * Save snapshot to DB with compensation data. * * @param snapshotId * The snapshot ID. * @param description * The snapshot description. * @param snapshotStatus * The snapshot status. * @param snapshotType * The snapshot type. * @param vm * The VM to link to & save configuration for (if necessary). * @param saveVmConfiguration * Should VM configuration be generated and saved? * @param compensationContext * In case compensation is needed. * @return the saved snapshot */ public Snapshot addSnapshot(Guid snapshotId, String description, SnapshotStatus snapshotStatus, SnapshotType snapshotType, VM vm, boolean saveVmConfiguration, String memoryVolume, List<DiskImage> disks, final CompensationContext compensationContext) { return addSnapshot(snapshotId, description, snapshotStatus, snapshotType, vm, saveVmConfiguration, memoryVolume, disks, null, compensationContext); } public Snapshot addSnapshot(Guid snapshotId, String description, SnapshotStatus snapshotStatus, SnapshotType snapshotType, VM vm, boolean saveVmConfiguration, String memoryVolume, List<DiskImage> disks, Map<Guid, VmDevice> vmDevices, final CompensationContext compensationContext) { final Snapshot snapshot = new Snapshot(snapshotId, snapshotStatus, vm.getId(), saveVmConfiguration ? generateVmConfiguration(vm, disks, vmDevices) : null, snapshotType, description, new Date(), vm.getAppList(), memoryVolume, MemoryUtils.getMemoryDiskId(memoryVolume), MemoryUtils.getMetadataDiskId(memoryVolume)); snapshotDao.save(snapshot); compensationContext.snapshotNewEntity(snapshot); return snapshot; } /** * Generate a string containing the given VM's configuration. * * @param vm * The VM to generate configuration from. * @return A String containing the VM configuration. */ protected String generateVmConfiguration(VM vm, List<DiskImage> disks, Map<Guid, VmDevice> vmDevices) { if (vm.getInterfaces() == null || vm.getInterfaces().isEmpty()) { vm.setInterfaces(vmNetworkInterfaceDao.getAllForVm(vm.getId())); } if (StringUtils.isEmpty(vm.getVmtName())) { VmTemplate t = vmTemplateDao.get(vm.getVmtGuid()); vm.setVmtName(t.getName()); } if (vmDevices == null) { vmDeviceUtils.setVmDevices(vm.getStaticData()); } else { vm.getStaticData().setManagedDeviceMap(vmDevices); } if (disks == null) { disks = DisksFilter.filterImageDisks(diskDao.getAllForVm(vm.getId()), ONLY_SNAPABLE, ONLY_ACTIVE); disks.addAll(ImagesHandler.getCinderLeafImages(diskDao.getAllForVm(vm.getId()))); } populateDisksWithVmData(disks, vm.getId()); for (DiskImage image : disks) { image.setStorageIds(null); } return ovfManager.exportVm(vm, new ArrayList<>(disks), ClusterUtils.getCompatibilityVersion(vm)); } private void populateDisksWithVmData(List<? extends Disk> disks, Guid vmId) { for (Disk disk : disks) { DiskVmElement dve = diskVmElementDao.get(new VmDeviceId(disk.getId(), vmId)); disk.setDiskVmElements(Collections.singletonList(dve)); } } /** * Remove all the snapshots that belong to the given VM. * * @param vmId * The ID of the VM. * @return Set of memoryVolumes of the removed snapshots */ public Set<String> removeSnapshots(Guid vmId) { final List<Snapshot> vmSnapshots = snapshotDao.getAll(vmId); for (Snapshot snapshot : vmSnapshots) { snapshotDao.remove(snapshot.getId()); } return MemoryUtils.getMemoryVolumesFromSnapshots(vmSnapshots); } /** * Remove all illegal disks which were associated with the given snapshot. This is done in order to be able to * switch correctly between snapshots where illegal images might be present. * * @param vmId * The vm ID the disk is associated with. * @param snapshotId * The ID of the snapshot for who to remove illegal images for. */ public void removeAllIllegalDisks(Guid snapshotId, Guid vmId) { for (DiskImage diskImage : diskImageDao.getAllSnapshotsForVmSnapshot(snapshotId)) { if (diskImage.getImageStatus() == ImageStatus.ILLEGAL) { ImagesHandler.removeDiskImage(diskImage, vmId); } } } /** * Attempt to read the configuration that is stored in the snapshot, and restore the VM from it.<br> * The NICs and Disks will be restored from the configuration (if available).<br> * <br> * <b>Note:</b>If the configuration is <code>null</code> or can't be decoded, then the VM configuration will remain * as it was but the underlying storage would still have changed.. * * @param snapshot * The snapshot containing the configuration. * @param user * The user that performs the action * @param vmInterfaceManager vmInterfaceManager instance */ public void attempToRestoreVmConfigurationFromSnapshot(VM vm, Snapshot snapshot, Guid activeSnapshotId, List<DiskImage> images, CompensationContext compensationContext, DbUser user, VmInterfaceManager vmInterfaceManager) { boolean vmUpdatedFromConfiguration = false; if (snapshot.getVmConfiguration() != null) { vmUpdatedFromConfiguration = updateVmFromConfiguration(vm, snapshot.getVmConfiguration()); if (images != null) { vmUpdatedFromConfiguration &= updateImagesByConfiguration(vm, images); } } if (!vmUpdatedFromConfiguration) { if (images == null) { images = diskImageDao.getAllSnapshotsForVmSnapshot(snapshot.getId()); } vm.setImages(new ArrayList<>(images)); } vm.setAppList(snapshot.getAppList()); vmDynamicDao.update(vm.getDynamicData()); synchronizeDisksFromSnapshot(vm.getId(), snapshot.getId(), activeSnapshotId, vm.getImages(), vm.getName()); if (vmUpdatedFromConfiguration) { vmStaticDao.update(vm.getStaticData()); synchronizeNics(vm, compensationContext, user, vmInterfaceManager); for (VmDevice vmDevice : vmDeviceDao.getVmDeviceByVmId(vm.getId())) { if (deviceCanBeRemoved(vmDevice)) { vmDeviceDao.remove(vmDevice.getId()); } } vmDeviceUtils.addImportedDevices(vm.getStaticData(), false); } } private boolean updateImagesByConfiguration(VM vm, List<DiskImage> images) { Map<Guid, VM> snapshotVmConfigurations = new HashMap<>(); ArrayList<DiskImage> imagesFromVmConf = new ArrayList<>(); for (DiskImage image : images) { Guid vmSnapshotId = image.getVmSnapshotId(); VM vmFromConf = snapshotVmConfigurations.get(vmSnapshotId); if (vmFromConf == null) { vmFromConf = new VM(); Snapshot snapshot = snapshotDao.get(image.getVmSnapshotId()); if (!updateVmFromConfiguration(vmFromConf, snapshot.getVmConfiguration())) { return false; } snapshotVmConfigurations.put(vmSnapshotId, vmFromConf); } for (DiskImage imageFromVmConf : vmFromConf.getImages()) { if (imageFromVmConf.getId().equals(image.getId())) { imageFromVmConf.setStorageIds(image.getStorageIds()); imagesFromVmConf.add(imageFromVmConf); break; } } } vm.setImages(imagesFromVmConf); return true; } /** * @return true if the device can be removed (disk which allows snapshot can be removed as it is part * of the snapshot. Other disks shouldn't be removed as they are not part of the snapshot). */ private boolean deviceCanBeRemoved(VmDevice vmDevice) { if (!vmDevice.getDevice().equals(VmDeviceType.DISK.getName()) || !vmDevice.isManaged()) { return true; } return vmDevice.getSnapshotId() == null && diskDao.get(vmDevice.getDeviceId()).isAllowSnapshot(); } /** * Update the given VM with the (static) data that is contained in the configuration. The {@link VM#getImages()} * will contain the images that were read from the configuration. * * @param vm * The VM to update. * @param configuration * The configuration to update from. * @return In case of a problem reading the configuration, <code>false</code>. Otherwise, <code>true</code>. */ public boolean updateVmFromConfiguration(VM vm, String configuration) { try { VmStatic oldVmStatic = vm.getStaticData(); VM tempVM = new VM(); ArrayList<DiskImage> images = new ArrayList<>(); ArrayList<VmNetworkInterface> interfaces = new ArrayList<>(); ovfManager.importVm(configuration, tempVM, images, interfaces); for (DiskImage diskImage : images) { DiskImage dbImage = diskImageDao.getSnapshotById(diskImage.getImageId()); if (dbImage != null) { diskImage.setStorageIds(dbImage.getStorageIds()); } } new VMStaticOvfLogHandler(tempVM.getStaticData()).resetDefaults(oldVmStatic); vm.setStaticData(tempVM.getStaticData()); IconUtils.preserveIcons(vm.getStaticData(), oldVmStatic); vm.setImages(images); vm.setInterfaces(interfaces); // These fields are not saved in the OVF, so get them from the current VM. vm.setIsoPath(oldVmStatic.getIsoPath()); vm.setCpuProfileId(oldVmStatic.getCpuProfileId()); vm.setClusterId(oldVmStatic.getClusterId()); // The VM configuration does not hold the vds group Id. // It is necessary to fetch the vm static from the Db, in order to get this information VmStatic vmStaticFromDb = vmStaticDao.get(vm.getId()); if (vmStaticFromDb != null) { Cluster cluster = clusterDao.get(vmStaticFromDb.getClusterId()); if (cluster != null) { vm.setStoragePoolId(cluster.getStoragePoolId()); vm.setClusterCompatibilityVersion(cluster.getCompatibilityVersion()); vm.setClusterName(cluster.getName()); vm.setClusterCpuName(cluster.getCpuName()); } } // if the required dedicated host is invalid -> use current VM dedicated host if (!vmHandler.validateDedicatedVdsExistOnSameCluster(vm.getStaticData(), null)) { vm.setDedicatedVmForVdsList(oldVmStatic.getDedicatedVmForVdsList()); } VmHandler.updateMaxMemorySize(vm.getStaticData(), vm.getCompatibilityVersion()); validateQuota(vm); return true; } catch (OvfReaderException e) { log.error("Failed to update VM from the configuration '{}': {}", configuration, e.getMessage()); log.debug("Exception", e); return false; } } /** * Validate whether the quota supplied in snapshot configuration exists in<br> * current setup, if not reset to null.<br> * * @param vm * imported vm */ private void validateQuota(VM vm) { if (vm.getQuotaId() != null) { Quota quota = quotaDao.getById(vm.getQuotaId()); if (quota == null) { vm.setQuotaId(null); } } } /** * Synchronize the VM's {@link VmNetworkInterface}s with the ones from the snapshot.<br> * All existing NICs will be deleted, and the ones from the snapshot re-added.<br> * In case a MAC address is already in use, the user will be issued a warning in the audit log. * * @param user * The user that performs the action * @param vmInterfaceManager vmInterfaceManager instance */ private void synchronizeNics(VM vm, CompensationContext compensationContext, DbUser user, VmInterfaceManager vmInterfaceManager) { VnicProfileHelper vnicProfileHelper = new VnicProfileHelper(vm.getClusterId(), vm.getStoragePoolId(), AuditLogType.IMPORTEXPORT_SNAPSHOT_VM_INVALID_INTERFACES); vmInterfaceManager.removeAll(vm.getId()); for (VmNetworkInterface vmInterface : vm.getInterfaces()) { vmInterface.setVmId(vm.getId()); // These fields might not be saved in the OVF, so fill them with reasonable values. if (vmInterface.getId() == null) { vmInterface.setId(Guid.newGuid()); } vnicProfileHelper.updateNicWithVnicProfileForUser(vmInterface, user); vmInterfaceManager.add(vmInterface, compensationContext, true, false, vm.getOs(), vm.getCompatibilityVersion()); } vnicProfileHelper.auditInvalidInterfaces(vm.getName()); } /** * Synchronize the VM's Disks with the images from the snapshot:<br> * <ul> * <li>Existing disks are updated.</li> * <li>Disks that don't exist anymore get re-added.</li> * <ul> * <li>If the image is still in the DB, the disk is linked to it.</li> * <li>If the image is not in the DB anymore, the disk will be marked as "broken"</li> * </ul> * </ul> * * @param vmId * The VM ID is needed to re-add disks. * @param snapshotId * The snapshot ID is used to find only the VM disks at the time. * @param disksFromSnapshot * The disks that existed in the snapshot. */ protected void synchronizeDisksFromSnapshot(Guid vmId, Guid snapshotId, Guid activeSnapshotId, List<DiskImage> disksFromSnapshot, String vmName) { List<Guid> diskIdsFromSnapshot = new ArrayList<>(); // Sync disks that exist or existed in the snapshot. int count = 1; for (DiskImage diskImage : disksFromSnapshot) { diskIdsFromSnapshot.add(diskImage.getId()); if (baseDiskDao.exists(diskImage.getId())) { baseDiskDao.update(diskImage); DiskVmElement dve = diskVmElementDao.get(diskImage.getDiskVmElementForVm(vmId).getId()); if (dve != null && !dve.equals(diskImage.getDiskVmElementForVm(vmId))) { diskVmElementDao.update(diskImage.getDiskVmElementForVm(vmId)); } } else { // If can't find the image, insert it as illegal so that it can't be used and make the device unplugged. if (diskImageDao.getSnapshotById(diskImage.getImageId()) == null) { diskImage.setImageStatus(ImageStatus.ILLEGAL); diskImage.setVmSnapshotId(activeSnapshotId); ImagesHandler.addImage(diskImage, true, (diskImage.getStorageIds() == null) ? null : new ImageStorageDomainMap(diskImage.getImageId(), diskImage.getStorageIds().get(0), diskImage.getQuotaId(), diskImage.getDiskProfileId())); } ImagesHandler.addDiskToVm(diskImage, vmId); } diskImage.setDiskAlias(ImagesHandler.getSuggestedDiskAlias(diskImage, vmName, count)); count++; } removeDisksNotInSnapshot(vmId, diskIdsFromSnapshot); } /** * Remove all the disks which are allowed to be snapshot but not exist in the snapshot and are not disk snapshots * @param vmId - The vm id which is being snapshot. * @param diskIdsFromSnapshot - An image group id list for images which are part of the VM. */ private void removeDisksNotInSnapshot(Guid vmId, List<Guid> diskIdsFromSnapshot) { for (VmDevice vmDevice : vmDeviceDao.getVmDeviceByVmIdTypeAndDevice( vmId, VmDeviceGeneralType.DISK, VmDeviceType.DISK.getName())) { if (!diskIdsFromSnapshot.contains(vmDevice.getDeviceId()) && vmDevice.getSnapshotId() == null) { Disk disk = diskDao.get(vmDevice.getDeviceId()); if (disk != null && disk.isAllowSnapshot()) { baseDiskDao.remove(vmDevice.getDeviceId()); vmDeviceDao.remove(vmDevice.getId()); } } } } }