package org.zstack.storage.backup.sftp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.util.UriComponentsBuilder;
import org.zstack.core.CoreGlobalProperty;
import org.zstack.core.ansible.AnsibleFacade;
import org.zstack.core.ansible.AnsibleGlobalProperty;
import org.zstack.core.ansible.AnsibleRunner;
import org.zstack.core.ansible.SshFileMd5Checker;
import org.zstack.core.errorcode.ErrorFacade;
import org.zstack.core.timeout.ApiTimeoutManager;
import org.zstack.header.core.Completion;
import org.zstack.header.core.NoErrorCompletion;
import org.zstack.header.core.ReturnValueCompletion;
import org.zstack.header.errorcode.ErrorCode;
import org.zstack.header.errorcode.OperationFailureException;
import org.zstack.header.exception.CloudRuntimeException;
import org.zstack.header.image.ImageBackupStorageRefInventory;
import org.zstack.header.image.ImageInventory;
import org.zstack.header.message.APIMessage;
import org.zstack.header.message.Message;
import org.zstack.header.rest.JsonAsyncRESTCallback;
import org.zstack.header.rest.RESTFacade;
import org.zstack.header.storage.backup.*;
import org.zstack.header.storage.backup.BackupStorageErrors.Opaque;
import org.zstack.storage.backup.BackupStorageBase;
import org.zstack.storage.backup.BackupStoragePathMaker;
import org.zstack.storage.backup.sftp.SftpBackupStorageCommands.*;
import org.zstack.utils.CollectionUtils;
import org.zstack.utils.Utils;
import org.zstack.utils.function.Function;
import org.zstack.utils.logging.CLogger;
import org.zstack.utils.path.PathUtil;
import static org.zstack.core.Platform.operr;
import javax.persistence.Query;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
public class SftpBackupStorage extends BackupStorageBase {
private static final CLogger logger = Utils.getLogger(SftpBackupStorage.class);
@Autowired
protected RESTFacade restf;
@Autowired
private AnsibleFacade asf;
@Autowired
private ErrorFacade errf;
@Autowired
private ApiTimeoutManager timeoutManager;
@Autowired
private SftpBackupStorageMetaDataMaker metaDataMaker;
private String agentPackageName = SftpBackupStorageGlobalProperty.AGENT_PACKAGE_NAME;
public SftpBackupStorage(SftpBackupStorageVO vo) {
super(vo);
}
public String buildUrl(String subPath) {
UriComponentsBuilder ub = UriComponentsBuilder.newInstance();
ub.scheme(SftpBackupStorageGlobalProperty.AGENT_URL_SCHEME);
if (CoreGlobalProperty.UNIT_TEST_ON) {
ub.host("localhost");
} else {
ub.host(getSelf().getHostname());
}
ub.port(SftpBackupStorageGlobalProperty.AGENT_PORT);
if (!"".equals(SftpBackupStorageGlobalProperty.AGENT_URL_ROOT_PATH)) {
ub.path(SftpBackupStorageGlobalProperty.AGENT_URL_ROOT_PATH);
}
ub.path(subPath);
return ub.build().toUriString();
}
private SftpBackupStorageVO getSelf() {
return (SftpBackupStorageVO) self;
}
protected BackupStorageInventory getSelfInventory() {
return SftpBackupStorageInventory.valueOf(getSelf());
}
private class DownloadResult {
String md5sum;
long size;
long actualSize;
}
private void download(String url, String installPath, String uuid, final ReturnValueCompletion<DownloadResult> completion) {
try {
URI uri = new URI(url);
String scheme = uri.getScheme();
if (!SftpBackupStorageFactory.type.getSupportedSchemes().contains(scheme)) {
throw new OperationFailureException(operr("SftpBackupStorage doesn't support scheme[%s] in url[%s]", scheme, url));
}
DownloadCmd cmd = new DownloadCmd();
cmd.setUuid(self.getUuid());
cmd.setImageUuid(uuid);
cmd.setUrl(url);
cmd.setUrlScheme(scheme);
cmd.setInstallPath(installPath);
cmd.setTimeout(timeoutManager.getTimeout(cmd.getClass(), "3h"));
restf.asyncJsonPost(buildUrl(SftpBackupStorageConstant.DOWNLOAD_IMAGE_PATH), cmd, new JsonAsyncRESTCallback<DownloadResponse>(completion) {
@Override
public void fail(ErrorCode err) {
completion.fail(err);
}
@Override
public void success(DownloadResponse ret) {
if (ret.isSuccess()) {
DownloadResult res = new DownloadResult();
res.md5sum = ret.getMd5Sum();
res.size = ret.getSize();
res.actualSize = ret.getActualSize();
updateCapacity(ret.getTotalCapacity(), ret.getAvailableCapacity());
completion.success(res);
} else {
completion.fail(operr(ret.getError()));
}
}
@Override
public Class<DownloadResponse> getReturnClass() {
return DownloadResponse.class;
}
});
} catch (URISyntaxException e) {
throw new CloudRuntimeException(e);
}
}
@Override
protected void handle(final GetImageSizeOnBackupStorageMsg msg) {
final GetImageSizeOnBackupStorageReply reply = new GetImageSizeOnBackupStorageReply();
GetImageSizeCmd cmd = new GetImageSizeCmd();
cmd.uuid = self.getUuid();
cmd.imageUuid = msg.getImageUuid();
cmd.installPath = msg.getImageUrl();
restf.asyncJsonPost(buildUrl(SftpBackupStorageConstant.GET_IMAGE_SIZE), cmd,
new JsonAsyncRESTCallback<GetImageSizeRsp>(msg) {
@Override
public void fail(ErrorCode err) {
reply.setError(err);
bus.reply(msg, reply);
}
@Override
public void success(GetImageSizeRsp rsp) {
if (!rsp.isSuccess()) {
reply.setError(operr(rsp.getError()));
} else {
reply.setSize(rsp.size);
}
bus.reply(msg, reply);
}
@Override
public Class<GetImageSizeRsp> getReturnClass() {
return GetImageSizeRsp.class;
}
});
}
@Override
@Transactional
protected void handle(final DownloadImageMsg msg) {
final DownloadImageReply reply = new DownloadImageReply();
final ImageInventory iinv = msg.getImageInventory();
final String installPath = PathUtil.join(getSelf().getUrl(), BackupStoragePathMaker.makeImageInstallPath(iinv));
String sql = "update ImageBackupStorageRefVO set installPath = :installPath " +
"where backupStorageUuid = :bsUuid and imageUuid = :imageUuid";
Query q = dbf.getEntityManager().createQuery(sql);
q.setParameter("installPath", installPath);
q.setParameter("bsUuid", msg.getBackupStorageUuid());
q.setParameter("imageUuid", msg.getImageInventory().getUuid());
q.executeUpdate();
download(iinv.getUrl(), installPath, iinv.getUuid(), new ReturnValueCompletion<DownloadResult>(msg) {
@Override
public void success(DownloadResult res) {
reply.setInstallPath(installPath);
reply.setSize(res.size);
reply.setActualSize(res.actualSize);
reply.setMd5sum(res.md5sum);
bus.reply(msg, reply);
}
@Override
public void fail(ErrorCode errorCode) {
reply.setError(errorCode);
bus.reply(msg, reply);
}
});
}
@Override
protected void handle(final DownloadVolumeMsg msg) {
final DownloadVolumeReply reply = new DownloadVolumeReply();
final String installPath = PathUtil.join(getSelf().getUrl(), BackupStoragePathMaker.makeVolumeInstallPath(msg.getUrl(), msg.getVolume()));
download(msg.getUrl(), installPath, msg.getVolume().getUuid(), new ReturnValueCompletion<DownloadResult>(msg) {
@Override
public void success(DownloadResult res) {
reply.setInstallPath(installPath);
reply.setSize(res.size);
reply.setMd5sum(res.md5sum);
bus.reply(msg, reply);
}
@Override
public void fail(ErrorCode errorCode) {
reply.setError(errorCode);
bus.reply(msg, reply);
}
});
}
@Override
protected void connectHook(boolean newAdded, Completion completion) {
connect(new Completion(completion) {
@Override
public void success() {
if (!newAdded) {
String backupStorageUrl = getSelf().getUrl();
String backStorageHostName = getSelf().getHostname();
String backupStorageUuid = getSelf().getUuid();
SftpBackupStorageDumpMetadataInfo dumpInfo = new SftpBackupStorageDumpMetadataInfo();
dumpInfo.setDumpAllInfo(true);
dumpInfo.setBackupStorageUuid(backupStorageUuid);
dumpInfo.setBackupStorageUrl(backupStorageUrl);
dumpInfo.setBackupStorageHostname(backStorageHostName);
metaDataMaker.dumpImagesBackupStorageInfoToMetaDataFile(dumpInfo);
}
completion.success();
}
@Override
public void fail(ErrorCode errorCode) {
completion.fail(errorCode);
}
});
}
@Override
protected void pingHook(final Completion completion) {
final PingCmd cmd = new PingCmd();
cmd.uuid = self.getUuid();
restf.asyncJsonPost(buildUrl(SftpBackupStorageConstant.PING_PATH), cmd, new JsonAsyncRESTCallback<PingResponse>(completion) {
@Override
public void fail(ErrorCode err) {
completion.fail(err);
}
@Override
public void success(PingResponse ret) {
if (ret.isSuccess() && !self.getUuid().equals(ret.getUuid())) {
ErrorCode err = operr("the uuid of sftpBackupStorage agent changed[expected:%s, actual:%s], it's most likely" +
" the agent was manually restarted. Issue a reconnect to sync the status", self.getUuid(), ret.getUuid());
err.putToOpaque(Opaque.RECONNECT_AGENT.toString(), true);
completion.fail(err);
} else if (ret.isSuccess()) {
completion.success();
} else {
completion.fail(operr(ret.getError()));
}
}
@Override
public Class<PingResponse> getReturnClass() {
return PingResponse.class;
}
});
}
private void continueConnect(final Completion complete) {
restf.echo(buildUrl(SftpBackupStorageConstant.ECHO_PATH), new Completion(complete) {
@Override
public void success() {
String url = buildUrl(SftpBackupStorageConstant.CONNECT_PATH);
ConnectCmd cmd = new ConnectCmd();
cmd.setUuid(self.getUuid());
cmd.setStoragePath(getSelf().getUrl());
ConnectResponse rsp = restf.syncJsonPost(url, cmd, ConnectResponse.class);
if (!rsp.isSuccess()) {
ErrorCode err = operr("unable to connect to SimpleHttpBackupStorage[url:%s], because %s", url, rsp.getError());
complete.fail(err);
return;
}
updateCapacity(rsp.getTotalCapacity(), rsp.getAvailableCapacity());
logger.debug(String.format("connected to backup storage[uuid:%s, name:%s, total capacity:%sG, available capacity: %sG",
getSelf().getUuid(), getSelf().getName(), rsp.getTotalCapacity(), rsp.getAvailableCapacity()));
complete.success();
}
@Override
public void fail(ErrorCode errorCode) {
complete.fail(errorCode);
}
});
}
private void connect(final Completion complete) {
if (CoreGlobalProperty.UNIT_TEST_ON) {
continueConnect(complete);
return;
}
SshFileMd5Checker checker = new SshFileMd5Checker();
checker.setTargetIp(getSelf().getHostname());
checker.setUsername(getSelf().getUsername());
checker.setPassword(getSelf().getPassword());
checker.setSshPort(getSelf().getSshPort());
checker.addSrcDestPair(SshFileMd5Checker.ZSTACKLIB_SRC_PATH, String.format("/var/lib/zstack/sftpbackupstorage/package/%s", AnsibleGlobalProperty.ZSTACKLIB_PACKAGE_NAME));
checker.addSrcDestPair(PathUtil.findFileOnClassPath(String.format("ansible/sftpbackupstorage/%s", agentPackageName), true).getAbsolutePath(),
String.format("/var/lib/zstack/sftpbackupstorage/package/%s", agentPackageName));
AnsibleRunner runner = new AnsibleRunner();
runner.installChecker(checker);
runner.setPassword(getSelf().getPassword());
runner.setUsername(getSelf().getUsername());
runner.setTargetIp(getSelf().getHostname());
runner.setSshPort(getSelf().getSshPort());
runner.setAgentPort(SftpBackupStorageGlobalProperty.AGENT_PORT);
runner.setPlayBookName(SftpBackupStorageConstant.ANSIBLE_PLAYBOOK_NAME);
runner.putArgument("pkg_sftpbackupstorage", agentPackageName);
runner.run(new Completion(complete) {
@Override
public void success() {
continueConnect(complete);
}
@Override
public void fail(ErrorCode errorCode) {
complete.fail(errorCode);
}
});
}
@Override
public List<ImageInventory> scanImages() {
return null;
}
@Override
protected void handleApiMessage(APIMessage msg) {
if (msg instanceof APIReconnectSftpBackupStorageMsg) {
handle((APIReconnectSftpBackupStorageMsg) msg);
} else {
super.handleApiMessage(msg);
}
}
@Override
protected void handleLocalMessage(Message msg) throws URISyntaxException {
if (msg instanceof GetSftpBackupStorageDownloadCredentialMsg) {
handle((GetSftpBackupStorageDownloadCredentialMsg) msg);
} else {
super.handleLocalMessage(msg);
}
}
@Override
protected void handle(final DeleteBitsOnBackupStorageMsg msg) {
final DeleteBitsOnBackupStorageReply reply = new DeleteBitsOnBackupStorageReply();
DeleteCmd cmd = new DeleteCmd();
cmd.uuid = self.getUuid();
cmd.setInstallUrl(msg.getInstallPath());
restf.asyncJsonPost(buildUrl(SftpBackupStorageConstant.DELETE_PATH), cmd, new JsonAsyncRESTCallback<DeleteResponse>(msg) {
@Override
public void fail(ErrorCode err) {
reply.setError(err);
bus.reply(msg, reply);
}
@Override
public void success(DeleteResponse ret) {
if (!ret.isSuccess()) {
logger.warn(String.format("failed to delete bits[%s], schedule clean up, %s",
msg.getInstallPath(), ret.getError()));
//TODO GC
} else {
updateCapacity(ret.getTotalCapacity(), ret.getAvailableCapacity());
}
bus.reply(msg, reply);
}
@Override
public Class<DeleteResponse> getReturnClass() {
return DeleteResponse.class;
}
});
}
@Override
protected void handle(BackupStorageAskInstallPathMsg msg) {
BackupStorageAskInstallPathReply reply = new BackupStorageAskInstallPathReply();
String installPath = PathUtil.join(self.getUrl(), BackupStoragePathMaker.makeImageInstallPath(msg.getImageUuid(), msg.getImageMediaType()));
reply.setInstallPath(installPath);
bus.reply(msg, reply);
}
@Override
protected void handle(final SyncImageSizeOnBackupStorageMsg msg) {
final SyncImageSizeOnBackupStorageReply reply = new SyncImageSizeOnBackupStorageReply();
ImageInventory image = msg.getImage();
GetImageSizeCmd cmd = new GetImageSizeCmd();
cmd.imageUuid = image.getUuid();
cmd.uuid = self.getUuid();
ImageBackupStorageRefInventory ref = CollectionUtils.find(image.getBackupStorageRefs(), new Function<ImageBackupStorageRefInventory, ImageBackupStorageRefInventory>() {
@Override
public ImageBackupStorageRefInventory call(ImageBackupStorageRefInventory arg) {
return arg.getBackupStorageUuid().equals(self.getUuid()) ? arg : null;
}
});
if (ref == null) {
throw new CloudRuntimeException(String.format("cannot find ImageBackupStorageRefInventory of image[uuid:%s] for the backup storage[uuid:%s]",
image.getUuid(), self.getUuid()));
}
cmd.installPath = ref.getInstallPath();
restf.asyncJsonPost(buildUrl(SftpBackupStorageConstant.GET_IMAGE_SIZE), cmd, new JsonAsyncRESTCallback<GetImageSizeRsp>(msg) {
@Override
public void fail(ErrorCode err) {
reply.setError(err);
bus.reply(msg, reply);
}
@Override
public void success(GetImageSizeRsp rsp) {
if (!rsp.isSuccess()) {
reply.setError(operr(rsp.getError()));
} else {
reply.setActualSize(rsp.actualSize);
reply.setSize(rsp.size);
}
bus.reply(msg, reply);
}
@Override
public Class<GetImageSizeRsp> getReturnClass() {
return GetImageSizeRsp.class;
}
});
}
private void handle(final GetSftpBackupStorageDownloadCredentialMsg msg) {
final GetSftpBackupStorageDownloadCredentialReply reply = new GetSftpBackupStorageDownloadCredentialReply();
String key = asf.getPrivateKey();
reply.setHostname(getSelf().getHostname());
reply.setUsername(getSelf().getUsername());
reply.setSshKey(key);
reply.setSshPort(getSelf().getSshPort());
bus.reply(msg, reply);
}
private void handle(final APIReconnectSftpBackupStorageMsg msg) {
final APIReconnectSftpBackupStorageEvent evt = new APIReconnectSftpBackupStorageEvent(msg.getId());
connect(new Completion(msg) {
@Override
public void success() {
changeStatus(BackupStorageStatus.Connected, new NoErrorCompletion(msg) {
@Override
public void done() {
self = dbf.reload(self);
evt.setInventory(SftpBackupStorageInventory.valueOf(getSelf()));
bus.publish(evt);
}
});
}
@Override
public void fail(ErrorCode errorCode) {
evt.setError(errf.instantiateErrorCode(SftpBackupStorageErrors.RECONNECT_ERROR, errorCode));
bus.publish(evt);
}
});
}
@Override
protected BackupStorageVO updateBackupStorage(APIUpdateBackupStorageMsg msg) {
if (!(msg instanceof APIUpdateSftpBackupStorageMsg)) {
return super.updateBackupStorage(msg);
}
SftpBackupStorageVO vo = (SftpBackupStorageVO) super.updateBackupStorage(msg);
vo = vo == null ? getSelf() : vo;
APIUpdateSftpBackupStorageMsg umsg = (APIUpdateSftpBackupStorageMsg) msg;
if (umsg.getUsername() != null) {
vo.setUsername(umsg.getUsername());
}
if (umsg.getPassword() != null) {
vo.setPassword(umsg.getPassword());
}
if (umsg.getHostname() != null) {
vo.setHostname(umsg.getHostname());
}
if (umsg.getSshPort() != null && umsg.getSshPort() > 0 && umsg.getSshPort() <= 65535) {
vo.setSshPort(umsg.getSshPort());
}
return vo;
}
}