package org.ovirt.engine.core.bll;
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_PLUGGED;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import javax.inject.Inject;
import org.apache.commons.lang.StringUtils;
import org.ovirt.engine.core.bll.context.CommandContext;
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.DisksFilter;
import org.ovirt.engine.core.bll.tasks.CommandCoordinatorUtil;
import org.ovirt.engine.core.bll.tasks.interfaces.CommandCallback;
import org.ovirt.engine.core.bll.validator.storage.DiskImagesValidator;
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.LockProperties;
import org.ovirt.engine.core.common.action.LockProperties.Scope;
import org.ovirt.engine.core.common.action.RemoveAllVmCinderDisksParameters;
import org.ovirt.engine.core.common.action.VdcActionType;
import org.ovirt.engine.core.common.action.VdcReturnValueBase;
import org.ovirt.engine.core.common.action.VmTemplateManagementParameters;
import org.ovirt.engine.core.common.asynctasks.EntityInfo;
import org.ovirt.engine.core.common.businessentities.VM;
import org.ovirt.engine.core.common.businessentities.VmEntityType;
import org.ovirt.engine.core.common.businessentities.VmTemplate;
import org.ovirt.engine.core.common.businessentities.storage.CinderDisk;
import org.ovirt.engine.core.common.businessentities.storage.Disk;
import org.ovirt.engine.core.common.businessentities.storage.DiskImage;
import org.ovirt.engine.core.common.errors.EngineMessage;
import org.ovirt.engine.core.common.locks.LockingGroup;
import org.ovirt.engine.core.common.utils.Pair;
import org.ovirt.engine.core.compat.Guid;
import org.ovirt.engine.core.dao.DiskDao;
import org.ovirt.engine.core.dao.VmDao;
import org.ovirt.engine.core.dao.VmIconDao;
import org.ovirt.engine.core.dao.VmTemplateDao;
import org.ovirt.engine.core.utils.collections.MultiValueMapUtils;
import org.ovirt.engine.core.utils.lock.EngineLock;
import org.ovirt.engine.core.utils.transaction.TransactionSupport;
@DisableInPrepareMode
@NonTransactiveCommandAttribute(forceCompensation = true)
public class RemoveVmTemplateCommand<T extends VmTemplateManagementParameters> extends VmTemplateManagementCommand<T>
implements QuotaStorageDependent {
@Inject
private VmTemplateDao vmTemplateDao;
@Inject
private DiskDao diskDao;
@Inject
private VmIconDao vmIconDao;
@Inject
private VmDao vmDao;
private List<DiskImage> imageTemplates;
private final Map<Guid, List<DiskImage>> storageToDisksMap = new HashMap<>();
/** used only while removing base-template */
private EngineLock baseTemplateSuccessorLock;
/** used only while removing base-template */
private VmTemplate baseTemplateSuccessor;
public RemoveVmTemplateCommand(T parameters, CommandContext cmdContext) {
super(parameters, cmdContext);
super.setVmTemplateId(parameters.getVmTemplateId());
parameters.setEntityInfo(new EntityInfo(VdcObjectType.VmTemplate, getVmTemplateId()));
}
public RemoveVmTemplateCommand(Guid commandId) {
super(commandId);
}
private void initStoragePoolInfo() {
if (getVmTemplate() != null) {
setStoragePoolId(getVmTemplate().getStoragePoolId());
}
}
@Override
public void init() {
initStoragePoolInfo();
getParameters().setUseCinderCommandCallback(!DisksFilter.filterCinderDisks(getImageTemplates()).isEmpty());
}
@Override
protected LockProperties applyLockProperties(LockProperties lockProperties) {
return lockProperties.withScope(Scope.Command);
}
@Override
protected void setActionMessageParameters() {
addValidationMessage(EngineMessage.VAR__ACTION__REMOVE);
addValidationMessage(EngineMessage.VAR__TYPE__VM_TEMPLATE);
}
@Override
protected boolean validate() {
Guid vmTemplateId = getVmTemplateId();
VmTemplate template = getVmTemplate();
if (!super.validate()) {
return false;
}
boolean isInstanceType = getVmTemplate().getTemplateType() == VmEntityType.INSTANCE_TYPE;
if (getCluster() == null && !isInstanceType) {
addValidationMessage(EngineMessage.ACTION_TYPE_FAILED_CLUSTER_CAN_NOT_BE_EMPTY);
return false;
}
// check template exists
if (!validate(templateExists())) {
return false;
}
// check not blank template
if (VmTemplateHandler.BLANK_VM_TEMPLATE_ID.equals(vmTemplateId)) {
return failValidation(EngineMessage.VMT_CANNOT_REMOVE_BLANK_TEMPLATE);
}
// check storage pool valid
if (!isInstanceType && !validate(new StoragePoolValidator(getStoragePool()).isUp())) {
return false;
}
// check if delete protected
if (template.isDeleteProtected()) {
return failValidation(EngineMessage.ACTION_TYPE_FAILED_DELETE_PROTECTION_ENABLED);
}
if (!isInstanceType) {
getImageTemplates();
}
// populate all the domains of the template
Set<Guid> allDomainsList = getStorageDomainsByDisks(imageTemplates, true);
getParameters().setStorageDomainsList(new ArrayList<>(allDomainsList));
// check template images for selected domains
for (Guid domainId : getParameters().getStorageDomainsList()) {
if (!validate(vmTemplateHandler.isVmTemplateImagesReady(getVmTemplate(),
domainId,
getParameters().isCheckDisksExists(),
true,
false,
true,
storageToDisksMap.get(domainId)))) {
return false;
}
}
// check no vms from this template on selected domains
List<VM> vms = vmDao.getAllWithTemplate(vmTemplateId);
List<String> problematicVmNames = new ArrayList<>();
for (VM vm : vms) {
problematicVmNames.add(vm.getName());
}
if (!problematicVmNames.isEmpty()) {
return failValidation(EngineMessage.VMT_CANNOT_REMOVE_DETECTED_DERIVED_VM,
String.format("$vmsList %1$s", StringUtils.join(problematicVmNames, ",")));
}
if (template.isBaseTemplate() && !tryLockSubVersionIfExists()) {
return false;
}
if (!isInstanceType && !validate(checkNoDisksBasedOnTemplateDisks())) {
return false;
}
return true;
}
/**
* It locks direct sub-template of deleted template if it exits.
* @return true if locking was successful or there is no direct sub-template, false otherwise
*/
private boolean tryLockSubVersionIfExists() {
final List<VmTemplate> templateSubVersions =
vmTemplateDao.getTemplateVersionsForBaseTemplate(getVmTemplateId());
if (templateSubVersions.isEmpty()) {
return true;
}
baseTemplateSuccessor = templateSubVersions
.stream()
.min(Comparator.comparing(VmTemplate::getTemplateVersionNumber))
.get();
if (!acquireBaseTemplateSuccessorLock()) {
return false;
}
if (vmTemplateDao.get(baseTemplateSuccessor.getId()) == null) {
return failValidation(EngineMessage.ACTION_TYPE_FAILED_SUBVERSION_BEING_CONCURRENTLY_REMOVED,
String.format("$subVersionId %s", baseTemplateSuccessor.getId().toString()));
}
return true;
}
/**
* To prevent concurrent deletion.
* @return first: true ~ successfully locked, false otherwise; second: fail reasons in form suitable for
* validationMessages
*/
private boolean acquireBaseTemplateSuccessorLock() {
final Map<String, Pair<String, String>> lockSharedMap = Collections.singletonMap(
baseTemplateSuccessor.getId().toString(),
LockMessagesMatchUtil.makeLockingPair(LockingGroup.TEMPLATE,
createSubTemplateLockMessage(baseTemplateSuccessor)));
baseTemplateSuccessorLock = new EngineLock(null, lockSharedMap);
final Pair<Boolean, Set<String>> isLockedAndFailReason = lockManager.acquireLock(baseTemplateSuccessorLock);
if (isLockedAndFailReason.getFirst()) {
return true;
}
baseTemplateSuccessorLock = null;
getReturnValue().getValidationMessages().addAll(extractVariableDeclarations(isLockedAndFailReason.getSecond()));
return false;
}
private String createSubTemplateLockMessage(VmTemplate template) {
return String.format("%s$templateName %s$templateId %s",
EngineMessage.ACTION_TYPE_FAILED_TEMPLATE_IS_BEING_SET_AS_BASE_TEMPLATE,
template.getName(),
template.getId());
}
private ValidationResult checkNoDisksBasedOnTemplateDisks() {
return new DiskImagesValidator(imageTemplates).diskImagesHaveNoDerivedDisks(null);
}
private List<DiskImage> getImageTemplates() {
if (imageTemplates == null) {
List<Disk> allImages = diskDao.getAllForVm(getVmTemplateId());
imageTemplates = DisksFilter.filterImageDisks(allImages, ONLY_ACTIVE);
imageTemplates.addAll(DisksFilter.filterCinderDisks(allImages, ONLY_PLUGGED));
}
return imageTemplates;
}
/**
* Get a list of all domains id that the template is on
*/
private Set<Guid> getStorageDomainsByDisks(List<DiskImage> disks, boolean isFillStorageTodDiskMap) {
Set<Guid> domainsList = new HashSet<>();
if (disks != null) {
for (DiskImage disk : disks) {
domainsList.addAll(disk.getStorageIds());
if (isFillStorageTodDiskMap) {
for (Guid storageDomainId : disk.getStorageIds()) {
MultiValueMapUtils.addToMap(storageDomainId, disk, storageToDisksMap);
}
}
}
}
return domainsList;
}
@Override
protected void executeCommand() {
if (getVmTemplate().isBaseTemplate()) {
shiftBaseTemplateToSuccessor();
}
List<Disk> templateImages = diskDao.getAllForVm(getVmTemplateId());
final List<CinderDisk> cinderDisks = DisksFilter.filterCinderDisks(templateImages);
final List<DiskImage> diskImages = DisksFilter.filterImageDisks(templateImages, ONLY_ACTIVE);
// Set VM to lock status immediately, for reducing race condition.
vmTemplateHandler.lockVmTemplateInTransaction(getVmTemplateId(), getCompensationContext());
if (!diskImages.isEmpty() || !cinderDisks.isEmpty()) {
TransactionSupport.executeInNewTransaction(() -> {
if (!diskImages.isEmpty() && removeVmTemplateImages()) {
vmHandler.removeVmInitFromDB(getVmTemplate());
setSucceeded(true);
}
if (!cinderDisks.isEmpty()) {
removeCinderDisks(cinderDisks);
setSucceeded(true);
}
return null;
});
}
// if for some reason template doesn't have images, remove it now and not in end action
if (noAsyncOperations()) {
handleEndAction();
}
}
private void shiftBaseTemplateToSuccessor() {
try {
vmTemplateDao.shiftBaseTemplate(getVmTemplateId());
} finally {
freeSubTemplateLock();
}
}
private void freeSubTemplateLock() {
if (baseTemplateSuccessorLock != null) {
lockManager.releaseLock(baseTemplateSuccessorLock);
baseTemplateSuccessorLock = null;
}
}
@Override
protected void freeCustomLocks() {
super.freeCustomLocks();
freeSubTemplateLock();
}
/**
* The following method performs a removing of all cinder disks from vm. These is only DB operation
*/
private void removeCinderDisks(List<CinderDisk> cinderDisks) {
RemoveAllVmCinderDisksParameters removeParam = new RemoveAllVmCinderDisksParameters(getVmTemplateId(), cinderDisks);
Future<VdcReturnValueBase> future =
CommandCoordinatorUtil.executeAsyncCommand(VdcActionType.RemoveAllVmCinderDisks,
withRootCommandInfo(removeParam),
cloneContextAndDetachFromParent());
try {
future.get().getActionReturnValue();
} catch (InterruptedException | ExecutionException e) {
log.error("Exception", e);
}
}
@Override
protected Map<String, Pair<String, String>> getExclusiveLocks() {
if (getVmTemplate() != null) {
return Collections.singletonMap(getVmTemplateId().toString(),
LockMessagesMatchUtil.makeLockingPair(LockingGroup.TEMPLATE,
new LockMessage(EngineMessage.ACTION_TYPE_FAILED_TEMPLATE_IS_BEING_REMOVED)
.with("templateName", getVmTemplate().getName())
.with("templateId", getVmTemplate().getId().toString())));
}
return null;
}
private void removeTemplateFromDb() {
removeNetwork();
vmTemplateDao.remove(getVmTemplate().getId());
vmIconDao.removeIfUnused(getVmTemplate().getSmallIconId());
vmIconDao.removeIfUnused(getVmTemplate().getLargeIconId());
}
protected boolean removeVmTemplateImages() {
getParameters().setEntityInfo(getParameters().getEntityInfo());
getParameters().setParentCommand(getActionType());
getParameters().setParentParameters(getParameters());
VdcReturnValueBase vdcReturnValue = runInternalActionWithTasksContext(
VdcActionType.RemoveAllVmTemplateImageTemplates,
getParameters());
if (!vdcReturnValue.getSucceeded()) {
setSucceeded(false);
getReturnValue().setFault(vdcReturnValue.getFault());
return false;
}
getReturnValue().getVdsmTaskIdList().addAll(vdcReturnValue.getInternalVdsmTaskIdList());
return true;
}
@Override
public AuditLogType getAuditLogTypeValue() {
switch (getActionState()) {
case EXECUTE:
return getSucceeded() ? AuditLogType.USER_REMOVE_VM_TEMPLATE : AuditLogType.USER_FAILED_REMOVE_VM_TEMPLATE;
case END_FAILURE:
case END_SUCCESS:
default:
return AuditLogType.USER_REMOVE_VM_TEMPLATE_FINISHED;
}
}
@Override
protected void endSuccessfully() {
handleEndAction();
}
@Override
protected void endWithFailure() {
handleEndAction();
}
private void handleEndAction() {
try {
removeTemplateFromDb();
setSucceeded(true);
} catch (RuntimeException e) {
// Set the try again of task to false, to prevent log spam and audit log spam.
getReturnValue().setEndActionTryAgain(false);
log.error("Encountered a problem removing template from DB, setting the action not to retry.");
}
}
@Override
public List<QuotaConsumptionParameter> getQuotaStorageConsumptionParameters() {
List<QuotaConsumptionParameter> list = new ArrayList<>();
getImageTemplates();
if (imageTemplates != null) {
for (DiskImage disk : imageTemplates) {
if (disk.getQuotaId() != null && !Guid.Empty.equals(disk.getQuotaId())) {
for (Guid storageId : disk.getStorageIds()) {
list.add(new QuotaStorageConsumptionParameter(
disk.getQuotaId(),
null,
QuotaStorageConsumptionParameter.QuotaAction.RELEASE,
storageId,
(double) disk.getSizeInGigabytes()));
}
}
}
}
return list;
}
@Override
public CommandCallback getCallback() {
return getParameters().isUseCinderCommandCallback() ? new ConcurrentChildCommandsExecutionCallback() : null;
}
}