package org.ovirt.engine.core.bll; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import javax.inject.Inject; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang.StringUtils; import org.ovirt.engine.core.bll.context.CommandContext; import org.ovirt.engine.core.bll.job.ExecutionContext; import org.ovirt.engine.core.bll.network.macpool.MacPool; import org.ovirt.engine.core.bll.network.macpool.MacPoolPerCluster; import org.ovirt.engine.core.bll.profiles.CpuProfileHelper; import org.ovirt.engine.core.bll.profiles.DiskProfileHelper; import org.ovirt.engine.core.bll.quota.QuotaConsumptionParameter; import org.ovirt.engine.core.bll.quota.QuotaStorageConsumptionParameter; import org.ovirt.engine.core.bll.quota.QuotaStorageDependent; import org.ovirt.engine.core.bll.storage.disk.image.ImagesHandler; import org.ovirt.engine.core.bll.tasks.interfaces.CommandCallback; import org.ovirt.engine.core.bll.utils.IconUtils; import org.ovirt.engine.core.bll.utils.VmDeviceUtils; import org.ovirt.engine.core.bll.validator.storage.MultipleStorageDomainsValidator; import org.ovirt.engine.core.bll.validator.storage.StoragePoolValidator; import org.ovirt.engine.core.common.AuditLogType; import org.ovirt.engine.core.common.VdcObjectType; import org.ovirt.engine.core.common.action.AddVmParameters; import org.ovirt.engine.core.common.action.AddVmPoolParameters; import org.ovirt.engine.core.common.action.VdcActionParametersBase.EndProcedure; import org.ovirt.engine.core.common.action.VdcActionType; import org.ovirt.engine.core.common.action.VdcReturnValueBase; import org.ovirt.engine.core.common.businessentities.ArchitectureType; import org.ovirt.engine.core.common.businessentities.StorageDomain; import org.ovirt.engine.core.common.businessentities.StorageDomainType; import org.ovirt.engine.core.common.businessentities.VmBase; import org.ovirt.engine.core.common.businessentities.VmPool; import org.ovirt.engine.core.common.businessentities.VmRngDevice; import org.ovirt.engine.core.common.businessentities.VmStatic; import org.ovirt.engine.core.common.businessentities.VmTemplate; import org.ovirt.engine.core.common.businessentities.profiles.DiskProfile; import org.ovirt.engine.core.common.businessentities.storage.Disk; import org.ovirt.engine.core.common.businessentities.storage.DiskImage; import org.ovirt.engine.core.common.config.Config; import org.ovirt.engine.core.common.config.ConfigValues; import org.ovirt.engine.core.common.errors.EngineMessage; import org.ovirt.engine.core.common.job.Step; import org.ovirt.engine.core.common.job.StepEnum; import org.ovirt.engine.core.common.queries.VmIconIdSizePair; import org.ovirt.engine.core.common.utils.CompatibilityVersionUtils; import org.ovirt.engine.core.compat.Guid; import org.ovirt.engine.core.compat.Version; import org.ovirt.engine.core.dal.job.ExecutionMessageDirector; import org.ovirt.engine.core.dao.DiskDao; import org.ovirt.engine.core.dao.StorageDomainDao; import org.ovirt.engine.core.dao.VmPoolDao; import org.ovirt.engine.core.dao.VmTemplateDao; import org.ovirt.engine.core.dao.network.VmNicDao; import org.ovirt.engine.core.dao.profiles.DiskProfileDao; import org.ovirt.engine.core.utils.NameForVmInPoolGenerator; /** * This class is responsible for creation of a vmpool with vms within it. This class is not transactive, * which means that the 'execute' method does not run in transaction. On the other hand, each vm is added to the system * and attached to the vmpool in a transaction (one transaction for two operations). * To make this work, a Transaction is generated in the Execute function. Transactions are isolated, * which means that if one of vms is not added for some reason (image does not exists, etc) - it does not affect other * vms generation. Each vm is created with this format: {vm_name}_{number} where number runs from 1 to vms count. If one of vms to be created * already exists - the number is increased. For example if vm_8 exists - vm_9 will be created instead of it. */ public abstract class CommonVmPoolCommand<T extends AddVmPoolParameters> extends VmPoolCommandBase<T> implements QuotaStorageDependent { @Inject private MacPoolPerCluster macPoolPerCluster; @Inject private VmDeviceUtils vmDeviceUtils; @Inject private DiskProfileHelper diskProfileHelper; @Inject private CpuProfileHelper cpuProfileHelper; @Inject protected VmTemplateHandler vmTemplateHandler; @Inject private VmTemplateDao vmTemplateDao; @Inject private VmNicDao vmNicDao; @Inject private VmPoolDao vmPoolDao; @Inject private DiskDao diskDao; @Inject private StorageDomainDao storageDomainDao; @Inject private DiskProfileDao diskProfileDao; private HashMap<Guid, DiskImage> diskInfoDestinationMap; private Map<Guid, List<DiskImage>> storageToDisksMap; private Map<Guid, StorageDomain> destStorages = new HashMap<>(); private Map<Guid, Long> targetDomainsSize; private List<Disk> templateDisks; private Map<Guid, List<Guid>> diskToStorageIds; private Map<Guid, List<Guid>> diskToProfileMap; /** * This flag is set to true if all of the VMs were added successfully, false otherwise. */ private boolean allAddVmsSucceeded = true; /** * This flag is set to true if any of the VMs was added successfully, false otherwise. */ private boolean anyAddVmSucceeded = false; private NameForVmInPoolGenerator nameForVmInPoolGenerator; private Version effectiveCompatibilityVersion; private MacPool macPool; /** * Constructor for command creation when compensation is applied on startup */ protected CommonVmPoolCommand(Guid commandId) { super(commandId); } public CommonVmPoolCommand(T parameters, CommandContext commandContext) { super(parameters, commandContext); setVmPool(parameters.getVmPool()); setClusterId(getVmPool().getClusterId()); } /* * this method exist not to do caching, but to deal with init being called from constructor in class hierarchy. init * method is not called via Postconstruct, but from constructor, meaning, that in tests we're unable pro inject * 'macPoolPerCluster' soon enough. */ protected MacPool getMacPool() { if (macPool == null) { macPool = macPoolPerCluster.getMacPoolForCluster(getClusterId(), getContext()); } return macPool; } @Override protected void init() { if (getCluster() == null) { return; } setEffectiveCompatibilityVersion( CompatibilityVersionUtils.getEffective(getParameters().getVmStaticData(), this::getCluster)); Guid templateIdToUse = getParameters().getVmStaticData().getVmtGuid(); // if set to use latest version, get it from db and use it as template if (getParameters().getVmStaticData().isUseLatestVersion()) { VmTemplate latest = vmTemplateDao.getTemplateWithLatestVersionInChain(templateIdToUse); if (latest != null) { // if not using original template, need to override storage mappings // as it may have different set of disks if (!templateIdToUse.equals(latest.getId())) { getParameters().setDiskInfoDestinationMap(null); } setVmTemplate(latest); templateIdToUse = latest.getId(); getParameters().getVmStaticData().setVmtGuid(templateIdToUse); } } setVmTemplateId(templateIdToUse); initTemplate(); if (getVmPool().isAutoStorageSelect()) { initTargetDomains(); } nameForVmInPoolGenerator = new NameForVmInPoolGenerator(getParameters().getVmPool().getName()); } protected void initTemplate() { if (getVmTemplate() != null) { vmTemplateHandler.updateDisksFromDb(getVmTemplate()); } } protected abstract void createOrUpdateVmPool(); protected Version getEffectiveCompatibilityVersion() { return effectiveCompatibilityVersion; } protected void setEffectiveCompatibilityVersion(Version effectiveCompatibilityVersion) { this.effectiveCompatibilityVersion = effectiveCompatibilityVersion; } /** * This operation may take much time so the inner commands have fine-grained TX handling which * means they aim to make all calls to Vds commands (i.e VDSM calls) out of TX. */ @Override protected void executeCommand() { updateVmInitPassword(); vmHandler.warnMemorySizeLegal(getParameters().getVmStaticData(), getEffectiveCompatibilityVersion()); // Free exclusive VM_POOL lock, if taken. Further AddVmAndAttachToPool commands // require shared VM_POOL locks only. freeLock(); createOrUpdateVmPool(); setActionReturnValue(getVmPool().getVmPoolId()); vmTemplateHandler.lockVmTemplateInTransaction(getParameters().getVmStaticData().getVmtGuid(), getCompensationContext()); addVmsToPool(); getReturnValue().setValid(isAllAddVmsSucceeded()); setSucceeded(isAllAddVmsSucceeded()); vmTemplateHandler.unlockVmTemplate(getParameters().getVmStaticData().getVmtGuid()); if (!isAnyAddVmSucceeded()) { onNoVmsAdded(); } } private void addVmsToPool() { int subsequentFailedAttempts = 0; int vmPoolMaxSubsequentFailures = Config.<Integer> getValue(ConfigValues.VmPoolMaxSubsequentFailures); for (int i = 0; i < getParameters().getVmsCount(); i++) { String currentVmName = generateUniqueVmName(); VdcReturnValueBase returnValue = runInternalAction(VdcActionType.AddVm, buildAddVmParameters(currentVmName), createAddVmStepContext(currentVmName)); if (returnValue != null && !returnValue.getSucceeded() && !returnValue.getValidationMessages().isEmpty()) { for (String msg : returnValue.getValidationMessages()) { if (!getReturnValue().getValidationMessages().contains(msg)) { getReturnValue().getValidationMessages().add(msg); } } allAddVmsSucceeded = false; subsequentFailedAttempts++; } else { // Succeed on that, reset subsequentFailedAttempts. subsequentFailedAttempts = 0; anyAddVmSucceeded = true; } // if subsequent attempts failure exceeds configuration value , abort the loop. if (subsequentFailedAttempts == vmPoolMaxSubsequentFailures) { auditLogDirector.log(this, AuditLogType.USER_VM_POOL_MAX_SUBSEQUENT_FAILURES_REACHED); break; } } } protected void onNoVmsAdded() { } private String generateUniqueVmName() { String currentVmName; do { currentVmName = nameForVmInPoolGenerator.generateVmName(); } while (VmHandler.isVmWithSameNameExistStatic(currentVmName, getStoragePoolId())); return currentVmName; } private AddVmParameters buildAddVmParameters(String vmName) { VmStatic currVm = new VmStatic(getParameters().getVmStaticData()); currVm.setName(vmName); currVm.setStateless(!getVmPool().isStateful()); if (getParameters().getVmLargeIcon() != null) { final VmIconIdSizePair iconIds = IconUtils.ensureIconPairInDatabase(getParameters().getVmLargeIcon()); currVm.setSmallIconId(iconIds.getSmall()); currVm.setLargeIconId(iconIds.getLarge()); } AddVmParameters parameters = new AddVmParameters(currVm); parameters.setPoolId(getVmPool().getVmPoolId()); if (getVmPool().isAutoStorageSelect()) { parameters.setDiskInfoDestinationMap(autoSelectTargetDomain()); } else { parameters.setDiskInfoDestinationMap(diskInfoDestinationMap); } if (StringUtils.isEmpty(getParameters().getSessionId())) { parameters.setParametersCurrentUser(getCurrentUser()); } else { parameters.setSessionId(getParameters().getSessionId()); } parameters.setParentCommand(getActionType()); parameters.setParentParameters(getParameters()); // check if device is enabled or we need to override it to true parameters.setSoundDeviceEnabled(Boolean.TRUE.equals(getParameters().isSoundDeviceEnabled()) || vmDeviceUtils.shouldOverrideSoundDevice( getParameters().getVmStaticData(), getEffectiveCompatibilityVersion(), getParameters().isSoundDeviceEnabled())); parameters.setConsoleEnabled(getParameters().isConsoleEnabled()); parameters.setVirtioScsiEnabled(getParameters().isVirtioScsiEnabled()); parameters.setBalloonEnabled(getParameters().isBalloonEnabled()); parameters.setEndProcedure(EndProcedure.COMMAND_MANAGED); VmRngDevice rngDevice = getParameters().getRngDevice(); if (rngDevice != null) { parameters.setUpdateRngDevice(true); parameters.setRngDevice(rngDevice); } parameters.getGraphicsDevices().putAll(getParameters().getGraphicsDevices()); return parameters; } private void updateVmInitPassword() { // We are not passing the VmInit password to the UI, // so we need to update the VmInit password from its template. if (getParameters().getVmStaticData().getVmInit() != null && getParameters().getVmStaticData().getVmInit().isPasswordAlreadyStored()) { VmBase temp = new VmBase(); temp.setId(getParameters().getVmStaticData().getVmtGuid()); vmHandler.updateVmInitFromDB(temp, false); getParameters().getVmStaticData().getVmInit().setRootPassword(temp.getVmInit().getRootPassword()); } } private CommandContext createAddVmStepContext(String currentVmName) { CommandContext commandCtx = null; try { Map<String, String> values = new HashMap<>(); values.put(VdcObjectType.VM.name().toLowerCase(), currentVmName); Step addVmStep = executionHandler.addSubStep(getExecutionContext(), getExecutionContext().getJob().getStep(StepEnum.EXECUTING), StepEnum.ADD_VM_TO_POOL, ExecutionMessageDirector.resolveStepMessage(StepEnum.ADD_VM_TO_POOL, values)); ExecutionContext ctx = new ExecutionContext(); ctx.setStep(addVmStep); ctx.setMonitored(true); commandCtx = cloneContextAndDetachFromParent().withExecutionContext(ctx); } catch (RuntimeException e) { log.error("Failed to create command context of adding VM '{}' to Pool '{}': {}", currentVmName, getParameters().getVmPool().getName(), e.getMessage()); log.debug("Exception", e); } return commandCtx; } @Override protected void setActionMessageParameters() { addValidationMessage(EngineMessage.VAR__TYPE__DESKTOP_POOL); } @Override protected boolean validate() { if (getCluster() == null) { return failValidation(EngineMessage.VDS_CLUSTER_IS_NOT_VALID); } // A Pool cannot be added in a cluster without a defined architecture if (getCluster().getArchitecture() == ArchitectureType.undefined) { return failValidation(EngineMessage.ACTION_TYPE_FAILED_CLUSTER_UNDEFINED_ARCHITECTURE); } VmPool pool = vmPoolDao.getByName(getParameters().getVmPool().getName()); if (pool != null && (getActionType() == VdcActionType.AddVmPool || !pool.getVmPoolId().equals( getParameters().getVmPoolId()))) { return failValidation(EngineMessage.ACTION_TYPE_FAILED_NAME_ALREADY_USED); } setStoragePoolId(getCluster().getStoragePoolId()); if (!validate(new StoragePoolValidator(getStoragePool()).isUp())) { return false; } ensureDestinationImageMap(); // check if the selected template is compatible with Cluster architecture. if (!getVmTemplate().getId().equals(VmTemplateHandler.BLANK_VM_TEMPLATE_ID) && getCluster().getArchitecture() != getVmTemplate().getClusterArch()) { return failValidation(EngineMessage.ACTION_TYPE_FAILED_TEMPLATE_IS_INCOMPATIBLE); } if (!verifyAddVm()) { return false; } if (getVmTemplate().getDiskTemplateMap().values().size() != diskInfoDestinationMap.size()) { log.error("Can not found any default active domain for one of the disks of template with id '{}'", getVmTemplate().getId()); addValidationMessage(EngineMessage.ACTION_TYPE_FAILED_MISSED_STORAGES_FOR_SOME_DISKS); return false; } List<Guid> storageIds = new ArrayList<>(); for (DiskImage diskImage : diskInfoDestinationMap.values()) { Guid storageId = diskImage.getStorageIds().get(0); if (!storageIds.contains(storageId) && !areTemplateImagesInStorageReady(storageId)) { return false; } storageIds.add(storageId); } if (getActionType() == VdcActionType.AddVmPool && getParameters().getVmsCount() < 1) { return failValidation(EngineMessage.VM_POOL_CANNOT_CREATE_WITH_NO_VMS); } if (getParameters().getVmPool().getPrestartedVms() > getParameters().getVmPool().getAssignedVmsCount() + getParameters().getVmsCount()) { return failValidation(EngineMessage.ACTION_TYPE_FAILED_PRESTARTED_VMS_CANNOT_EXCEED_VMS_COUNT); } if (!setAndValidateDiskProfiles()) { return false; } if (!setAndValidateCpuProfile()) { return false; } return checkDestDomains(); } protected boolean verifyAddVm() { final List<String> reasons = getReturnValue().getValidationMessages(); final int nicsCount = getParameters().getVmsCount() * vmNicDao.getAllForTemplate(getVmTemplateId()).size(); final int priority = getParameters().getVmStaticData().getPriority(); return VmHandler.verifyAddVm(reasons, nicsCount, priority, getMacPool()); } protected boolean areTemplateImagesInStorageReady(Guid storageId) { return validate(vmTemplateHandler.isVmTemplateImagesReady(getVmTemplate(), storageId, false, true, true, destStorages.isEmpty(), storageToDisksMap.get(storageId))); } private Guid findAvailableStorageDomain(long diskSize, List<Guid> storageIds) { Guid dest = storageIds.get(0); for (Guid storageId: storageIds) { if (targetDomainsSize.get(storageId) > targetDomainsSize.get(dest)) { dest = storageId; } } long destSize = targetDomainsSize.get(dest); targetDomainsSize.put(dest, destSize - diskSize); return dest; } private void initTargetDomains() { templateDisks = diskDao.getAllForVm(getParameters().getVmStaticData().getVmtGuid()); targetDomainsSize = new HashMap<>(); diskToProfileMap = new HashMap<>(); diskToStorageIds = new HashMap<>(); for (Disk disk: templateDisks) { DiskImage diskImage = (DiskImage)disk; diskToProfileMap.put(disk.getId(), diskImage.getDiskProfileIds()); diskToStorageIds.put(disk.getId(), diskImage.getStorageIds()); for (Guid storageId: diskImage.getStorageIds()) { if (!targetDomainsSize.containsKey(storageId)) { StorageDomain domain = storageDomainDao.get(storageId); targetDomainsSize.put(domain.getId(), domain.getAvailableDiskSizeInBytes()); } } } } private HashMap<Guid, DiskImage> autoSelectTargetDomain() { HashMap<Guid, DiskImage> destinationMap = new HashMap<>(); for (Disk disk: templateDisks) { DiskImage diskImage = (DiskImage)disk; ArrayList<Guid> storageIds = new ArrayList<>(); Guid storageId = findAvailableStorageDomain(disk.getSize(), diskToStorageIds.get(disk.getId())); storageIds.add(storageId); List<Guid> profileIds = diskToProfileMap.get(disk.getId()); for (Guid profileId: profileIds) { DiskProfile profile = diskProfileDao.get(profileId); if (profile.getStorageDomainId().equals(storageId)) { diskImage.setDiskProfileId(profile.getId()); break; } } diskImage.setStorageIds(storageIds); destinationMap.put(disk.getId(), diskImage); } return destinationMap; } protected void ensureDestinationImageMap() { if (getVmPool().isAutoStorageSelect() || MapUtils.isEmpty(getParameters().getDiskInfoDestinationMap())) { diskInfoDestinationMap = new HashMap<>(); if (getVmTemplate() == null) { return; } if (!Guid.isNullOrEmpty(getParameters().getStorageDomainId()) && !getVmPool().isAutoStorageSelect()) { Guid storageId = getParameters().getStorageDomainId(); ArrayList<Guid> storageIds = new ArrayList<>(); storageIds.add(storageId); for (DiskImage image : getVmTemplate().getDiskTemplateMap().values()) { image.setStorageIds(storageIds); diskInfoDestinationMap.put(image.getId(), image); } } else { ImagesHandler.fillImagesMapBasedOnTemplate(getVmTemplate(), diskInfoDestinationMap, destStorages); } } else { diskInfoDestinationMap = getParameters().getDiskInfoDestinationMap(); } storageToDisksMap = ImagesHandler.buildStorageToDiskMap(getVmTemplate().getDiskTemplateMap().values(), diskInfoDestinationMap); } protected boolean checkDestDomains() { List<Guid> validDomains = new ArrayList<>(); for (DiskImage diskImage : diskInfoDestinationMap.values()) { Guid domainId = diskImage.getStorageIds().get(0); if (validDomains.contains(domainId)) { continue; } StorageDomain domain = destStorages.get(domainId); if (domain == null) { domain = this.storageDomainDao.getForStoragePool(domainId, getVmTemplate().getStoragePoolId()); destStorages.put(domainId, domain); } if (storageToDisksMap.containsKey(domainId)) { int numOfDisksOnDomain = storageToDisksMap.get(domainId).size(); if (numOfDisksOnDomain > 0 && (domain.getStorageDomainType() == StorageDomainType.ImportExport)) { return failValidation(EngineMessage.ACTION_TYPE_FAILED_STORAGE_DOMAIN_TYPE_ILLEGAL); } } validDomains.add(domainId); } return validateSpaceRequirements(); } private boolean validateSpaceRequirements() { int numOfVms = getParameters().getVmsCount(); Collection<DiskImage> diskDummies = ImagesHandler.getDisksDummiesForStorageAllocations(diskInfoDestinationMap.values()); Collection<DiskImage> disks = new ArrayList<>(numOfVms * diskDummies.size()); // Number of added disks multiplies by the vms number for (int i = 0; i < numOfVms; ++i) { disks.addAll(diskDummies); } Guid spId = getVmTemplate().getStoragePoolId(); Set<Guid> sdIds = destStorages.keySet(); MultipleStorageDomainsValidator storageDomainsValidator = getStorageDomainsValidator(spId, sdIds); return validate(storageDomainsValidator.allDomainsWithinThresholds()) && validate(storageDomainsValidator.allDomainsHaveSpaceForNewDisks(disks)); } protected boolean isAllAddVmsSucceeded() { return allAddVmsSucceeded; } private boolean isAnyAddVmSucceeded() { return anyAddVmSucceeded; } protected boolean setAndValidateDiskProfiles() { if (diskInfoDestinationMap != null && !diskInfoDestinationMap.isEmpty()) { Map<DiskImage, Guid> map = new HashMap<>(); for (DiskImage diskImage : diskInfoDestinationMap.values()) { map.put(diskImage, diskImage.getStorageIds().get(0)); } return validate(diskProfileHelper.setAndValidateDiskProfiles(map, getCurrentUser())); } return true; } protected boolean setAndValidateCpuProfile() { return validate(cpuProfileHelper.setAndValidateCpuProfile( getParameters().getVmStaticData(), getUserIdIfExternal().orElse(null))); } @Override public List<QuotaConsumptionParameter> getQuotaStorageConsumptionParameters() { return diskInfoDestinationMap.values().stream() .map(disk -> new QuotaStorageConsumptionParameter( disk.getQuotaId(), null, QuotaConsumptionParameter.QuotaAction.CONSUME, disk.getStorageIds().get(0), (double)(disk.getSizeInGigabytes() * getParameters().getVmsCount()))) .collect(Collectors.toList()); } protected MultipleStorageDomainsValidator getStorageDomainsValidator(Guid spId, Set<Guid> sdIds) { return new MultipleStorageDomainsValidator(spId, sdIds); } @Override public CommandCallback getCallback() { return new ConcurrentChildCommandsExecutionCallback(); } /** * Required for the audit logging */ public String getVmsCount() { return String.valueOf(getParameters().getVmsCount()); } }