package org.zstack.storage.backup;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.http.HttpHeaders;
import org.springframework.transaction.annotation.Transactional;
import org.zstack.core.CoreGlobalProperty;
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.cloudbus.EventFacade;
import org.zstack.core.config.GlobalConfigFacade;
import org.zstack.core.db.DatabaseFacade;
import org.zstack.core.db.TransactionalCallback;
import org.zstack.core.errorcode.ErrorFacade;
import org.zstack.core.thread.ChainTask;
import org.zstack.core.thread.SyncTask;
import org.zstack.core.thread.SyncTaskChain;
import org.zstack.core.thread.ThreadFacade;
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.workflow.*;
import org.zstack.header.errorcode.ErrorCode;
import org.zstack.header.errorcode.OperationFailureException;
import org.zstack.header.errorcode.SysErrors;
import org.zstack.header.image.ImageInventory;
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.rest.RESTFacade;
import org.zstack.header.storage.backup.*;
import org.zstack.header.storage.backup.BackupStorageCanonicalEvents.BackupStorageStatusChangedData;
import org.zstack.header.storage.backup.BackupStorageErrors.Opaque;
import org.zstack.utils.Utils;
import org.zstack.utils.logging.CLogger;
import static org.zstack.core.Platform.operr;
import javax.persistence.LockModeType;
import javax.persistence.Query;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@Configurable(preConstruction = true, autowire = Autowire.BY_TYPE)
public abstract class BackupStorageBase extends AbstractBackupStorage {
private static final CLogger logger = Utils.getLogger(BackupStorageBase.class);
protected BackupStorageVO self;
@Autowired
protected CloudBus bus;
@Autowired
protected DatabaseFacade dbf;
@Autowired
protected GlobalConfigFacade gcf;
@Autowired
protected BackupStorageExtensionPointEmitter extpEmitter;
@Autowired
protected CascadeFacade casf;
@Autowired
protected ErrorFacade errf;
@Autowired
protected ThreadFacade thdf;
@Autowired
protected BackupStoragePingTracker tracker;
@Autowired
protected EventFacade evtf;
@Autowired
protected RESTFacade restf;
abstract protected void handle(DownloadImageMsg msg);
abstract protected void handle(DownloadVolumeMsg msg);
abstract protected void handle(DeleteBitsOnBackupStorageMsg msg);
abstract protected void handle(BackupStorageAskInstallPathMsg msg);
abstract protected void handle(GetImageSizeOnBackupStorageMsg msg);
abstract protected void handle(SyncImageSizeOnBackupStorageMsg msg);
abstract protected void connectHook(boolean newAdd, Completion completion);
abstract protected void pingHook(Completion completion);
public BackupStorageBase(BackupStorageVO self) {
this.self = self;
}
@Override
public void deleteHook() {
}
@Override
public void detachHook(Completion completion) {
completion.success();
}
@Override
public void attachHook(String zoneUuid, Completion completion) {
completion.success();
}
@Override
public void changeStateHook(BackupStorageStateEvent evt, BackupStorageState nextState) {
}
protected void exceptionIfImageSizeGreaterThanAvailableCapacity(String url) {
if (CoreGlobalProperty.UNIT_TEST_ON) {
return;
}
url = url.trim();
if (!url.startsWith("http") && !url.startsWith("https")) {
return;
}
String len;
try {
HttpHeaders header = restf.getRESTTemplate().headForHeaders(url);
len = header.getFirst("Content-Length");
} catch (Exception e) {
throw new OperationFailureException(operr("cannot get image. The image url is %s. Exception is %s", url, e.toString()));
}
if (len == null) {
return;
}
long size = Long.valueOf(len);
if (size > self.getAvailableCapacity()) {
throw new OperationFailureException(operr("the backup storage[uuid:%s, name:%s] has not enough capacity to download the image[%s]." +
"Required size:%s, available size:%s", self.getUuid(), self.getName(), url, size, self.getAvailableCapacity()));
}
}
protected void refreshVO() {
self = dbf.reload(self);
}
protected BackupStorageInventory getSelfInventory() {
return BackupStorageInventory.valueOf(self);
}
protected void checkStatus(Message msg) {
if (!statusChecker.isOperationAllowed(msg.getClass().getName(), self.getStatus().toString())) {
throw new OperationFailureException(operr("backup storage cannot proceed message[%s] because its status is %s", msg.getClass().getName(), self.getStatus()));
}
}
protected void checkState(Message msg) {
if (!stateChecker.isOperationAllowed(msg.getClass().getName(), self.getState().toString())) {
throw new OperationFailureException(operr("backup storage cannot proceed message[%s] because its state is %s", msg.getClass().getName(), self.getState()));
}
}
@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 void handleLocalMessage(Message msg) throws URISyntaxException {
if (msg instanceof DownloadImageMsg) {
handleBase((DownloadImageMsg) msg);
} else if (msg instanceof ScanBackupStorageMsg) {
handle((ScanBackupStorageMsg) msg);
} else if (msg instanceof BackupStorageDeletionMsg) {
handle((BackupStorageDeletionMsg) msg);
} else if (msg instanceof ChangeBackupStorageStatusMsg) {
handle((ChangeBackupStorageStatusMsg) msg);
} else if (msg instanceof ConnectBackupStorageMsg) {
handle((ConnectBackupStorageMsg) msg);
} else if (msg instanceof ReturnBackupStorageMsg) {
handle((ReturnBackupStorageMsg) msg);
} else if (msg instanceof DownloadVolumeMsg) {
handleBase((DownloadVolumeMsg) msg);
} else if (msg instanceof DeleteBitsOnBackupStorageMsg) {
handleBase((DeleteBitsOnBackupStorageMsg) msg);
} else if (msg instanceof PingBackupStorageMsg) {
handle((PingBackupStorageMsg) msg);
} else if (msg instanceof BackupStorageAskInstallPathMsg) {
handle((BackupStorageAskInstallPathMsg) msg);
} else if (msg instanceof SyncImageSizeOnBackupStorageMsg) {
handle((SyncImageSizeOnBackupStorageMsg) msg);
} else if (msg instanceof GetImageSizeOnBackupStorageMsg) {
handle((GetImageSizeOnBackupStorageMsg) msg);
} else {
bus.dealWithUnknownMessage(msg);
}
}
private void handle(final PingBackupStorageMsg msg) {
final PingBackupStorageReply reply = new PingBackupStorageReply();
pingHook(new Completion(msg) {
private void reconnect() {
ConnectBackupStorageMsg cmsg = new ConnectBackupStorageMsg();
cmsg.setBackupStorageUuid(self.getUuid());
cmsg.setNewAdd(false);
bus.makeTargetServiceIdByResourceUuid(cmsg, BackupStorageConstant.SERVICE_ID, self.getUuid());
bus.send(cmsg);
}
@Override
public void success() {
if (self.getStatus() != BackupStorageStatus.Connected) {
reconnect();
}
bus.reply(msg, reply);
}
@Override
public void fail(ErrorCode errorCode) {
changeStatus(BackupStorageStatus.Disconnected);
Boolean doReconnect = (Boolean) errorCode.getFromOpaque(Opaque.RECONNECT_AGENT.toString());
if (doReconnect != null && doReconnect) {
reconnect();
}
reply.setError(errorCode);
bus.reply(msg, reply);
}
});
}
private void handleBase(DownloadImageMsg msg) {
checkState(msg);
checkStatus(msg);
exceptionIfImageSizeGreaterThanAvailableCapacity(msg.getImageInventory().getUrl());
handle(msg);
}
private void handleBase(DownloadVolumeMsg msg) {
checkState(msg);
checkStatus(msg);
handle(msg);
}
private void handleBase(DeleteBitsOnBackupStorageMsg msg) {
checkState(msg);
checkStatus(msg);
handle(msg);
}
@Transactional
private void handle(ReturnBackupStorageMsg msg) {
self = dbf.getEntityManager().find(BackupStorageVO.class, self.getUuid(), LockModeType.PESSIMISTIC_WRITE);
long availSize = self.getAvailableCapacity() + msg.getSize();
if (availSize > self.getTotalCapacity()) {
availSize = self.getTotalCapacity();
}
self.setAvailableCapacity(availSize);
dbf.getEntityManager().merge(self);
bus.reply(msg, new ReturnBackupStorageReply());
}
private void handle(final ConnectBackupStorageMsg msg) {
thdf.chainSubmit(new ChainTask(msg) {
@Override
public String getSyncSignature() {
return String.format("connect-backup-storage-%s", self.getUuid());
}
@Override
public void run(final SyncTaskChain chain) {
final ConnectBackupStorageReply reply = new ConnectBackupStorageReply();
changeStatus(BackupStorageStatus.Connecting);
connectHook(msg.isNewAdd(), new Completion(msg, chain) {
@Override
public void success() {
self = dbf.reload(self);
changeStatus(BackupStorageStatus.Connected);
tracker.track(self.getUuid());
bus.reply(msg, reply);
chain.next();
}
@Override
public void fail(ErrorCode errorCode) {
if (!msg.isNewAdd()) {
changeStatus(BackupStorageStatus.Disconnected);
}
reply.setError(errorCode);
bus.reply(msg, reply);
chain.next();
}
});
}
@Override
public String getName() {
return getSyncSignature();
}
});
}
protected void changeStatus(final BackupStorageStatus status, final NoErrorCompletion completion) {
thdf.syncSubmit(new SyncTask<Void>() {
private String name = String.format("backupstorage-%s-change-status", self.getUuid());
@Override
public String getSyncSignature() {
return name;
}
@Override
public int getSyncLevel() {
return 1;
}
@Override
public String getName() {
return name;
}
@Override
public Void call() throws Exception {
if (status == self.getStatus()) {
completion.done();
return null;
}
changeStatus(status);
logger.debug(String.format("backup storage[uuid:%s, name:%s] change status from %s to %s",
self.getUuid(), self.getName(), self.getStatus(), status));
completion.done();
return null;
}
});
}
private void handle(final ChangeBackupStorageStatusMsg msg) {
changeStatus(BackupStorageStatus.valueOf(msg.getStatus()), new NoErrorCompletion(msg) {
@Override
public void done() {
ChangeBackupStorageStatusReply reply = new ChangeBackupStorageStatusReply();
bus.reply(msg, reply);
}
});
}
private void handle(BackupStorageDeletionMsg msg) {
BackupStorageInventory inv = BackupStorageInventory.valueOf(self);
extpEmitter.beforeDelete(inv);
deleteHook();
extpEmitter.afterDelete(inv);
BackupStorageDeletionReply reply = new BackupStorageDeletionReply();
tracker.untrack(self.getUuid());
bus.reply(msg, reply);
}
private void doScanImages() {
try {
List<ImageInventory> images = this.scanImages();
} catch (Exception e) {
logger.warn(String.format("Unhandled exception happened while scanning backup storage[uuid:%s]", self.getUuid()), e);
}
}
private void handle(ScanBackupStorageMsg msg) {
doScanImages();
}
protected void handleApiMessage(APIMessage msg) {
try {
if (msg instanceof APIDeleteBackupStorageMsg) {
handle((APIDeleteBackupStorageMsg) msg);
} else if (msg instanceof APIChangeBackupStorageStateMsg) {
handle((APIChangeBackupStorageStateMsg) msg);
} else if (msg instanceof APIAttachBackupStorageToZoneMsg) {
handle((APIAttachBackupStorageToZoneMsg) msg);
} else if (msg instanceof APIDetachBackupStorageFromZoneMsg) {
handle((APIDetachBackupStorageFromZoneMsg) msg);
} else if (msg instanceof APIScanBackupStorageMsg) {
handle((APIScanBackupStorageMsg) msg);
} else if (msg instanceof APIUpdateBackupStorageMsg) {
handle((APIUpdateBackupStorageMsg) msg);
} else if (msg instanceof APIReconnectBackupStorageMsg) {
handle((APIReconnectBackupStorageMsg) msg);
} else {
bus.dealWithUnknownMessage(msg);
}
} catch (Exception e) {
bus.logExceptionWithMessageDump(msg, e);
bus.replyErrorByMessageType(msg, e);
}
}
private void handle(APIReconnectBackupStorageMsg msg) {
final APIReconnectBackupStorageEvent evt = new APIReconnectBackupStorageEvent(msg.getId());
ConnectBackupStorageMsg cmsg = new ConnectBackupStorageMsg();
cmsg.setBackupStorageUuid(self.getUuid());
bus.makeTargetServiceIdByResourceUuid(cmsg, BackupStorageConstant.SERVICE_ID, self.getUuid());
bus.send(cmsg, new CloudBusCallBack(msg) {
@Override
public void run(MessageReply reply) {
if (!reply.isSuccess()) {
evt.setError(reply.getError());
} else {
self = dbf.reload(self);
evt.setInventory(getSelfInventory());
}
bus.publish(evt);
}
});
}
protected BackupStorageVO updateBackupStorage(APIUpdateBackupStorageMsg msg) {
boolean update = false;
if (msg.getName() != null) {
self.setName(msg.getName());
update = true;
}
if (msg.getDescription() != null) {
self.setDescription(msg.getDescription());
update = true;
}
return update ? self : null;
}
private void handle(APIUpdateBackupStorageMsg msg) {
BackupStorageVO vo = updateBackupStorage(msg);
if (vo != null) {
self = dbf.updateAndRefresh(vo);
}
APIUpdateBackupStorageEvent evt = new APIUpdateBackupStorageEvent(msg.getId());
evt.setInventory(getSelfInventory());
bus.publish(evt);
}
private void handle(APIScanBackupStorageMsg msg) {
APIScanBackupStorageEvent evt = new APIScanBackupStorageEvent(msg.getId());
bus.publish(evt);
doScanImages();
}
protected void handle(final APIDetachBackupStorageFromZoneMsg msg) {
final APIDetachBackupStorageFromZoneEvent evt = new APIDetachBackupStorageFromZoneEvent(msg.getId());
try {
extpEmitter.preDetach(self, msg.getZoneUuid());
} catch (BackupStorageException e) {
evt.setError(errf.instantiateErrorCode(BackupStorageErrors.DETACH_ERROR, e.getMessage()));
bus.publish(evt);
return;
}
extpEmitter.beforeDetach(self, msg.getZoneUuid());
detachHook(new Completion(msg) {
@Transactional
private BackupStorageVO updateDb(BackupStorageVO vo, String zoneUuid) {
dbf.entityForTranscationCallback(TransactionalCallback.Operation.REMOVE, BackupStorageZoneRefVO.class);
String sql = "delete from BackupStorageZoneRefVO bz where bz.zoneUuid = :zoneUuid and bz.backupStorageUuid = :bsUuid";
Query q = dbf.getEntityManager().createQuery(sql);
q.setParameter("zoneUuid", zoneUuid);
q.setParameter("bsUuid", vo.getUuid());
q.executeUpdate();
vo = dbf.getEntityManager().find(BackupStorageVO.class, vo.getUuid());
return vo;
}
@Override
public void success() {
self = updateDb(self, msg.getZoneUuid());
extpEmitter.afterDetach(self, msg.getZoneUuid());
evt.setInventory(getSelfInventory());
logger.debug(String.format("successfully detached backup storage[uuid:%s] from zone[uuid:%s]", self.getUuid(), msg.getBackupStorageUuid()));
bus.publish(evt);
}
@Override
public void fail(ErrorCode errorCode) {
logger.warn(errorCode.toString());
extpEmitter.failToDetach(self, msg.getZoneUuid());
evt.setError(errf.instantiateErrorCode(BackupStorageErrors.DETACH_ERROR, errorCode));
bus.publish(evt);
}
});
}
protected void handle(final APIAttachBackupStorageToZoneMsg msg) {
final APIAttachBackupStorageToZoneEvent evt = new APIAttachBackupStorageToZoneEvent(msg.getId());
final BackupStorageVO svo = dbf.findByUuid(msg.getBackupStorageUuid(), BackupStorageVO.class);
String err = extpEmitter.preAttach(svo, msg.getZoneUuid());
if (err != null) {
evt.setError(errf.instantiateErrorCode(BackupStorageErrors.ATTACH_ERROR, err));
bus.publish(evt);
return;
}
extpEmitter.beforeAttach(svo, msg.getZoneUuid());
attachHook(msg.getZoneUuid(), new Completion(msg) {
@Override
public void success() {
BackupStorageZoneRefVO rvo = new BackupStorageZoneRefVO();
rvo.setBackupStorageUuid(svo.getUuid());
rvo.setZoneUuid(msg.getZoneUuid());
dbf.persist(rvo);
refreshVO();
extpEmitter.afterAttach(self, msg.getZoneUuid());
evt.setInventory(getSelfInventory());
logger.debug(String.format("successfully attached backup storage[uuid:%s, name:%s]", self.getUuid(), self.getName()));
bus.publish(evt);
}
@Override
public void fail(ErrorCode errorCode) {
logger.warn(errorCode.toString());
extpEmitter.failToAttach(svo, msg.getZoneUuid());
evt.setError(errf.instantiateErrorCode(BackupStorageErrors.ATTACH_ERROR, errorCode));
bus.publish(evt);
}
});
}
protected void handle(APIChangeBackupStorageStateMsg msg) {
APIChangeBackupStorageStateEvent evt = new APIChangeBackupStorageStateEvent(msg.getId());
BackupStorageState currState = self.getState();
BackupStorageStateEvent event = BackupStorageStateEvent.valueOf(msg.getStateEvent());
BackupStorageState nextState = AbstractBackupStorage.getNextState(currState, event);
try {
extpEmitter.preChange(self, event);
} catch (BackupStorageException e) {
evt.setError(errf.instantiateErrorCode(SysErrors.CHANGE_RESOURCE_STATE_ERROR, e.getMessage()));
bus.publish(evt);
return;
}
extpEmitter.beforeChange(self, event);
changeStateHook(event, nextState);
self.setState(nextState);
self = dbf.updateAndRefresh(self);
extpEmitter.afterChange(self, event, currState);
evt.setInventory(getSelfInventory());
bus.publish(evt);
}
protected void handle(APIDeleteBackupStorageMsg msg) {
final APIDeleteBackupStorageEvent evt = new APIDeleteBackupStorageEvent(msg.getId());
final String issuer = BackupStorageVO.class.getSimpleName();
final List<BackupStorageInventory> ctx = BackupStorageInventory.valueOf(Arrays.asList(self));
FlowChain chain = FlowChainBuilder.newSimpleFlowChain();
chain.setName(String.format("delete-backup-storage-%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();
}
protected void updateCapacity(Long totalCapacity, Long availableCapacity) {
if (totalCapacity != null && availableCapacity != null) {
self.setTotalCapacity(totalCapacity);
self.setAvailableCapacity(availableCapacity);
dbf.update(self);
}
}
protected void changeStatus(BackupStorageStatus status) {
if (status == self.getStatus()) {
return;
}
BackupStorageStatus oldStatus = self.getStatus();
self.setStatus(status);
dbf.update(self);
BackupStorageStatusChangedData d = new BackupStorageStatusChangedData();
d.setBackupStorageUuid(self.getUuid());
d.setNewStatus(status.toString());
d.setOldStatus(oldStatus.toString());
d.setInventory(BackupStorageInventory.valueOf(self));
evtf.fire(BackupStorageCanonicalEvents.BACKUP_STORAGE_STATUS_CHANGED, d);
logger.debug(String.format("change backup storage[uuid:%s] status to %s", self.getUuid(), status));
}
}