package org.ovirt.engine.core.bll.storage.ovfstore;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import org.apache.commons.lang.StringUtils;
import org.ovirt.engine.core.bll.CommandActionState;
import org.ovirt.engine.core.bll.LockMessagesMatchUtil;
import org.ovirt.engine.core.bll.NonTransactiveCommandAttribute;
import org.ovirt.engine.core.bll.SerialChildCommandsExecutionCallback;
import org.ovirt.engine.core.bll.SerialChildExecutingCommand;
import org.ovirt.engine.core.bll.UploadStreamParameters;
import org.ovirt.engine.core.bll.context.CommandContext;
import org.ovirt.engine.core.bll.storage.domain.StorageDomainCommandBase;
import org.ovirt.engine.core.bll.tasks.interfaces.CommandCallback;
import org.ovirt.engine.core.bll.validator.storage.StorageDomainValidator;
import org.ovirt.engine.core.common.AuditLogType;
import org.ovirt.engine.core.common.action.CreateOvfVolumeForStorageDomainCommandParameters;
import org.ovirt.engine.core.common.action.LockProperties;
import org.ovirt.engine.core.common.action.LockProperties.Scope;
import org.ovirt.engine.core.common.action.ProcessOvfUpdateForStorageDomainCommandParameters;
import org.ovirt.engine.core.common.action.ProcessOvfUpdateForStorageDomainCommandParameters.OvfUpdateStep;
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.OvfEntityData;
import org.ovirt.engine.core.common.businessentities.StorageDomainOvfInfo;
import org.ovirt.engine.core.common.businessentities.StorageDomainOvfInfoStatus;
import org.ovirt.engine.core.common.businessentities.StoragePool;
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.constants.StorageConstants;
import org.ovirt.engine.core.common.errors.EngineException;
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.common.vdscommands.SetVolumeDescriptionVDSCommandParameters;
import org.ovirt.engine.core.common.vdscommands.VDSCommandType;
import org.ovirt.engine.core.compat.Guid;
import org.ovirt.engine.core.dao.DiskDao;
import org.ovirt.engine.core.dao.StorageDomainDao;
import org.ovirt.engine.core.dao.StorageDomainOvfInfoDao;
import org.ovirt.engine.core.dao.StoragePoolDao;
import org.ovirt.engine.core.dao.UnregisteredOVFDataDao;
import org.ovirt.engine.core.dao.VmAndTemplatesGenerationsDao;
import org.ovirt.engine.core.dao.VmStaticDao;
import org.ovirt.engine.core.utils.JsonHelper;
import org.ovirt.engine.core.utils.archivers.tar.InMemoryTar;
import org.ovirt.engine.core.utils.ovf.OvfInfoFileConstants;
@NonTransactiveCommandAttribute
public class ProcessOvfUpdateForStorageDomainCommand<T extends ProcessOvfUpdateForStorageDomainCommandParameters> extends StorageDomainCommandBase<T> implements SerialChildExecutingCommand {
@Inject
private VmAndTemplatesGenerationsDao vmAndTemplatesGenerationsDao;
@Inject
private DiskDao diskDao;
@Inject
private VmStaticDao vmStaticDao;
@Inject
private StoragePoolDao storagePoolDao;
@Inject
private StorageDomainOvfInfoDao storageDomainOvfInfoDao;
@Inject
private UnregisteredOVFDataDao unregisteredOVFDataDao;
@Inject
private StorageDomainDao storageDomainDao;
private LinkedList<Pair<StorageDomainOvfInfo, DiskImage>> domainOvfStoresInfoForUpdate = new LinkedList<>();
private int ovfDiskCount;
private String postUpdateDescription;
private Date updateDate;
private List<Guid> failedOvfDisks;
public ProcessOvfUpdateForStorageDomainCommand(T parameters, CommandContext commandContext) {
super(parameters, commandContext);
}
public ProcessOvfUpdateForStorageDomainCommand(Guid commandId) {
super(commandId);
}
@Override
public void init() {
super.init();
setStorageDomainId(getParameters().getStorageDomainId());
Guid poolId = getParameters().getStoragePoolId();
Guid storageDomainId = getParameters().getStorageDomainId();
if (poolId == null || poolId.equals(Guid.Empty)) {
List<StoragePool> pool = storagePoolDao.getAllForStorageDomain(storageDomainId);
if (!pool.isEmpty()) {
getParameters().setStoragePoolId(pool.get(0).getId());
}
}
setStoragePoolId(getParameters().getStoragePoolId());
populateStorageDomainOvfData();
}
@Override
protected LockProperties applyLockProperties(LockProperties lockProperties) {
return lockProperties.withScope(Scope.Execution);
}
@Override
public CommandCallback getCallback() {
return new SerialChildCommandsExecutionCallback();
}
@Override
protected boolean validate() {
if (!getParameters().isSkipDomainChecks()) {
StorageDomainValidator storageDomainValidator = new StorageDomainValidator(getStorageDomain());
return validate(storageDomainValidator.isDomainExistAndActive()) &&
validate(storageDomainValidator.isDataDomain());
}
return true;
}
private void populateStorageDomainOvfData() {
List<StorageDomainOvfInfo> storageDomainOvfInfos =
storageDomainOvfInfoDao.getAllForDomain(getStorageDomainId());
ovfDiskCount = storageDomainOvfInfos.size();
Collections.sort(storageDomainOvfInfos, OVF_INFO_COMPARATOR);
for (StorageDomainOvfInfo storageDomainOvfInfo : storageDomainOvfInfos) {
if (storageDomainOvfInfo.getStatus() != StorageDomainOvfInfoStatus.DISABLED) {
DiskImage ovfDisk = (DiskImage) diskDao.get(storageDomainOvfInfo.getOvfDiskId());
domainOvfStoresInfoForUpdate.add(new Pair<>(storageDomainOvfInfo, ovfDisk));
}
}
}
private String getPostUpdateOvfStoreDescription(long size) {
if (postUpdateDescription == null) {
postUpdateDescription = generateOvfStoreDescription(updateDate, true, size);
}
return postUpdateDescription;
}
private String generateOvfStoreDescription(Date updateDate, boolean isUpdated, Long size) {
Map<String, Object> description = new HashMap<>();
description.put(OvfInfoFileConstants.DiskDescription, OvfInfoFileConstants.OvfStoreDescriptionLabel);
description.put(OvfInfoFileConstants.Domains, Collections.singletonList(getParameters().getStorageDomainId()));
description.put(OvfInfoFileConstants.IsUpdated, isUpdated);
description.put(OvfInfoFileConstants.LastUpdated, updateDate != null ? updateDate.toString() : null);
if (size != null) {
description.put(OvfInfoFileConstants.Size, size);
}
return buildJson(description, false);
}
private String generateInfoFileData() {
Map<String, Object> data = new HashMap<>();
data.put(OvfInfoFileConstants.LastUpdated, updateDate.toString());
data.put(OvfInfoFileConstants.Domains, Collections.singletonList(getParameters().getStorageDomainId()));
return buildJson(data, true);
}
private String buildJson(Map<String, Object> map, boolean prettyPrint) {
try {
return JsonHelper.mapToJson(map, prettyPrint);
} catch (IOException e) {
throw new RuntimeException("Exception while generating json containing ovf store info", e);
}
}
private byte[] buildOvfInfoFileByteArray(List<Guid> vmAndTemplatesIds) {
ByteArrayOutputStream bufferedOutputStream = new ByteArrayOutputStream();
Set<Guid> processedIds = new HashSet<>();
try (InMemoryTar inMemoryTar = new InMemoryTar(bufferedOutputStream)) {
inMemoryTar.addTarEntry(generateInfoFileData().getBytes(),
"info.json");
int i = 0;
while (i < vmAndTemplatesIds.size()) {
int size =
Math.min(StorageConstants.OVF_MAX_ITEMS_PER_SQL_STATEMENT, vmAndTemplatesIds.size() - i);
List<Guid> idsToProcess = vmAndTemplatesIds.subList(i, i + size);
i += size;
List<Pair<Guid, String>> ovfs = vmAndTemplatesGenerationsDao.loadOvfDataForIds(idsToProcess);
if (!ovfs.isEmpty()) {
processedIds.addAll(buildFilesForOvfs(ovfs, inMemoryTar));
}
}
List<Pair<Guid, String>> unprocessedOvfData = retrieveUnprocessedUnregisteredOvfData(processedIds);
buildFilesForOvfs(unprocessedOvfData, inMemoryTar);
} catch (Exception e) {
throw new RuntimeException(String.format("Exception while building in memory tar of the OVFs of domain %s",
getParameters().getStorageDomainId()), e);
}
return bufferedOutputStream.toByteArray();
}
private List<Pair<Guid, String>> retrieveUnprocessedUnregisteredOvfData(Set<Guid> processedIds) {
List<OvfEntityData> ovfList = unregisteredOVFDataDao.getAllForStorageDomainByEntityType(
getParameters().getStorageDomainId(), null);
List<Pair<Guid, String>> ovfData = new LinkedList<>();
for (OvfEntityData ovfEntityData : ovfList) {
if (!processedIds.contains(ovfEntityData.getEntityId())) {
Pair<Guid, String> data = new Pair<>(ovfEntityData.getEntityId(), ovfEntityData.getOvfData());
ovfData.add(data);
}
}
return ovfData;
}
protected void updateOvfStoreContent() {
if (domainOvfStoresInfoForUpdate.isEmpty()) {
return;
}
updateDate = new Date();
List<Guid> vmAndTemplatesIds =
storageDomainDao.getVmAndTemplatesIdsByStorageDomainId(getParameters().getStorageDomainId(),
false,
false);
vmAndTemplatesIds.addAll(vmStaticDao.getVmAndTemplatesIdsWithoutAttachedImageDisks(getParameters().getStoragePoolId(), false));
byte[] bytes = buildOvfInfoFileByteArray(vmAndTemplatesIds);
Pair<StorageDomainOvfInfo, DiskImage> lastOvfStoreForUpdate = domainOvfStoresInfoForUpdate.getLast();
// means that the last ovf store was never updated, if it was - we don't want to update
// it within the loop unless some other ovf store was updated successfully (we use it as best effort backup so
// we'll
// possibly have some ovf data on storage)
if (lastOvfStoreForUpdate.getFirst().getLastUpdated() != null) {
domainOvfStoresInfoForUpdate.removeLast();
} else {
lastOvfStoreForUpdate = null;
}
boolean shouldUpdateLastOvfStore = false;
failedOvfDisks = new ArrayList<>();
for (Pair<StorageDomainOvfInfo, DiskImage> pair : domainOvfStoresInfoForUpdate) {
shouldUpdateLastOvfStore |=
performOvfUpdateForDomain(bytes,
pair.getFirst(),
pair.getSecond(),
vmAndTemplatesIds);
}
// if we successfully updated any ovf store, we can attempt to also update the one we kept for best effort
// backup (if we did)
if (shouldUpdateLastOvfStore && lastOvfStoreForUpdate != null) {
performOvfUpdateForDomain(bytes,
lastOvfStoreForUpdate.getFirst(),
lastOvfStoreForUpdate.getSecond(),
vmAndTemplatesIds);
}
if (!failedOvfDisks.isEmpty()) {
addCustomValue("DataCenterName", getStoragePool().getName());
addCustomValue("StorageDomainName", getStorageDomain().getName());
addCustomValue("DisksIds", StringUtils.join(failedOvfDisks, ", "));
auditLogDirector.log(this, AuditLogType.UPDATE_FOR_OVF_STORES_FAILED);
}
}
@Override
protected void setActionMessageParameters() {
super.setActionMessageParameters();
addValidationMessage(EngineMessage.VAR__ACTION__UPDATE_OVFS);
}
private void setOvfVolumeDescription(Guid storagePoolId,
Guid storageDomainId,
Guid diskId,
Guid volumeId,
String description) {
SetVolumeDescriptionVDSCommandParameters vdsCommandParameters =
new SetVolumeDescriptionVDSCommandParameters(storagePoolId, storageDomainId,
diskId, volumeId, description);
runVdsCommand(VDSCommandType.SetVolumeDescription, vdsCommandParameters);
}
private boolean performOvfUpdateForDomain(byte[] ovfData,
StorageDomainOvfInfo storageDomainOvfInfo,
DiskImage ovfDisk,
List<Guid> vmAndTemplatesIds) {
Guid storagePoolId = ovfDisk.getStoragePoolId();
Guid storageDomainId = ovfDisk.getStorageIds().get(0);
Guid diskId = ovfDisk.getId();
Guid volumeId = ovfDisk.getImageId();
storageDomainOvfInfo.setStoredOvfIds(null);
try {
setOvfVolumeDescription(storagePoolId,
storageDomainId,
diskId,
volumeId,
generateOvfStoreDescription(storageDomainOvfInfo.getLastUpdated(), false, null));
storageDomainOvfInfoDao.update(storageDomainOvfInfo);
ByteArrayInputStream byteArrayInputStream =
new ByteArrayInputStream(ovfData);
Long size = Long.valueOf(ovfData.length);
UploadStreamParameters uploadStreamParameters =
new UploadStreamParameters(storagePoolId, storageDomainId,
diskId, volumeId, byteArrayInputStream,
size);
uploadStreamParameters.setParentCommand(getActionType());
uploadStreamParameters.setParentParameters(getParameters());
uploadStreamParameters.setEndProcedure(EndProcedure.COMMAND_MANAGED);
VdcReturnValueBase vdcReturnValueBase =
runInternalActionWithTasksContext(VdcActionType.UploadStream, uploadStreamParameters);
if (vdcReturnValueBase.getSucceeded()) {
storageDomainOvfInfo.setStatus(StorageDomainOvfInfoStatus.UPDATED);
storageDomainOvfInfo.setStoredOvfIds(vmAndTemplatesIds);
storageDomainOvfInfo.setLastUpdated(updateDate);
setOvfVolumeDescription(storagePoolId, storageDomainId,
diskId, volumeId, getPostUpdateOvfStoreDescription(size));
storageDomainOvfInfoDao.update(storageDomainOvfInfo);
return true;
}
} catch (EngineException e) {
log.warn("failed to update domain '{}' ovf store disk '{}'", storageDomainId, diskId);
}
failedOvfDisks.add(diskId);
return false;
}
private int getMissingDiskCount() {
return Config.<Integer>getValue(ConfigValues.StorageDomainOvfStoreCount) - ovfDiskCount;
}
public void createOvfStoreDisks(int missingDiskCount) {
for (int i = 0; i < missingDiskCount; i++) {
CreateOvfVolumeForStorageDomainCommandParameters parameters = createCreateOvfVolumeForStorageDomainParams();
runInternalAction(VdcActionType.CreateOvfVolumeForStorageDomain, parameters, getContext().clone().withoutLock());
}
}
@Override
public boolean ignoreChildCommandFailure() {
return true;
}
public CreateOvfVolumeForStorageDomainCommandParameters createCreateOvfVolumeForStorageDomainParams() {
CreateOvfVolumeForStorageDomainCommandParameters parameters = new CreateOvfVolumeForStorageDomainCommandParameters(getParameters().getStoragePoolId(),
getParameters().getStorageDomainId());
parameters.setSkipDomainChecks(getParameters().isSkipDomainChecks());
parameters.setParentCommand(getActionType());
parameters.setParentParameters(getParameters());
parameters.setEndProcedure(EndProcedure.COMMAND_MANAGED);
return parameters;
}
@Override
public boolean performNextOperation(int completedChildCount) {
if (getParameters().getOvfUpdateStep() == OvfUpdateStep.OVF_STORES_CREATION) {
setOvfUpdateStep(OvfUpdateStep.OVF_UPLOAD);
updateOvfStoreContent();
return true;
}
return false;
}
private void setOvfUpdateStep(OvfUpdateStep step){
getParameters().setOvfUpdateStep(step);
persistCommand(getParameters().getParentCommand(), true);
}
@Override
protected void executeCommand() {
int missingDiskCount = getMissingDiskCount();
if (missingDiskCount == 0) {
setOvfUpdateStep(OvfUpdateStep.OVF_UPLOAD);
updateOvfStoreContent();
} else {
setOvfUpdateStep(OvfUpdateStep.OVF_STORES_CREATION);
createOvfStoreDisks(getMissingDiskCount());
}
setSucceeded(true);
}
protected Set<Guid> buildFilesForOvfs(List<Pair<Guid, String>> ovfs, InMemoryTar inMemoryTar) throws Exception {
Set<Guid> addedOvfIds = new HashSet<>();
for (Pair<Guid, String> pair : ovfs) {
if (pair.getSecond() != null) {
inMemoryTar.addTarEntry(pair.getSecond().getBytes(), pair.getFirst() + ".ovf");
addedOvfIds.add(pair.getFirst());
}
}
return addedOvfIds;
}
@Override
protected Map<String, Pair<String, String>> getExclusiveLocks() {
return getParameters().isSkipDomainChecks() ? Collections.emptyMap() :
Collections.singletonMap(getParameters().getStorageDomainId().toString(),
LockMessagesMatchUtil.makeLockingPair(LockingGroup.STORAGE,
EngineMessage.ACTION_TYPE_FAILED_DOMAIN_OVF_ON_UPDATE));
}
@Override
protected void endSuccessfully() {
setSucceeded(true);
}
@Override
protected void endWithFailure() {
setSucceeded(true);
}
@Override
protected Map<String, Pair<String, String>> getSharedLocks() {
return Collections.singletonMap(getParameters().getStoragePoolId().toString(),
LockMessagesMatchUtil.makeLockingPair(LockingGroup.OVF_UPDATE,
EngineMessage.ACTION_TYPE_FAILED_DOMAIN_OVF_ON_UPDATE));
}
@Override
public AuditLogType getAuditLogTypeValue() {
this.addCustomValue("StorageDomainName", getStorageDomain().getName());
this.setUserName(getUserName());
if (getActionState() == CommandActionState.EXECUTE) {
if (!getSucceeded()) {
return AuditLogType.UPDATE_OVF_FOR_STORAGE_DOMAIN_FAILED;
}
if (!SYSTEM_USER_NAME.equals(this.getUserName())) {
return AuditLogType.USER_UPDATE_OVF_STORE;
}
}
return super.getAuditLogTypeValue();
}
/**
* Ordering to provide consistent ovf update order - the order should be as follows, so that in case
* of failure we will have "previous" version of the data on other disk.
* 1. disks that were never ovf updated (getLastUpdated is null)
* 2. disk id
*/
public static final Comparator<StorageDomainOvfInfo> OVF_INFO_COMPARATOR =
Comparator.comparing(StorageDomainOvfInfo::getLastUpdated,
Comparator.nullsFirst(Comparator.naturalOrder())).
thenComparing(StorageDomainOvfInfo::getOvfDiskId);
}