package org.ovirt.engine.core.bll.validator.storage;
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;
import org.ovirt.engine.core.bll.ValidationResult;
import org.ovirt.engine.core.bll.storage.disk.image.ImagesHandler;
import org.ovirt.engine.core.bll.storage.utils.BlockStorageDiscardFunctionalityHelper;
import org.ovirt.engine.core.common.FeatureSupported;
import org.ovirt.engine.core.common.action.VdcActionType;
import org.ovirt.engine.core.common.businessentities.StorageDomain;
import org.ovirt.engine.core.common.businessentities.StorageDomainDynamic;
import org.ovirt.engine.core.common.businessentities.StorageDomainStatic;
import org.ovirt.engine.core.common.businessentities.StorageDomainStatus;
import org.ovirt.engine.core.common.businessentities.StorageDomainType;
import org.ovirt.engine.core.common.businessentities.StorageFormatType;
import org.ovirt.engine.core.common.businessentities.StoragePoolIsoMap;
import org.ovirt.engine.core.common.businessentities.SubchainInfo;
import org.ovirt.engine.core.common.businessentities.storage.DiskImage;
import org.ovirt.engine.core.common.businessentities.storage.LUNs;
import org.ovirt.engine.core.common.businessentities.storage.StorageType;
import org.ovirt.engine.core.common.businessentities.storage.VolumeFormat;
import org.ovirt.engine.core.common.businessentities.storage.VolumeType;
import org.ovirt.engine.core.common.constants.StorageConstants;
import org.ovirt.engine.core.common.errors.EngineMessage;
import org.ovirt.engine.core.compat.Guid;
import org.ovirt.engine.core.compat.Version;
import org.ovirt.engine.core.di.Injector;
public class StorageDomainValidator {
private static final long INITIAL_BLOCK_ALLOCATION_SIZE = 1024L * 1024L * 1024L;
private static final long EMPTY_QCOW_HEADER_SIZE = 1024L * 1024L;
private final StorageDomain storageDomain;
public StorageDomainValidator(StorageDomain domain) {
storageDomain = domain;
}
public ValidationResult isDomainExist() {
if (storageDomain == null) {
return new ValidationResult(EngineMessage.ACTION_TYPE_FAILED_STORAGE_DOMAIN_NOT_EXIST);
}
return ValidationResult.VALID;
}
public ValidationResult isDomainExistAndActive() {
ValidationResult domainExistValidation = isDomainExist();
if (!ValidationResult.VALID.equals(domainExistValidation)) {
return domainExistValidation;
}
if (storageDomain.getStatus() != StorageDomainStatus.Active) {
return new ValidationResult(EngineMessage.ACTION_TYPE_FAILED_STORAGE_DOMAIN_STATUS_ILLEGAL2,
String.format("$%1$s %2$s", "status", storageDomain.getStatus().name()));
}
return ValidationResult.VALID;
}
public ValidationResult domainIsValidDestination() {
if (storageDomain.getStorageDomainType().isIsoOrImportExportDomain()) {
return new ValidationResult(EngineMessage.ACTION_TYPE_FAILED_STORAGE_DOMAIN_TYPE_ILLEGAL);
}
return ValidationResult.VALID;
}
public ValidationResult isDomainWithinThresholds() {
if (storageDomain.getStorageType().isCinderDomain()) {
return ValidationResult.VALID;
}
StorageDomainDynamic dynamicData = storageDomain.getStorageDynamicData();
StorageDomainStatic staticData = storageDomain.getStorageStaticData();
if (dynamicData != null && staticData != null
&& dynamicData.getAvailableDiskSize() != null
&& staticData.getCriticalSpaceActionBlocker() != null
&& dynamicData.getAvailableDiskSize() < staticData.getCriticalSpaceActionBlocker()) {
return new ValidationResult(EngineMessage.ACTION_TYPE_FAILED_DISK_SPACE_LOW_ON_STORAGE_DOMAIN,
storageName());
}
return ValidationResult.VALID;
}
private String storageName() {
return String.format("$%1$s %2$s", "storageName", storageDomain.getStorageName());
}
/**
* Verify there's enough space in the storage domain for creating new DiskImages.
* Some space should be allocated on the storage domain according to the volumes type and format, and allocation policy,
* according to the following table:
*
* | File Domain | Block Domain
* -----|-----------------------------------------|-------------
* qcow | 1M (header size) | 1G
* -----|-----------------------------------------|-------------
* raw | preallocated: disk capacity (getSize()) | disk capacity
* | thin (sparse): 1M | (there is no raw sparse on
* | | block domains)
*
*/
private double getTotalSizeForNewDisks(Collection<DiskImage> diskImages) {
return getTotalSizeForDisksByMethod(diskImages, diskImage -> {
double sizeForDisk = diskImage.getSize();
if (diskImage.getVolumeFormat() == VolumeFormat.COW) {
if (storageDomain.getStorageType().isFileDomain()) {
sizeForDisk = EMPTY_QCOW_HEADER_SIZE;
} else {
sizeForDisk = INITIAL_BLOCK_ALLOCATION_SIZE;
}
} else if (diskImage.getVolumeType() == VolumeType.Sparse) {
sizeForDisk = EMPTY_QCOW_HEADER_SIZE;
}
return sizeForDisk;
});
}
/**
* Returns the required space in the storage domain for creating cloned DiskImages with collapse.
* */
private double getTotalSizeForClonedDisks(Collection<DiskImage> diskImages) {
return getTotalSizeForDisksByMethod(diskImages, this::getTotalSizeForClonedDisk);
}
/**
* Calculates the required space in the storage domain for creating cloned DiskImages with collapse.
* When creating COW volume the actual used space will be the needed space * QCOW_OVERHEAD_FACTOR as implemented
* currently in the VDSM code.
*
* */
private double getTotalSizeForClonedDisk(DiskImage diskImage) {
double sizeForDisk = ImagesHandler.getTotalActualSizeOfDisk(diskImage, storageDomain.getStorageStaticData());
if (diskImage.getVolumeFormat() == VolumeFormat.COW) {
sizeForDisk = Math.ceil(StorageConstants.QCOW_OVERHEAD_FACTOR * sizeForDisk);
}
return sizeForDisk;
}
/**
* Verify there's enough space in the storage domain for creating cloned DiskImages with snapshots without collapse.
* Space should be allocated according to the volumes type and format, and allocation policy,
* according to the following table:
*
* | File Domain | Block Domain
* -----|-----------------------------------------|----------------
* qcow | 1.1 * used space |1.1 * used space
* -----|-----------------------------------------|----------------
* raw | preallocated: disk capacity |disk capacity
* | sparse: used space |
*
* */
private double getTotalSizeForDisksWithSnapshots(Collection<DiskImage> diskImages) {
return getTotalSizeForDisksByMethod(diskImages, diskImage -> {
double sizeForDisk = diskImage.getSize();
if ((storageDomain.getStorageType().isFileDomain() && diskImage.getVolumeType() == VolumeType.Sparse)
|| diskImage.getVolumeFormat() == VolumeFormat.COW) {
sizeForDisk = diskImage.getActualDiskWithSnapshotsSizeInBytes();
}
if (diskImage.getVolumeFormat() == VolumeFormat.COW) {
sizeForDisk = Math.ceil(StorageConstants.QCOW_OVERHEAD_FACTOR * sizeForDisk);
}
return sizeForDisk;
});
}
/**
* Validate space for new, empty disks. Used for a new Active Image.
*/
public ValidationResult hasSpaceForNewDisks(Collection<DiskImage> diskImages) {
if (storageDomain.getStorageType().isCinderDomain()) {
return ValidationResult.VALID;
}
Long availableSize = storageDomain.getAvailableDiskSizeInBytes();
double totalSizeForDisks = getTotalSizeForNewDisks(diskImages);
return validateRequiredSpace(availableSize, totalSizeForDisks);
}
/**
* Validate space for a cloned disk with the collapse option.
*/
public ValidationResult hasSpaceForClonedDisks(Collection<DiskImage> diskImages) {
if (storageDomain.getStorageType().isCinderDomain()) {
return ValidationResult.VALID;
}
Long availableSize = storageDomain.getAvailableDiskSizeInBytes();
double totalSizeForDisks = getTotalSizeForClonedDisks(diskImages);
return validateRequiredSpace(availableSize, totalSizeForDisks);
}
public ValidationResult hasSpaceForMerge(SubchainInfo subchain, VdcActionType snapshotActionType) {
if (storageDomain.getStorageType().isCinderDomain()) {
return ValidationResult.VALID;
}
Long availableSize = storageDomain.getAvailableDiskSizeInBytes();
double totalSizeForDisks = getRequiredSizeForMerge(subchain, snapshotActionType);
return validateRequiredSpace(availableSize, totalSizeForDisks);
}
/**
* Validate space for cloned disks without the collapse option. Every snapshot will be cloned.
*/
public ValidationResult hasSpaceForDisksWithSnapshots(Collection<DiskImage> diskImages) {
if (storageDomain.getStorageType().isCinderDomain()) {
return ValidationResult.VALID;
}
Long availableSize = storageDomain.getAvailableDiskSizeInBytes();
double totalSizeForDisks = getTotalSizeForDisksWithSnapshots(diskImages);
return validateRequiredSpace(availableSize, totalSizeForDisks);
}
/**
* Validate space for new and cloned (with collapse) disks. When this option is needed a combined method should be
* used in order to check the space on the domain for the two types of validation, done here.
* Note that at this time there is no need for the same functionality for clone without collapse,
* so there's no method for this.
*/
public ValidationResult hasSpaceForAllDisks(Collection<DiskImage> newDiskImages, Collection<DiskImage> clonedDiskImages) {
if (storageDomain.getStorageType().isCinderDomain()) {
return ValidationResult.VALID;
}
Long availableSize = storageDomain.getAvailableDiskSizeInBytes();
double totalSizeForNewDisks = getTotalSizeForNewDisks(newDiskImages);
double totalSizeForClonedDisks = getTotalSizeForClonedDisks(clonedDiskImages);
double totalSizeForDisks = totalSizeForNewDisks + totalSizeForClonedDisks;
return validateRequiredSpace(availableSize, totalSizeForDisks);
}
/**
* Validate space for a cloned disk without the collapse option. Every snapshot will be cloned.
*/
public ValidationResult hasSpaceForDiskWithSnapshots(DiskImage diskImage) {
return hasSpaceForDisksWithSnapshots(Collections.singleton(diskImage));
}
/**
* Validate space for a cloned disk with the collapse option.
*/
public ValidationResult hasSpaceForClonedDisk(DiskImage diskImage) {
return hasSpaceForClonedDisks(Collections.singleton(diskImage));
}
/**
* Validate space for a new, empty disk. Used for a new Active Image.
*/
public ValidationResult hasSpaceForNewDisk(DiskImage diskImage) {
return hasSpaceForNewDisks(Collections.singleton(diskImage));
}
private ValidationResult validateRequiredSpace(Long availableSize, double requiredSize) {
// If availableSize is not yet set, we'll allow the operation.
if (availableSize == null || availableSize.doubleValue() >= requiredSize) {
return ValidationResult.VALID;
}
return new ValidationResult(EngineMessage.ACTION_TYPE_FAILED_DISK_SPACE_LOW_ON_STORAGE_DOMAIN,
storageName());
}
/**
* Validates all the storage domains by a given predicate.
*
* @return {@link ValidationResult#VALID} if all the domains are OK, or the
* first validation error if they aren't.
*/
private double getTotalSizeForDisksByMethod(Collection<DiskImage> diskImages, SizeAssessment sizeAssessment) {
double totalSizeForDisks = 0.0;
if (diskImages != null) {
for (DiskImage diskImage : diskImages) {
double sizeForDisk = sizeAssessment.getSizeForDisk(diskImage);
totalSizeForDisks += sizeForDisk;
}
}
return totalSizeForDisks;
}
/**
* Calculates the required space for snpashot merge (live and cold).
* The calculation is performed as follows:
*
*
* | File Domain | Block Domain
* -----|-----------------------------------------------------------------------|--------------
* qcow | min(virtual_size(top) * 1.1 - actual_size(base), actual_size(top)) | same as file
* -----|-----------------------------------------------------------------------|--------------
* raw | min(virtual_size(top) / 1.1, virtual_size(base) - actual_size(base)) | 0
* | |
*
* If the cold merge operation performed is pre-4.1,
* we return min(actual_size(top) + actual_size(base), virtual_size(top))
* * The qcow/raw refers to the base snapshot format
* * base/top - base snapshot/ top snapshot
* * 1.1 - qcow2 overhead
*
* @param subchain - The snapshot subchain, containing base and top snapshots
* @param snapshotActionType - Type of the merge operation (cold/live)
* @return required size for merge
*/
private double getRequiredSizeForMerge(SubchainInfo subchain, VdcActionType snapshotActionType) {
DiskImage baseSnapshot = subchain.getBaseImage();
DiskImage topSnapshot = subchain.getTopImage();
// We are doing a pre-4.1 cold merge, using block-rebase
if (snapshotActionType == VdcActionType.RemoveSnapshotSingleDisk) {
return Math.min(baseSnapshot.getActualSizeInBytes() + topSnapshot.getActualSizeInBytes(),
baseSnapshot.getSize()) * StorageConstants.QCOW_OVERHEAD_FACTOR;
}
VolumeType volumeType = snapshotActionType == VdcActionType.ColdMergeSnapshotSingleDisk ?
baseSnapshot.getVolumeType() :
topSnapshot.getVolumeType();
// The snapshot is the root snapshot
if (Guid.isNullOrEmpty(baseSnapshot.getParentId())) {
if (baseSnapshot.getVolumeFormat() == VolumeFormat.RAW) {
// Raw/Block can only be preallocated thus we are necessarily overlapping
// with existing data
if (volumeType == VolumeType.Preallocated) {
return 0.0;
}
return Math.min(topSnapshot.getActualSizeInBytes() / StorageConstants.QCOW_OVERHEAD_FACTOR,
baseSnapshot.getSize() - baseSnapshot.getActualSizeInBytes());
}
}
// The required size for the extension of the volume we merge into.
// If the actual size of top is larger than the actual size of base we
// will be overlapping, hence we extend by the lower of the two.
return Math.min(topSnapshot.getSize() * StorageConstants.QCOW_OVERHEAD_FACTOR - baseSnapshot.getActualSizeInBytes(),
topSnapshot.getActualSizeInBytes());
}
@FunctionalInterface
private static interface SizeAssessment {
public double getSizeForDisk(DiskImage diskImage);
}
public ValidationResult isInProcess() {
StoragePoolIsoMap domainIsoMap = storageDomain.getStoragePoolIsoMapData();
if (domainIsoMap.getStatus() != null && domainIsoMap.getStatus().isStorageDomainInProcess()) {
return new ValidationResult(EngineMessage.ACTION_TYPE_FAILED_STORAGE_DOMAIN_STATUS_ILLEGAL2,
String.format("$status %1$s", domainIsoMap.getStatus()));
}
return ValidationResult.VALID;
}
public ValidationResult isStorageFormatCompatibleWithDomain() {
StorageFormatType storageFormat = storageDomain.getStorageFormat();
StorageType storageType = storageDomain.getStorageType();
StorageDomainType storageDomainFunction = storageDomain.getStorageDomainType();
boolean validationSucceeded = true;
// V2 is applicable only for block data storage domains
if (storageFormat == StorageFormatType.V2) {
if ( !(storageDomainFunction.isDataDomain() && storageType.isBlockDomain()) ) {
validationSucceeded = false;
}
}
// Above V3 is applicable only for data storage domains
if (storageFormat.compareTo(StorageFormatType.V3) >= 0) {
if (!storageDomainFunction.isDataDomain()) {
validationSucceeded = false;
}
}
return validationSucceeded? ValidationResult.VALID : new ValidationResult(EngineMessage.ACTION_TYPE_FAILED_STORAGE_DOMAIN_FORMAT_ILLEGAL_HOST,
String.format("$storageFormat %1$s", storageDomain.getStorageFormat()));
}
public ValidationResult isDataDomain() {
if (storageDomain.getStorageDomainType().isDataDomain()) {
return ValidationResult.VALID;
}
return new ValidationResult(EngineMessage.ACTION_TYPE_FAILED_ACTION_IS_SUPPORTED_ONLY_FOR_DATA_DOMAINS);
}
public ValidationResult isDiscardAfterDeleteLegalForExistingStorageDomain() {
return isDiscardAfterDeleteLegal(this::discardAfterDeleteLegalForExistingStorageDomainPredicate);
}
protected Boolean discardAfterDeleteLegalForExistingStorageDomainPredicate() {
return Boolean.TRUE.equals(storageDomain.getSupportsDiscard());
}
public ValidationResult isDiscardAfterDeleteLegalForNewBlockStorageDomain(Collection<LUNs> luns) {
return isDiscardAfterDeleteLegal(getDiscardAfterDeleteLegalForNewBlockStorageDomainPredicate(luns));
}
protected Supplier<Boolean> getDiscardAfterDeleteLegalForNewBlockStorageDomainPredicate(Collection<LUNs> luns) {
return () -> Injector.get(BlockStorageDiscardFunctionalityHelper.class).allLunsSupportDiscard(luns);
}
protected ValidationResult isDiscardAfterDeleteLegal(Supplier<Boolean> supportsDiscardSupplier) {
if (!storageDomain.isDiscardAfterDelete()) {
return ValidationResult.VALID;
}
if (storageDomain.getStorageType().isBlockDomain()) {
if (supportsDiscardSupplier.get()) {
return ValidationResult.VALID;
}
return new ValidationResult(
EngineMessage.ACTION_TYPE_FAILED_DISCARD_AFTER_DELETE_NOT_SUPPORTED_BY_UNDERLYING_STORAGE,
String.format("$storageDomainName %s", storageDomain.getName()));
}
return new ValidationResult(
EngineMessage.ACTION_TYPE_FAILED_DISCARD_AFTER_DELETE_SUPPORTED_ONLY_BY_BLOCK_DOMAINS);
}
public ValidationResult isDiscardAfterDeleteSupportedByDcVersion(Version version) {
if (storageDomain.isDiscardAfterDelete() && !FeatureSupported.discardAfterDeleteSupported(version)) {
return new ValidationResult(
EngineMessage.ACTION_TYPE_FAILED_DISCARD_AFTER_DELETE_NOT_SUPPORTED_BY_DC_VERSION,
String.format("$dataCenterVersion %s", version.toString()));
}
return ValidationResult.VALID;
}
}