package org.zstack.storage.volume; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.zstack.core.cascade.*; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.CloudBusListCallBack; import org.zstack.core.db.DatabaseFacade; import org.zstack.core.db.SimpleQuery; import org.zstack.core.db.SimpleQuery.Op; import org.zstack.core.db.UpdateQuery; import org.zstack.header.configuration.DiskOfferingInventory; import org.zstack.header.configuration.DiskOfferingVO; import org.zstack.header.core.Completion; import org.zstack.header.exception.CloudRuntimeException; import org.zstack.header.identity.AccountInventory; import org.zstack.header.identity.AccountResourceRefVO; import org.zstack.header.identity.AccountResourceRefVO_; import org.zstack.header.identity.AccountVO; import org.zstack.header.message.MessageReply; import org.zstack.header.storage.primary.PrimaryStorageInventory; import org.zstack.header.storage.primary.PrimaryStorageVO; import org.zstack.header.volume.*; import org.zstack.header.volume.VolumeDeletionPolicyManager.VolumeDeletionPolicy; import org.zstack.utils.CollectionUtils; import org.zstack.utils.Utils; import org.zstack.utils.function.Function; import org.zstack.utils.logging.CLogger; import javax.persistence.Query; import javax.persistence.TypedQuery; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; import java.util.stream.Collectors; /** */ public class VolumeCascadeExtension extends AbstractAsyncCascadeExtension { private static final CLogger logger = Utils.getLogger(VolumeCascadeExtension.class); @Autowired private DatabaseFacade dbf; @Autowired private CloudBus bus; @Autowired private VolumeDeletionPolicyManager deletionPolicyManager; private static final String NAME = VolumeVO.class.getSimpleName(); private static final int OP_DELETE_VOLUME = 0; private static final int OP_UPDATE_DISK_OFFERING_COLUMN = 1; @Override public void asyncCascade(CascadeAction action, Completion completion) { if (action.isActionCode(CascadeConstant.DELETION_CHECK_CODE)) { handleDeletionCheck(action, completion); } else if (action.isActionCode(CascadeConstant.DELETION_DELETE_CODE, CascadeConstant.DELETION_FORCE_DELETE_CODE)) { handleDeletion(action, completion); } else if (action.isActionCode(CascadeConstant.DELETION_CLEANUP_CODE)) { handleDeletionCleanup(action, completion); } else { completion.success(); } } private int actionToOpCode(CascadeAction action) { if (action.getParentIssuer().equals(NAME) || action.getParentIssuer().equals(PrimaryStorageVO.class.getSimpleName())) { return OP_DELETE_VOLUME; } if (action.getParentIssuer().equals(DiskOfferingVO.class.getSimpleName())) { return OP_UPDATE_DISK_OFFERING_COLUMN; } if (action.getParentIssuer().equals(AccountVO.class.getSimpleName())) { return OP_DELETE_VOLUME; } throw new CloudRuntimeException(String.format("unknown edge [%s]", action.getParentIssuer())); } private String actionToDeletionPolicy(CascadeAction action, String volUuid) { if (action.getParentIssuer().equals(PrimaryStorageVO.class.getSimpleName())) { return VolumeDeletionPolicy.DBOnly.toString(); } else { return deletionPolicyManager.getDeletionPolicy(volUuid).toString(); } } private void handleDeletionCleanup(CascadeAction action, Completion completion) { dbf.eoCleanup(VolumeVO.class); completion.success(); } private List<VolumeDeletionStruct> toVolumeDeletionStruct(CascadeAction action, List<VolumeVO> vos) { List<VolumeDeletionStruct> structs = new ArrayList<VolumeDeletionStruct>(); for (VolumeVO vo : vos) { VolumeDeletionStruct s = new VolumeDeletionStruct(); s.setInventory(VolumeInventory.valueOf(vo)); s.setDeletionPolicy(actionToDeletionPolicy(action, vo.getUuid())); structs.add(s); } return structs; } private List<VolumeDeletionStruct> volumesFromAction(CascadeAction action) { if (CascadeConstant.DELETION_CODES.contains(action.getActionCode())) { if (NAME.equals(action.getParentIssuer())) { return action.getParentIssuerContext(); } else if (PrimaryStorageVO.class.getSimpleName().equals(action.getParentIssuer())) { List<PrimaryStorageInventory> pinvs = action.getParentIssuerContext(); List<String> psUuids = CollectionUtils.transformToList(pinvs, new Function<String, PrimaryStorageInventory>() { @Override public String call(PrimaryStorageInventory arg) { return arg.getUuid(); } }); SimpleQuery<VolumeVO> q = dbf.createQuery(VolumeVO.class); q.add(VolumeVO_.type, Op.EQ, VolumeType.Data); q.add(VolumeVO_.primaryStorageUuid, Op.IN, psUuids); List<VolumeVO> vos = q.list(); return toVolumeDeletionStruct(action, vos); } else if (AccountVO.class.getSimpleName().equals(action.getParentIssuer())) { final List<String> auuids = CollectionUtils.transformToList((List<AccountInventory>) action.getParentIssuerContext(), new Function<String, AccountInventory>() { @Override public String call(AccountInventory arg) { return arg.getUuid(); } }); List<VolumeVO> vos = new Callable<List<VolumeVO>>() { @Override @Transactional(readOnly = true) public List<VolumeVO> call() { String sql = "select d" + " from VolumeVO d, AccountResourceRefVO r" + " where d.uuid = r.resourceUuid" + " and r.resourceType = :rtype" + " and r.accountUuid in (:auuids)" + " and d.type = :dtype"; TypedQuery<VolumeVO> q = dbf.getEntityManager().createQuery(sql, VolumeVO.class); q.setParameter("auuids", auuids); q.setParameter("rtype", VolumeVO.class.getSimpleName()); q.setParameter("dtype", VolumeType.Data); return q.getResultList(); } }.call(); if (!vos.isEmpty()) { return toVolumeDeletionStruct(action, vos); } } } return null; } private void handleDeletion(final CascadeAction action, final Completion completion) { int op = actionToOpCode(action); if (op == OP_DELETE_VOLUME) { deleteVolume(action, completion); } else if (op == OP_UPDATE_DISK_OFFERING_COLUMN) { if (VolumeGlobalConfig.UPDATE_DISK_OFFERING_TO_NULL_WHEN_DELETING.value(Boolean.class)) { updateDiskOfferingColumn(action, completion); } else { completion.success(); } } } @Transactional private void updateDiskOfferingColumn(CascadeAction action, Completion completion) { List<DiskOfferingInventory> diskOfferingInventories = action.getParentIssuerContext(); List<String> diskOfferingUuids = CollectionUtils.transformToList(diskOfferingInventories, new Function<String, DiskOfferingInventory>() { @Override public String call(DiskOfferingInventory arg) { return arg.getUuid(); } }); String sql = "update VolumeVO vol set vol.diskOfferingUuid = null where vol.diskOfferingUuid in (:uuids)"; Query q = dbf.getEntityManager().createQuery(sql); q.setParameter("uuids", diskOfferingUuids); q.executeUpdate(); completion.success(); } private void deleteVolume(final CascadeAction action, final Completion completion) { final List<VolumeDeletionStruct> volumes = volumesFromAction(action); if (volumes == null || volumes.isEmpty()) { completion.success(); return; } List<VolumeDeletionMsg> msgs = new ArrayList<VolumeDeletionMsg>(); for (VolumeDeletionStruct vol : volumes) { VolumeDeletionMsg msg = new VolumeDeletionMsg(); msg.setDetachBeforeDeleting(vol.isDetachBeforeDeleting()); msg.setForceDelete(action.isActionCode(CascadeConstant.DELETION_FORCE_DELETE_CODE)); msg.setVolumeUuid(vol.getInventory().getUuid()); msg.setDeletionPolicy(vol.getDeletionPolicy()); bus.makeTargetServiceIdByResourceUuid(msg, VolumeConstant.SERVICE_ID, vol.getInventory().getUuid()); msgs.add(msg); } bus.send(msgs, 20, new CloudBusListCallBack(completion) { @Override public void run(List<MessageReply> replies) { if (!action.isActionCode(CascadeConstant.DELETION_FORCE_DELETE_CODE)) { for (MessageReply r : replies) { if (!r.isSuccess()) { completion.fail(r.getError()); return; } } } if (action.getParentIssuer().equals(PrimaryStorageVO.class.getSimpleName())) { // when deleting the primary storage, the foreign key of VolumeVO to PrimaryStorageVO // will cause VolumeVO to be deleted but left AccountResourceRefVO of the volume left List<String> volUuids = volumes.stream().map(s -> s.getInventory().getUuid()).collect(Collectors.toList()); UpdateQuery q = UpdateQuery.New(AccountResourceRefVO.class); q.condAnd(AccountResourceRefVO_.resourceUuid, Op.IN, volUuids); q.condAnd(AccountResourceRefVO_.resourceType, Op.EQ, VolumeVO.class.getSimpleName()); q.delete(); } completion.success(); } }); } private void handleDeletionCheck(CascadeAction action, Completion completion) { completion.success(); } @Override public List<String> getEdgeNames() { return Arrays.asList(PrimaryStorageVO.class.getSimpleName(), DiskOfferingVO.class.getSimpleName(), AccountVO.class.getSimpleName()); } @Override public String getCascadeResourceName() { return NAME; } @Override public CascadeAction createActionForChildResource(CascadeAction action) { int op = actionToOpCode(action); if (op == OP_DELETE_VOLUME) { List<VolumeDeletionStruct> invs = volumesFromAction(action); if (invs != null) { return action.copy().setParentIssuer(NAME).setParentIssuerContext(invs); } } return null; } }