package org.zstack.image; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Configurable; import org.springframework.transaction.annotation.Transactional; import org.zstack.core.asyncbatch.While; import org.zstack.core.cascade.CascadeConstant; import org.zstack.core.cascade.CascadeFacade; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.CloudBusCallBack; import org.zstack.core.componentloader.PluginRegistry; import org.zstack.core.db.DatabaseFacade; import org.zstack.core.db.SQL; import org.zstack.core.db.SQLBatch; import org.zstack.core.db.SimpleQuery; import org.zstack.core.db.SimpleQuery.Op; import org.zstack.core.errorcode.ErrorFacade; import org.zstack.core.notification.N; import org.zstack.core.workflow.FlowChainBuilder; import org.zstack.header.core.Completion; import org.zstack.header.core.NoErrorCompletion; import org.zstack.header.core.NopeCompletion; import org.zstack.header.core.ReturnValueCompletion; import org.zstack.header.core.workflow.*; import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.errorcode.OperationFailureException; import org.zstack.header.errorcode.SysErrors; import org.zstack.header.image.*; import org.zstack.header.image.ImageConstant.ImageMediaType; import org.zstack.header.image.ImageDeletionPolicyManager.ImageDeletionPolicy; import org.zstack.header.message.APIDeleteMessage; import org.zstack.header.message.APIMessage; import org.zstack.header.message.Message; import org.zstack.header.message.MessageReply; import org.zstack.header.storage.backup.*; import org.zstack.utils.CollectionUtils; import org.zstack.utils.Utils; import org.zstack.utils.function.ForEachFunction; import org.zstack.utils.function.Function; import org.zstack.utils.logging.CLogger; import java.sql.Timestamp; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import static org.zstack.core.Platform.argerr; import static org.zstack.core.Platform.operr; /** * Created with IntelliJ IDEA. * User: frank * Time: 5:38 PM * To change this template use File | Settings | File Templates. */ @Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) public class ImageBase implements Image { private static final CLogger logger = Utils.getLogger(ImageBase.class); @Autowired protected CloudBus bus; @Autowired protected DatabaseFacade dbf; @Autowired private CascadeFacade casf; @Autowired private ErrorFacade errf; @Autowired private ImageDeletionPolicyManager deletionPolicyMgr; @Autowired private PluginRegistry pluginRgty; protected ImageVO self; public ImageBase(ImageVO vo) { self = vo; } @Override public void handleMessage(Message msg) { try { if (msg instanceof APIMessage) { handleApiMessage((APIMessage) msg); } else { handleLocalMessage(msg); } } catch (Exception e) { bus.logExceptionWithMessageDump(msg, e); bus.replyErrorByMessageType(msg, e); } } protected ImageVO getSelf() { return self; } protected ImageInventory getSelfInventory() { return ImageInventory.valueOf(getSelf()); } private void handleLocalMessage(Message msg) { if (msg instanceof ImageDeletionMsg) { handle((ImageDeletionMsg) msg); } else if (msg instanceof ExpungeImageMsg) { handle((ExpungeImageMsg) msg); } else if (msg instanceof SyncImageSizeMsg) { handle((SyncImageSizeMsg) msg); } else { bus.dealWithUnknownMessage(msg); } } class ImageSize { long size; long actualSize; } private void handle(final SyncImageSizeMsg msg) { final SyncImageSizeReply reply = new SyncImageSizeReply(); syncImageSize(msg.getBackupStorageUuid(), new ReturnValueCompletion<ImageSize>(msg) { @Override public void success(ImageSize ret) { reply.setActualSize(ret.actualSize); reply.setSize(ret.size); bus.reply(msg, reply); } @Override public void fail(ErrorCode errorCode) { reply.setError(errorCode); bus.reply(msg, reply); } }); } private void syncImageSize(String backupStorageUuid, final ReturnValueCompletion<ImageSize> completion) { if (backupStorageUuid == null) { List<String> bsUuids = CollectionUtils.transformToList(self.getBackupStorageRefs(), new Function<String, ImageBackupStorageRefVO>() { @Override public String call(ImageBackupStorageRefVO arg) { return arg.getBackupStorageUuid(); } }); if (bsUuids.isEmpty()) { throw new OperationFailureException(operr("the image[uuid:%s, name:%s] is not on any backup storage", self.getUuid(), self.getName())); } SimpleQuery<BackupStorageVO> q = dbf.createQuery(BackupStorageVO.class); q.select(BackupStorageVO_.uuid); q.add(BackupStorageVO_.uuid, Op.IN, bsUuids); q.add(BackupStorageVO_.status, Op.EQ, BackupStorageStatus.Connected); q.setLimit(1); backupStorageUuid = q.findValue(); if (backupStorageUuid == null) { completion.fail(operr("No connected backup storage found for image[uuid:%s, name:%s]", self.getUuid(), self.getName())); return; } } SyncImageSizeOnBackupStorageMsg smsg = new SyncImageSizeOnBackupStorageMsg(); smsg.setBackupStorageUuid(backupStorageUuid); smsg.setImage(ImageInventory.valueOf(self)); bus.makeTargetServiceIdByResourceUuid(smsg, BackupStorageConstant.SERVICE_ID, backupStorageUuid); bus.send(smsg, new CloudBusCallBack(completion) { @Override public void run(MessageReply reply) { if (!reply.isSuccess()) { completion.fail(reply.getError()); } else { SyncImageSizeOnBackupStorageReply sr = reply.castReply(); self.setSize(sr.getSize()); self.setActualSize(sr.getActualSize()); dbf.update(self); ImageSize ret = new ImageSize(); ret.actualSize = sr.getActualSize(); ret.size = sr.getSize(); completion.success(ret); } } }); } private void handle(final ExpungeImageMsg msg) { final ExpungeImageReply reply = new ExpungeImageReply(); final ImageBackupStorageRefVO ref = CollectionUtils.find( self.getBackupStorageRefs(), arg -> arg.getBackupStorageUuid().equals(msg.getBackupStorageUuid()) ? arg : null ); if (ref == null) { logger.debug(String.format("cannot find reference for the image[uuid:%s] on the backup storage[uuid:%s], assume it's been deleted", self.getUuid(), msg.getBackupStorageUuid())); bus.reply(msg, reply); return; } DeleteBitsOnBackupStorageMsg dmsg = new DeleteBitsOnBackupStorageMsg(); dmsg.setBackupStorageUuid(ref.getBackupStorageUuid()); dmsg.setInstallPath(ref.getInstallPath()); bus.makeTargetServiceIdByResourceUuid(dmsg, BackupStorageConstant.SERVICE_ID, dmsg.getBackupStorageUuid()); bus.send(dmsg, new CloudBusCallBack(msg) { @Override public void run(MessageReply r) { if (!r.isSuccess()) { BackupStorageDeleteBitGC gc = new BackupStorageDeleteBitGC(); gc.NAME = String.format("gc-delete-bits-%s-on-backup-storage-%s", msg.getImageUuid(), ref.getBackupStorageUuid()); gc.backupStorageUuid = ref.getBackupStorageUuid(); gc.imageUuid = msg.getImageUuid(); gc.installPath = ref.getInstallPath(); gc.submit(ImageGlobalConfig.DELETION_GARBAGE_COLLECTION_INTERVAL.value(Long.class), TimeUnit.SECONDS); } returnBackupStorageCapacity(ref.getBackupStorageUuid(), self.getActualSize()); //TODO remove ref from metadata, this logic should after all refs deleted CollectionUtils.safeForEach(pluginRgty.getExtensionList(ExpungeImageExtensionPoint.class), new ForEachFunction<ExpungeImageExtensionPoint>() { @Override public void run(ExpungeImageExtensionPoint ext) { ext.afterExpungeImage(ImageInventory.valueOf(self), ref.getBackupStorageUuid()); } }); dbf.remove(ref); logger.debug(String.format("successfully expunged the image[uuid: %s, name: %s] on the backup storage[uuid: %s]", self.getUuid(), self.getName(), ref.getBackupStorageUuid())); new SQLBatch() { // delete the image if it's not on any backup storage @Override protected void scripts() { long count = sql("select count(ref) from ImageBackupStorageRefVO ref" + " where ref.imageUuid = :uuid", Long.class) .param("uuid", msg.getImageUuid()).find(); if (count == 0) { // the image is expunged on all backup storage // // Our trigger for ImageEO in AccoutResourceRefVO is triggered // by the 'UPDATE' action. We can not directly use a 'DELETE' // expression here. sql("update ImageEO set status = :status, deleted = :deleted where uuid = :uuid") .param("status", ImageStatus.Deleted) .param("deleted", new Timestamp(new Date().getTime()).toString()) .param("uuid", msg.getImageUuid()) .execute(); logger.debug(String.format("the image[uuid:%s, name:%s] has been expunged on all backup storage, remove it from database", self.getUuid(), self.getName())); } } }.execute(); bus.reply(msg, reply); } }); } private void returnBackupStorageCapacity(final String bsUuid, final long size) { ReturnBackupStorageMsg msg = new ReturnBackupStorageMsg(); msg.setBackupStorageUuid(bsUuid); msg.setSize(size); bus.makeTargetServiceIdByResourceUuid(msg, BackupStorageConstant.SERVICE_ID, bsUuid); bus.send(msg, new CloudBusCallBack(null) { @Override public void run(MessageReply reply) { if (!reply.isSuccess()) { N.New(BackupStorageVO.class, bsUuid).warn_("failed to return capacity[%s] to the backup storage[uuid:%s], %s", size, bsUuid, reply.getError()); } } }); } private void handle(final ImageDeletionMsg msg) { final ImageDeletionReply reply = new ImageDeletionReply(); Set<ImageBackupStorageRefVO> bsRefs = self.getBackupStorageRefs(); if (bsRefs.isEmpty() || bsRefs.stream().anyMatch( r -> r.getStatus() == ImageStatus.Creating || r.getStatus() == ImageStatus.Downloading)) { // the image is not on any backup storage; mostly likely the image is not in the status of Ready, for example // it's still downloading // in this case, we directly delete it from the database new SQLBatch() { @Override protected void scripts() { // in case 'recover api' called for an incomplete image sql(ImageBackupStorageRefVO.class).eq(ImageBackupStorageRefVO_.imageUuid, self.getUuid()).hardDelete(); sql(ImageVO.class).eq(ImageVO_.uuid, self.getUuid()).hardDelete(); } }.execute(); bus.reply(msg, reply); return; } final ImageDeletionPolicy deletionPolicy = msg.getDeletionPolicy() == null ? deletionPolicyMgr.getDeletionPolicy(self.getUuid()) : ImageDeletionPolicy.valueOf(msg.getDeletionPolicy()); FlowChain chain = FlowChainBuilder.newSimpleFlowChain(); chain.setName(String.format("delete-image-%s", self.getUuid())); Collection<ImageBackupStorageRefVO> toDelete = msg.getBackupStorageUuids() == null ? self.getBackupStorageRefs() : CollectionUtils.transformToList( self.getBackupStorageRefs(), new Function<ImageBackupStorageRefVO, ImageBackupStorageRefVO>() { @Override public ImageBackupStorageRefVO call(ImageBackupStorageRefVO arg) { return msg.getBackupStorageUuids().contains(arg.getBackupStorageUuid()) ? arg : null; } } ); List<Object> refs = new ArrayList<>(); for (final ImageBackupStorageRefVO ref : toDelete) { chain.then(new NoRollbackFlow() { String __name__ = String.format("delete-image-%s-from-backup-storage-%s", self.getUuid(), ref.getBackupStorageUuid()); @Override public void run(final FlowTrigger trigger, Map data) { if (deletionPolicy == ImageDeletionPolicy.Direct) { DeleteBitsOnBackupStorageMsg dmsg = new DeleteBitsOnBackupStorageMsg(); dmsg.setBackupStorageUuid(ref.getBackupStorageUuid()); dmsg.setInstallPath(ref.getInstallPath()); bus.makeTargetServiceIdByResourceUuid(dmsg, BackupStorageConstant.SERVICE_ID, dmsg.getBackupStorageUuid()); bus.send(dmsg, new CloudBusCallBack(trigger) { @Override public void run(MessageReply reply) { if (!reply.isSuccess()) { //TODO logger.warn(String.format("failed to delete image[uuid:%s, name:%s] from backup storage[uuid:%s] because %s," + " need to garbage collect it", self.getUuid(), self.getName(), reply.getError(), ref.getBackupStorageUuid())); } else { returnBackupStorageCapacity(ref.getBackupStorageUuid(), self.getActualSize()); dbf.remove(ref); //TODO should delete ref in metadata } trigger.next(); } }); } else if (deletionPolicy == ImageDeletionPolicy.DeleteReference) { dbf.remove(ref); logger.debug(String.format("delete the image[uuid: %s, name:%s]'s reference of the backup storage[uuid:%s]", self.getUuid(), self.getName(), ref.getBackupStorageUuid())); trigger.next(); } else { ref.setStatus(ImageStatus.Deleted); refs.add(ref); trigger.next(); } } }); } chain.done(new FlowDoneHandler(msg) { @Override public void handle(Map data) { new SQLBatch() { @Override protected void scripts() { for (Object ref : refs) { // update ref status if there is any dbf.getEntityManager().merge(ref); } dbf.getEntityManager().flush(); self = dbf.getEntityManager().find(ImageVO.class, self.getUuid()); if (self.getBackupStorageRefs().isEmpty()) { // the image is directly deleted from all backup storage // hard delete it sql(ImageVO.class).eq(ImageVO_.uuid, self.getUuid()).delete(); if (deletionPolicy == ImageDeletionPolicy.DeleteReference) { logger.debug(String.format("successfully directly deleted the image[uuid:%s, name:%s] from the database," + " as the policy is DeleteReference, it's still on the physical backup storage", self.getUuid(), self.getName())); } else { logger.debug(String.format("successfully directly deleted the image[uuid:%s, name:%s]", self.getUuid(), self.getName())); } } else { if (self.getBackupStorageRefs().stream().noneMatch(r -> r.getStatus() != ImageStatus.Deleted)) { self.setStatus(ImageStatus.Deleted); dbf.getEntityManager().merge(self); logger.debug(String.format("successfully deleted the image[uuid:%s, name:%s] with deletion policy[%s]", self.getUuid(), self.getName(), deletionPolicy)); } } } }.execute(); bus.reply(msg, reply); } }).error(new FlowErrorHandler(msg) { @Override public void handle(ErrorCode errCode, Map data) { reply.setError(errCode); bus.reply(msg, reply); } }).start(); } private void handleApiMessage(APIMessage msg) { if (msg instanceof APIChangeImageStateMsg) { handle((APIChangeImageStateMsg) msg); } else if (msg instanceof APIExpungeImageMsg) { handle((APIExpungeImageMsg) msg); } else if (msg instanceof APIDeleteImageMsg) { handle((APIDeleteImageMsg) msg); } else if (msg instanceof APIUpdateImageMsg) { handle((APIUpdateImageMsg) msg); } else if (msg instanceof APIRecoverImageMsg) { handle((APIRecoverImageMsg) msg); } else if (msg instanceof APISyncImageSizeMsg) { handle((APISyncImageSizeMsg) msg); } else { bus.dealWithUnknownMessage(msg); } } private void handle(APISyncImageSizeMsg msg) { final APISyncImageSizeEvent evt = new APISyncImageSizeEvent(msg.getId()); syncImageSize(null, new ReturnValueCompletion<ImageSize>(msg) { @Override public void success(ImageSize ret) { self = dbf.reload(self); evt.setInventory(getSelfInventory()); bus.publish(evt); } @Override public void fail(ErrorCode errorCode) { evt.setError(errorCode); bus.publish(evt); } }); } private void handle(APIRecoverImageMsg msg) { List<String> toRecoverBsUuids; if (msg.getBackupStorageUuids() == null || msg.getBackupStorageUuids().isEmpty()) { toRecoverBsUuids = CollectionUtils.transformToList(self.getBackupStorageRefs(), new Function<String, ImageBackupStorageRefVO>() { @Override public String call(ImageBackupStorageRefVO arg) { return arg.getStatus() == ImageStatus.Deleted ? arg.getBackupStorageUuid() : null; } }); if (toRecoverBsUuids.isEmpty()) { throw new OperationFailureException(operr("the image[uuid:%s, name:%s] is not deleted on any backup storage", self.getUuid(), self.getName())); } } else { toRecoverBsUuids = new ArrayList<String>(); for (final String bsUuid : msg.getBackupStorageUuids()) { ImageBackupStorageRefVO ref = CollectionUtils.find(self.getBackupStorageRefs(), new Function<ImageBackupStorageRefVO, ImageBackupStorageRefVO>() { @Override public ImageBackupStorageRefVO call(ImageBackupStorageRefVO arg) { return bsUuid.equals(arg.getBackupStorageUuid()) ? arg : null; } }); if (ref == null) { throw new OperationFailureException(argerr("the image[uuid:%s, name:%s] is not on the backup storage[uuid:%s]", self.getUuid(), self.getName(), bsUuid)); } if (ref.getStatus() != ImageStatus.Deleted) { throw new OperationFailureException(argerr("the image[uuid:%s, name:%s]'s status[%s] is not Deleted on the backup storage[uuid:%s]", self.getUuid(), self.getName(), ref.getStatus(), bsUuid)); } toRecoverBsUuids.add(bsUuid); } } List<Object> refs = new ArrayList<>(); for (ImageBackupStorageRefVO ref : self.getBackupStorageRefs()) { if (toRecoverBsUuids.contains(ref.getBackupStorageUuid())) { ref.setStatus(ImageStatus.Ready); refs.add(ref); } } self.setStatus(ImageStatus.Ready); refs.add(self); dbf.updateCollection(refs); self = dbf.reload(self); logger.debug(String.format("successfully recovered the image[uuid:%s, name:%s] on the backup storage%s", self.getUuid(), self.getName(), toRecoverBsUuids)); APIRecoverImageEvent evt = new APIRecoverImageEvent(msg.getId()); evt.setInventory(getSelfInventory()); bus.publish(evt); } private void handle(final APIExpungeImageMsg msg) { List<String> bsUuids = new ArrayList<>(); if (msg.getBackupStorageUuids() == null || msg.getBackupStorageUuids().isEmpty()) { bsUuids = CollectionUtils.transformToList( self.getBackupStorageRefs(), new Function<String, ImageBackupStorageRefVO>() { @Override public String call(ImageBackupStorageRefVO arg) { return ImageStatus.Deleted == arg.getStatus() ? arg.getBackupStorageUuid() : null; } } ); if (bsUuids.isEmpty()) { throw new OperationFailureException(operr("the image[uuid:%s, name:%s] is not deleted on any backup storage", self.getUuid(), self.getName())); } } else { for (final String bsUuid : msg.getBackupStorageUuids()) { ImageBackupStorageRefVO ref = CollectionUtils.find( self.getBackupStorageRefs(), new Function<ImageBackupStorageRefVO, ImageBackupStorageRefVO>() { @Override public ImageBackupStorageRefVO call(ImageBackupStorageRefVO arg) { return arg.getBackupStorageUuid().equals(bsUuid) ? arg : null; } } ); if (ref == null) { throw new OperationFailureException(argerr("the image[uuid:%s, name:%s] is not on the backup storage[uuid:%s]", self.getUuid(), self.getName(), bsUuid)); } if (ref.getStatus() != ImageStatus.Deleted) { throw new OperationFailureException(argerr("the image[uuid:%s, name:%s] is not deleted on the backup storage[uuid:%s]", self.getUuid(), self.getName(), bsUuid)); } bsUuids.add(bsUuid); } } new While<>(bsUuids).all((bsUuid, completion) -> { ExpungeImageMsg emsg = new ExpungeImageMsg(); emsg.setBackupStorageUuid(bsUuid); emsg.setImageUuid(self.getUuid()); bus.makeTargetServiceIdByResourceUuid(emsg, ImageConstant.SERVICE_ID, self.getUuid()); bus.send(emsg, new CloudBusCallBack(completion) { @Override public void run(MessageReply reply) { if (!reply.isSuccess()) { logger.warn(reply.getError().toString()); } completion.done(); } }); }).run(new NoErrorCompletion(msg) { @Override public void done() { bus.publish(new APIExpungeImageEvent(msg.getId())); } }); } private void handle(APIUpdateImageMsg msg) { boolean update = false; if (msg.getName() != null) { self.setName(msg.getName()); update = true; } if (msg.getDescription() != null) { self.setDescription(msg.getDescription()); update = true; } if (msg.getSystem() != null) { self.setSystem(msg.getSystem()); update = true; } if (msg.getGuestOsType() != null) { self.setGuestOsType(msg.getGuestOsType()); update = true; } if (msg.getMediaType() != null) { self.setMediaType(ImageMediaType.valueOf(msg.getMediaType())); update = true; } if (msg.getFormat() != null) { self.setFormat(msg.getFormat()); update = true; } if (msg.getPlatform() != null) { self.setPlatform(ImagePlatform.valueOf(msg.getPlatform())); update = true; } if (update) { self = dbf.updateAndRefresh(self); } APIUpdateImageEvent evt = new APIUpdateImageEvent(msg.getId()); evt.setInventory(getSelfInventory()); bus.publish(evt); } private void handle(APIChangeImageStateMsg msg) { ImageStateEvent sevt = ImageStateEvent.valueOf(msg.getStateEvent()); if (sevt == ImageStateEvent.disable) { self.setState(ImageState.Disabled); } else { self.setState(ImageState.Enabled); } self = dbf.updateAndRefresh(self); APIChangeImageStateEvent evt = new APIChangeImageStateEvent(msg.getId()); evt.setInventory(ImageInventory.valueOf(self)); bus.publish(evt); } private void handle(APIDeleteImageMsg msg) { final APIDeleteImageEvent evt = new APIDeleteImageEvent(msg.getId()); final String issuer = ImageVO.class.getSimpleName(); ImageDeletionStruct struct = new ImageDeletionStruct(); struct.setImage(ImageInventory.valueOf(self)); struct.setBackupStorageUuids(msg.getBackupStorageUuids()); final List<ImageDeletionStruct> ctx = Arrays.asList(struct); FlowChain chain = FlowChainBuilder.newSimpleFlowChain(); chain.setName(String.format("delete-image-%s", msg.getUuid())); if (msg.getDeletionMode() == APIDeleteMessage.DeletionMode.Permissive) { chain.then(new NoRollbackFlow() { @Override public void run(final FlowTrigger trigger, Map data) { casf.asyncCascade(CascadeConstant.DELETION_CHECK_CODE, issuer, ctx, new Completion(trigger) { @Override public void success() { trigger.next(); } @Override public void fail(ErrorCode errorCode) { trigger.fail(errorCode); } }); } }).then(new NoRollbackFlow() { @Override public void run(final FlowTrigger trigger, Map data) { casf.asyncCascade(CascadeConstant.DELETION_DELETE_CODE, issuer, ctx, new Completion(trigger) { @Override public void success() { trigger.next(); } @Override public void fail(ErrorCode errorCode) { trigger.fail(errorCode); } }); } }); } else { chain.then(new NoRollbackFlow() { @Override public void run(final FlowTrigger trigger, Map data) { casf.asyncCascade(CascadeConstant.DELETION_FORCE_DELETE_CODE, issuer, ctx, new Completion(trigger) { @Override public void success() { trigger.next(); } @Override public void fail(ErrorCode errorCode) { trigger.fail(errorCode); } }); } }); } chain.done(new FlowDoneHandler(msg) { @Override public void handle(Map data) { casf.asyncCascadeFull(CascadeConstant.DELETION_CLEANUP_CODE, issuer, ctx, new NopeCompletion()); bus.publish(evt); } }).error(new FlowErrorHandler(msg) { @Override public void handle(ErrorCode errCode, Map data) { evt.setError(errf.instantiateErrorCode(SysErrors.DELETE_RESOURCE_ERROR, errCode)); bus.publish(evt); } }).start(); } }