diff --git a/build/pom.xml b/build/pom.xml index 91ac875a6c2..63cc249a444 100755 --- a/build/pom.xml +++ b/build/pom.xml @@ -523,6 +523,11 @@ zce-x-plugin ${project.version} + + org.zstack + zmigrate-plugin + ${project.version} + org.zstack zsv diff --git a/conf/errorCodes/ZMigratePlugin.xml b/conf/errorCodes/ZMigratePlugin.xml new file mode 100644 index 00000000000..11dbb8be2bc --- /dev/null +++ b/conf/errorCodes/ZMigratePlugin.xml @@ -0,0 +1,79 @@ + + ZMigrate + + + 1000 + ZMigrate generic error + + + + 1001 + Failed to create account in ZMigrate + + + + 1002 + Failed to verify platform connection + + + + 1003 + Failed to register ZSV to ZMigrate + + + + 1004 + Failed to verify gateway connection + + + + 1005 + Failed to register gateway to ZMigrate + + + + 1006 + Failed to get ZMigrate management server information + + + + 1007 + Failed to get platform information + + + + 1008 + Failed to get gateway server information + + + + 1009 + Failed to get migration jobs + + + + 1010 + Failed to get licenses from ZMigrate + + + + 1011 + Failed to get encrypt key + + + + 1012 + Failed to get gateway hostname + + + + 1013 + Failed to export activation infos from ZMigrate + + + + 1014 + Failed to import activation package to ZMigrate + + + diff --git a/header/src/main/java/org/zstack/header/storage/backup/CancelDownloadFileOnBackupStorageHostMsg.java b/header/src/main/java/org/zstack/header/storage/backup/CancelDownloadFileOnBackupStorageHostMsg.java new file mode 100644 index 00000000000..5d4e79d4f6e --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/backup/CancelDownloadFileOnBackupStorageHostMsg.java @@ -0,0 +1,25 @@ +package org.zstack.header.storage.backup; + +import org.zstack.header.message.CancelMessage; + +public class CancelDownloadFileOnBackupStorageHostMsg extends CancelMessage implements BackupStorageMessage { + private String backupStorageUuid; + private String backupStorageHostUuid; + + @Override + public String getBackupStorageUuid() { + return backupStorageUuid; + } + + public void setBackupStorageUuid(String backupStorageUuid) { + this.backupStorageUuid = backupStorageUuid; + } + + public String getBackupStorageHostUuid() { + return backupStorageHostUuid; + } + + public void setBackupStorageHostUuid(String backupStorageHostUuid) { + this.backupStorageHostUuid = backupStorageHostUuid; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/backup/CancelDownloadFileOnBackupStorageHostReply.java b/header/src/main/java/org/zstack/header/storage/backup/CancelDownloadFileOnBackupStorageHostReply.java new file mode 100644 index 00000000000..53f1c07b543 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/backup/CancelDownloadFileOnBackupStorageHostReply.java @@ -0,0 +1,6 @@ +package org.zstack.header.storage.backup; + +import org.zstack.header.message.MessageReply; + +public class CancelDownloadFileOnBackupStorageHostReply extends MessageReply { +} diff --git a/header/src/main/java/org/zstack/header/storage/backup/DeleteFilesOnBackupStorageHostMsg.java b/header/src/main/java/org/zstack/header/storage/backup/DeleteFilesOnBackupStorageHostMsg.java new file mode 100644 index 00000000000..e69d38ad2d8 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/backup/DeleteFilesOnBackupStorageHostMsg.java @@ -0,0 +1,37 @@ +package org.zstack.header.storage.backup; + +import org.zstack.header.message.NeedReplyMessage; + +import java.util.ArrayList; +import java.util.List; + +public class DeleteFilesOnBackupStorageHostMsg extends NeedReplyMessage implements BackupStorageMessage { + private String backupStorageUuid; + private String backupStorageHostUuid; + private List filePaths = new ArrayList<>(); + + @Override + public String getBackupStorageUuid() { + return backupStorageUuid; + } + + public void setBackupStorageUuid(String backupStorageUuid) { + this.backupStorageUuid = backupStorageUuid; + } + + public String getBackupStorageHostUuid() { + return backupStorageHostUuid; + } + + public void setBackupStorageHostUuid(String backupStorageHostUuid) { + this.backupStorageHostUuid = backupStorageHostUuid; + } + + public List getFilePaths() { + return filePaths; + } + + public void setFilePaths(List filePaths) { + this.filePaths = filePaths == null ? new ArrayList<>() : filePaths; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/backup/DeleteFilesOnBackupStorageHostReply.java b/header/src/main/java/org/zstack/header/storage/backup/DeleteFilesOnBackupStorageHostReply.java new file mode 100644 index 00000000000..fc79c452951 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/backup/DeleteFilesOnBackupStorageHostReply.java @@ -0,0 +1,6 @@ +package org.zstack.header.storage.backup; + +import org.zstack.header.message.MessageReply; + +public class DeleteFilesOnBackupStorageHostReply extends MessageReply { +} diff --git a/header/src/main/java/org/zstack/header/storage/backup/GetFileDownloadProgressFromBackupStorageHostMsg.java b/header/src/main/java/org/zstack/header/storage/backup/GetFileDownloadProgressFromBackupStorageHostMsg.java new file mode 100644 index 00000000000..738bc98d3da --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/backup/GetFileDownloadProgressFromBackupStorageHostMsg.java @@ -0,0 +1,34 @@ +package org.zstack.header.storage.backup; + +import org.zstack.header.message.NeedReplyMessage; + +public class GetFileDownloadProgressFromBackupStorageHostMsg extends NeedReplyMessage implements BackupStorageMessage { + private String backupStorageUuid; + private String backupStorageHostUuid; + private String taskUuid; + + @Override + public String getBackupStorageUuid() { + return backupStorageUuid; + } + + public void setBackupStorageUuid(String backupStorageUuid) { + this.backupStorageUuid = backupStorageUuid; + } + + public String getBackupStorageHostUuid() { + return backupStorageHostUuid; + } + + public void setBackupStorageHostUuid(String backupStorageHostUuid) { + this.backupStorageHostUuid = backupStorageHostUuid; + } + + public String getTaskUuid() { + return taskUuid; + } + + public void setTaskUuid(String taskUuid) { + this.taskUuid = taskUuid; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/backup/GetFileDownloadProgressFromBackupStorageHostReply.java b/header/src/main/java/org/zstack/header/storage/backup/GetFileDownloadProgressFromBackupStorageHostReply.java new file mode 100644 index 00000000000..a791d95e27a --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/backup/GetFileDownloadProgressFromBackupStorageHostReply.java @@ -0,0 +1,97 @@ +package org.zstack.header.storage.backup; + +import org.zstack.header.message.MessageReply; + +public class GetFileDownloadProgressFromBackupStorageHostReply extends MessageReply { + private boolean completed; + private int progress; + + private long size; + private long actualSize; + private long downloadSize; + private String installPath; + private long lastOpTime; + private boolean supportSuspend; + private String md5sum; + private String format; + + public boolean isCompleted() { + return completed; + } + + public void setCompleted(boolean completed) { + this.completed = completed; + } + + public int getProgress() { + return progress; + } + + public void setProgress(int progress) { + this.progress = progress; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public long getActualSize() { + return actualSize; + } + + public void setActualSize(long actualSize) { + this.actualSize = actualSize; + } + + public String getInstallPath() { + return installPath; + } + + public void setInstallPath(String installPath) { + this.installPath = installPath; + } + + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } + + public long getLastOpTime() { + return lastOpTime; + } + + public void setLastOpTime(long lastOpTime) { + this.lastOpTime = lastOpTime; + } + + public long getDownloadSize() { + return downloadSize; + } + + public void setDownloadSize(long downloadSize) { + this.downloadSize = downloadSize; + } + + public boolean isSupportSuspend() { + return supportSuspend; + } + + public void setSupportSuspend(boolean supportSuspend) { + this.supportSuspend = supportSuspend; + } + + public String getMd5sum() { + return md5sum; + } + + public void setMd5sum(String md5sum) { + this.md5sum = md5sum; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/backup/SoftwareUpgradePackageDeployMsg.java b/header/src/main/java/org/zstack/header/storage/backup/SoftwareUpgradePackageDeployMsg.java new file mode 100644 index 00000000000..997450a455d --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/backup/SoftwareUpgradePackageDeployMsg.java @@ -0,0 +1,99 @@ +package org.zstack.header.storage.backup; + +import org.zstack.header.log.NoLogging; +import org.zstack.header.message.NeedReplyMessage; + +public class SoftwareUpgradePackageDeployMsg extends NeedReplyMessage implements BackupStorageMessage { + private String backupStorageUuid; + private String backupStorageHostUuid; + private String upgradePackagePath; + private String upgradePackageTargetPath; + private int targetHostSshPort; + private String targetHostSshUsername; + @NoLogging + private String targetHostSshPassword; + private String targetHostIp; + private String upgradeScriptPath; + private String softwareType; + + @Override + public String getBackupStorageUuid() { + return backupStorageUuid; + } + + public void setBackupStorageUuid(String backupStorageUuid) { + this.backupStorageUuid = backupStorageUuid; + } + + public String getBackupStorageHostUuid() { + return backupStorageHostUuid; + } + + public void setBackupStorageHostUuid(String backupStorageHostUuid) { + this.backupStorageHostUuid = backupStorageHostUuid; + } + + public String getUpgradePackageTargetPath() { + return upgradePackageTargetPath; + } + + public void setUpgradePackageTargetPath(String upgradePackageTargetPath) { + this.upgradePackageTargetPath = upgradePackageTargetPath; + } + + public String getUpgradePackagePath() { + return upgradePackagePath; + } + + public void setUpgradePackagePath(String upgradePackagePath) { + this.upgradePackagePath = upgradePackagePath; + } + + public int getTargetHostSshPort() { + return targetHostSshPort; + } + + public void setTargetHostSshPort(int targetHostSshPort) { + this.targetHostSshPort = targetHostSshPort; + } + + public String getTargetHostSshUsername() { + return targetHostSshUsername; + } + + public void setTargetHostSshUsername(String targetHostSshUsername) { + this.targetHostSshUsername = targetHostSshUsername; + } + + public String getTargetHostSshPassword() { + return targetHostSshPassword; + } + + public void setTargetHostSshPassword(String targetHostSshPassword) { + this.targetHostSshPassword = targetHostSshPassword; + } + + public String getTargetHostIp() { + return targetHostIp; + } + + public void setTargetHostIp(String targetHostIp) { + this.targetHostIp = targetHostIp; + } + + public String getUpgradeScriptPath() { + return upgradeScriptPath; + } + + public void setUpgradeScriptPath(String upgradeScriptPath) { + this.upgradeScriptPath = upgradeScriptPath; + } + + public String getSoftwareType() { + return softwareType; + } + + public void setSoftwareType(String softwareType) { + this.softwareType = softwareType; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/backup/SoftwareUpgradePackageDeployReply.java b/header/src/main/java/org/zstack/header/storage/backup/SoftwareUpgradePackageDeployReply.java new file mode 100644 index 00000000000..49a6a129ec8 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/backup/SoftwareUpgradePackageDeployReply.java @@ -0,0 +1,6 @@ +package org.zstack.header.storage.backup; + +import org.zstack.header.message.MessageReply; + +public class SoftwareUpgradePackageDeployReply extends MessageReply { +} diff --git a/header/src/main/java/org/zstack/header/storage/backup/UnzipFileOnBackupStorageHostMsg.java b/header/src/main/java/org/zstack/header/storage/backup/UnzipFileOnBackupStorageHostMsg.java new file mode 100644 index 00000000000..0cbd91848df --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/backup/UnzipFileOnBackupStorageHostMsg.java @@ -0,0 +1,34 @@ +package org.zstack.header.storage.backup; + +import org.zstack.header.message.NeedReplyMessage; + +public class UnzipFileOnBackupStorageHostMsg extends NeedReplyMessage implements BackupStorageMessage { + private String backupStorageUuid; + private String backupStorageHostUuid; + private String installPath; + + @Override + public String getBackupStorageUuid() { + return backupStorageUuid; + } + + public void setBackupStorageUuid(String backupStorageUuid) { + this.backupStorageUuid = backupStorageUuid; + } + + public String getBackupStorageHostUuid() { + return backupStorageHostUuid; + } + + public void setBackupStorageHostUuid(String backupStorageHostUuid) { + this.backupStorageHostUuid = backupStorageHostUuid; + } + + public String getInstallPath() { + return installPath; + } + + public void setInstallPath(String installPath) { + this.installPath = installPath; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/backup/UnzipFileOnBackupStorageHostReply.java b/header/src/main/java/org/zstack/header/storage/backup/UnzipFileOnBackupStorageHostReply.java new file mode 100644 index 00000000000..8064a18c51d --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/backup/UnzipFileOnBackupStorageHostReply.java @@ -0,0 +1,26 @@ +package org.zstack.header.storage.backup; + +import org.zstack.header.message.MessageReply; + +import java.util.Map; + +public class UnzipFileOnBackupStorageHostReply extends MessageReply { + private String unzipInstallPath; + private Map fileSizes; + + public String getUnzipInstallPath() { + return unzipInstallPath; + } + + public void setUnzipInstallPath(String unzipInstallPath) { + this.unzipInstallPath = unzipInstallPath; + } + + public Map getFileSizes() { + return fileSizes; + } + + public void setFileSizes(Map fileSizes) { + this.fileSizes = fileSizes; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/backup/UploadFileToBackupStorageHostMsg.java b/header/src/main/java/org/zstack/header/storage/backup/UploadFileToBackupStorageHostMsg.java new file mode 100644 index 00000000000..4f2d2b173c4 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/backup/UploadFileToBackupStorageHostMsg.java @@ -0,0 +1,45 @@ +package org.zstack.header.storage.backup; + +import org.zstack.header.log.NoLogging; +import org.zstack.header.message.NeedReplyMessage; + +public class UploadFileToBackupStorageHostMsg extends NeedReplyMessage implements BackupStorageMessage { + private String backupStorageUuid; + private String taskUuid; + @NoLogging(type = NoLogging.Type.Uri) + private String url; + private String installPath; + + @Override + public String getBackupStorageUuid() { + return backupStorageUuid; + } + + public void setBackupStorageUuid(String backupStorageUuid) { + this.backupStorageUuid = backupStorageUuid; + } + + public String getTaskUuid() { + return taskUuid; + } + + public void setTaskUuid(String taskUuid) { + this.taskUuid = taskUuid; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getInstallPath() { + return installPath; + } + + public void setInstallPath(String installPath) { + this.installPath = installPath; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/backup/UploadFileToBackupStorageHostReply.java b/header/src/main/java/org/zstack/header/storage/backup/UploadFileToBackupStorageHostReply.java new file mode 100644 index 00000000000..35477e98c96 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/backup/UploadFileToBackupStorageHostReply.java @@ -0,0 +1,53 @@ +package org.zstack.header.storage.backup; + +import org.zstack.header.log.NoLogging; +import org.zstack.header.message.MessageReply; + +public class UploadFileToBackupStorageHostReply extends MessageReply { + private String md5sum; + private long size; + private String format; + @NoLogging(type = NoLogging.Type.Uri) + private String directUploadUrl; + private String backupStorageHostUuid; + + public String getMd5sum() { + return md5sum; + } + + public void setMd5sum(String md5sum) { + this.md5sum = md5sum; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } + + public String getDirectUploadUrl() { + return directUploadUrl; + } + + public void setDirectUploadUrl(String directUploadUrl) { + this.directUploadUrl = directUploadUrl; + } + + public String getBackupStorageHostUuid() { + return backupStorageHostUuid; + } + + public void setBackupStorageHostUuid(String backupStorageHostUuid) { + this.backupStorageHostUuid = backupStorageHostUuid; + } +} diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageBase.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageBase.java index e24fdc21e11..4ad5df7bc50 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageBase.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageBase.java @@ -1,5 +1,6 @@ package org.zstack.storage.ceph.backup; +import org.apache.commons.lang.StringUtils; import org.apache.logging.log4j.ThreadContext; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowired; @@ -19,6 +20,7 @@ import org.zstack.core.thread.ChainTask; import org.zstack.core.thread.SyncTaskChain; import org.zstack.core.thread.SyncThread; +import org.zstack.core.timeout.ApiTimeoutManager; import org.zstack.core.workflow.FlowChainBuilder; import org.zstack.core.workflow.ShareFlow; import org.zstack.header.Constants; @@ -47,10 +49,13 @@ import org.zstack.utils.Utils; import org.zstack.utils.gson.JSONObjectUtil; import org.zstack.utils.logging.CLogger; +import org.zstack.utils.network.NetworkUtils; +import org.zstack.utils.path.RemotePathValidator; import javax.persistence.Tuple; import javax.persistence.TypedQuery; import java.io.Serializable; +import java.net.URI; import java.net.URISyntaxException; import java.util.*; import java.util.concurrent.TimeUnit; @@ -91,6 +96,8 @@ void unlock() { protected RESTFacade restf; @Autowired protected CephBackupStorageMetaDataMaker metaDataMaker; + @Autowired + private ApiTimeoutManager timeoutManager; public enum PingOperationFailure { UnableToCreateFile, @@ -659,6 +666,97 @@ public void setCancellationApiId(String cancellationApiId) { } } + public static class DownloadFileCmd extends AgentCommand implements HasThreadContext, Serializable { + public String taskUuid; + public String installPath; + @NoLogging(type = NoLogging.Type.Uri) + public String url; + @NoLogging(type = NoLogging.Type.Uri) + public String urlScheme; + public long timeout; + @NoLogging(type = NoLogging.Type.Uri) + public String sendCommandUrl; + } + + public static class DownloadFileResponse extends AgentResponse { + public String md5sum; + public long size; + public String format; + } + + public static class DeleteFilesCmd extends AgentCommand implements HasThreadContext, Serializable { + public List filePaths; + } + + public static class DeleteFilesResponse extends AgentResponse { + } + + public static class UploadFileCmd extends AgentCommand implements HasThreadContext, Serializable { + public String taskUuid; + public String installPath; + @NoLogging(type = NoLogging.Type.Uri) + public String url; + public long timeout; + } + + public static class UploadFileResponse extends AgentResponse { + public String directUploadUrl; + } + + public static class GetDownloadFileProgressCmd extends AgentCommand implements HasThreadContext, Serializable { + public String taskUuid; + } + + public static class GetDownloadFileProgressResponse extends AgentResponse { + public boolean completed; + public int progress; + public long size; + public long actualSize; + public String installPath; + public String format; + public long lastOpTime; + public long downloadSize; + public String md5sum; + public boolean supportSuspend; + } + + public static class UnzipFileCmd extends AgentCommand implements HasThreadContext, Serializable { + public String installPath; + } + + public static class UnzipFileResponse extends AgentResponse { + public String unzipInstallPath; + public Map fileSizes; + } + + /** + * Command to deploy a software upgrade package from a Ceph backup storage host + * to a target host via SCP/SSH. + * + * NOTE: targetHostSshPassword is the Base64-encoded password (not plaintext), + * and is annotated with @NoLogging so it will never appear in logs. + * The HTTP transport to the Ceph agent is within the trusted management network. + * + * TODO(security): The SSH password is transmitted over HTTP (management network) as Base64. + * While @NoLogging prevents log leakage and the management network is considered trusted, + * this is still vulnerable to network sniffing. Consider migrating to HTTPS transport + * or pushing password handling down to the agent side (e.g., vault/key-based auth). + */ + public static class SoftwareUpgradePackageCmd extends AgentCommand implements HasThreadContext, Serializable { + public String upgradePackagePath; + public String upgradePackageTargetPath; + public String upgradeScriptPath; + public String softwareType; + public int targetHostSshPort; + public String targetHostSshUsername; + @NoLogging + public String targetHostSshPassword; + public String targetHostIp; + } + + public static class SoftwareUpgradePackageResponse extends AgentResponse { + } + // common response of storage migration public static class StorageMigrationRsp extends AgentResponse { } @@ -680,6 +778,13 @@ public static class StorageMigrationRsp extends AgentResponse { public static final String GET_LOCAL_FILE_SIZE = "/ceph/backupstorage/getlocalfilesize"; public static final String CEPH_TO_CEPH_MIGRATE_IMAGE_PATH = "/ceph/backupstorage/image/migrate"; + public static final String FILE_DOWNLOAD_PATH = "/ceph/file/download"; + public static final String FILE_UPLOAD_PATH = "/ceph/file/upload"; + public static final String FILE_DOWNLOAD_PROGRESS_PATH = "/ceph/file/progress"; + public static final String DELETE_FILES_PATH = "/ceph/files/delete"; + public static final String UNZIP_FILE_PATH = "/ceph/file/unzip"; + public static final String SOFTWARE_UPGRADE_PACKAGE_DEPLOY_PATH = "/ceph/upgrade/deploy"; + protected String makeImageInstallPath(String imageUuid) { return String.format("ceph://%s/%s", getSelf().getPoolName(), imageUuid); } @@ -2021,4 +2126,377 @@ protected void handle(CalculateImageHashOnBackupStorageMsg msg) { private void doRestoreImagesBackupStorageMetadataToDatabase(RestoreImagesBackupStorageMetadataToDatabaseMsg msg) { metaDataMaker.restoreImagesBackupStorageMetadataToDatabase(msg.getImagesMetadata(), msg.getBackupStorageUuid()); } + + /** + * Find a specific Ceph mon by its UUID. + * @throws OperationFailureException if the mon cannot be found + */ + private CephBackupStorageMonVO findMonByUuid(String backupStorageHostUuid) { + if (backupStorageHostUuid == null) { + throw new OperationFailureException(operr("backup storage host uuid is null")); + } + + Set mons = getSelf().getMons(); + if (mons == null || mons.isEmpty()) { + throw new OperationFailureException(operr("backup storage [%s] has no mon", getSelf().getName())); + } + + return mons.stream() + .filter(m -> m.getUuid().equals(backupStorageHostUuid)).findAny() + .orElseThrow(() -> new OperationFailureException( + operr("failed to find mon with uuid [%s]", backupStorageHostUuid))); + } + + @Override + protected void handle(final UploadFileToBackupStorageHostMsg msg) { + UploadFileToBackupStorageHostReply reply = new UploadFileToBackupStorageHostReply(); + if (StringUtils.isEmpty(msg.getUrl())) { + reply.setError(operr("url cannot be null or empty")); + bus.reply(msg, reply); + return; + } + + // Validate installPath to prevent path traversal and injection attacks. + if (msg.getInstallPath() != null) { + String pathErr = RemotePathValidator.validateRemotePath(msg.getInstallPath(), "installPath"); + if (pathErr != null) { + reply.setError(operr(pathErr)); + bus.reply(msg, reply); + return; + } + } + + // "upload://" scheme: the caller will push file data directly to the agent's upload endpoint. + // The agent returns a directUploadUrl that the caller uses for the actual data transfer. + if (msg.getUrl().startsWith("upload://")) { + UploadFileCmd cmd = new UploadFileCmd(); + cmd.url = msg.getUrl(); + cmd.installPath = msg.getInstallPath(); + cmd.timeout = timeoutManager.getTimeout(); + cmd.taskUuid = msg.getTaskUuid(); + httpCall(FILE_UPLOAD_PATH, cmd, UploadFileResponse.class, new ReturnValueCompletion(msg) { + @Override + public void fail(ErrorCode err) { + reply.setError(err); + bus.reply(msg, reply); + } + + @Override + public void success(UploadFileResponse rsp) { + reply.setDirectUploadUrl(rsp.directUploadUrl); + reply.setBackupStorageHostUuid(rsp.handleMon.getMonUuid()); + bus.reply(msg, reply); + } + }); + return; + } + + // Other URL schemes (http://, https://, ftp://, etc.): the agent pulls the file + // from the given URL. Used for remote download scenarios where the file is + // hosted on an accessible server. + DownloadFileCmd cmd = new DownloadFileCmd(); + cmd.url = msg.getUrl(); + cmd.installPath = msg.getInstallPath(); + cmd.timeout = timeoutManager.getTimeout(); + cmd.taskUuid = msg.getTaskUuid(); + cmd.sendCommandUrl = restf.getSendCommandUrl(); + + String[] urlResult = RemotePathValidator.validateAndExtractUrlScheme(msg.getUrl()); + if (urlResult[0] != null) { + reply.setError(operr(urlResult[0])); + bus.reply(msg, reply); + return; + } + cmd.urlScheme = urlResult[1]; + + httpCall(FILE_DOWNLOAD_PATH, cmd, DownloadFileResponse.class, new ReturnValueCompletion(msg) { + @Override + public void fail(ErrorCode err) { + reply.setError(err); + bus.reply(msg, reply); + } + + @Override + public void success(DownloadFileResponse rsp) { + reply.setMd5sum(rsp.md5sum); + reply.setSize(rsp.size); + reply.setFormat(rsp.format); + reply.setBackupStorageHostUuid(rsp.handleMon.getMonUuid()); + bus.reply(msg, reply); + } + }); + } + + @Override + protected void handle(final UnzipFileOnBackupStorageHostMsg msg) { + UnzipFileOnBackupStorageHostReply reply = new UnzipFileOnBackupStorageHostReply(); + + if (StringUtils.isEmpty(msg.getInstallPath())) { + reply.setError(operr("installPath cannot be null or empty")); + bus.reply(msg, reply); + return; + } + + String pathErr = RemotePathValidator.validateRemotePath(msg.getInstallPath(), "installPath"); + if (pathErr != null) { + reply.setError(operr(pathErr)); + bus.reply(msg, reply); + return; + } + + CephBackupStorageMonVO mon; + try { + mon = findMonByUuid(msg.getBackupStorageHostUuid()); + } catch (OperationFailureException e) { + reply.setError(e.getErrorCode()); + bus.reply(msg, reply); + return; + } + + UnzipFileCmd cmd = new UnzipFileCmd(); + cmd.uuid = self.getUuid(); + cmd.fsid = getSelf().getFsid(); + cmd.installPath = msg.getInstallPath(); + + CephBackupStorageMonBase monBase = new CephBackupStorageMonBase(mon); + monBase.httpCall(UNZIP_FILE_PATH, cmd, UnzipFileResponse.class, new ReturnValueCompletion(msg) { + @Override + public void fail(ErrorCode err) { + reply.setError(err); + bus.reply(msg, reply); + } + + @Override + public void success(UnzipFileResponse rsp) { + reply.setUnzipInstallPath(rsp.unzipInstallPath); + reply.setFileSizes(rsp.fileSizes); + bus.reply(msg, reply); + } + }); + } + + @Override + protected void handle(final DeleteFilesOnBackupStorageHostMsg msg) { + DeleteFilesOnBackupStorageHostReply reply = new DeleteFilesOnBackupStorageHostReply(); + + if (msg.getFilePaths() == null || msg.getFilePaths().isEmpty()) { + bus.reply(msg, reply); + return; + } + + // Validate each file path to prevent path traversal and injection attacks. + String filePathErr = RemotePathValidator.validateFilePaths(msg.getFilePaths()); + if (filePathErr != null) { + reply.setError(operr(filePathErr)); + bus.reply(msg, reply); + return; + } + + CephBackupStorageMonVO mon; + try { + mon = findMonByUuid(msg.getBackupStorageHostUuid()); + } catch (OperationFailureException e) { + reply.setError(e.getErrorCode()); + bus.reply(msg, reply); + return; + } + + DeleteFilesCmd cmd = new DeleteFilesCmd(); + cmd.uuid = self.getUuid(); + cmd.fsid = getSelf().getFsid(); + cmd.filePaths = msg.getFilePaths(); + + CephBackupStorageMonBase monBase = new CephBackupStorageMonBase(mon); + monBase.httpCall(DELETE_FILES_PATH, cmd, DeleteFilesResponse.class, new ReturnValueCompletion(msg) { + @Override + public void fail(ErrorCode err) { + reply.setError(err); + bus.reply(msg, reply); + } + + @Override + public void success(DeleteFilesResponse rsp) { + bus.reply(msg, reply); + } + }); + } + + @Override + protected void handle(GetFileDownloadProgressFromBackupStorageHostMsg msg) { + GetFileDownloadProgressFromBackupStorageHostReply reply = new GetFileDownloadProgressFromBackupStorageHostReply(); + + if (msg.getTaskUuid() == null || msg.getTaskUuid().isEmpty()) { + reply.setError(operr("taskUuid cannot be null or empty")); + bus.reply(msg, reply); + return; + } + + CephBackupStorageMonVO mon; + try { + mon = findMonByUuid(msg.getBackupStorageHostUuid()); + } catch (OperationFailureException e) { + reply.setError(e.getErrorCode()); + bus.reply(msg, reply); + return; + } + + GetDownloadFileProgressCmd cmd = new GetDownloadFileProgressCmd(); + cmd.uuid = self.getUuid(); + cmd.fsid = getSelf().getFsid(); + cmd.taskUuid = msg.getTaskUuid(); + + CephBackupStorageMonBase monBase = new CephBackupStorageMonBase(mon); + monBase.httpCall(FILE_DOWNLOAD_PROGRESS_PATH, cmd, GetDownloadFileProgressResponse.class, new ReturnValueCompletion(msg) { + @Override + public void fail(ErrorCode err) { + reply.setError(err); + bus.reply(msg, reply); + } + + @Override + public void success(GetDownloadFileProgressResponse rsp) { + reply.setCompleted(rsp.completed); + reply.setProgress(rsp.progress); + reply.setActualSize(rsp.actualSize); + reply.setSize(rsp.size); + reply.setInstallPath(rsp.installPath); + reply.setDownloadSize(rsp.downloadSize); + reply.setLastOpTime(rsp.lastOpTime); + reply.setMd5sum(rsp.md5sum); + reply.setSupportSuspend(rsp.supportSuspend); + reply.setFormat(rsp.format); + bus.reply(msg, reply); + } + }); + } + + @Override + protected void handle(SoftwareUpgradePackageDeployMsg msg) { + SoftwareUpgradePackageDeployReply reply = new SoftwareUpgradePackageDeployReply(); + + if (msg.getUpgradePackagePath() == null || msg.getUpgradePackagePath().isEmpty()) { + reply.setError(operr("upgradePackagePath cannot be null or empty")); + bus.reply(msg, reply); + return; + } + + if (msg.getTargetHostIp() == null || msg.getTargetHostIp().isEmpty()) { + reply.setError(operr("targetHostIp cannot be null or empty")); + bus.reply(msg, reply); + return; + } + + if (msg.getTargetHostSshPort() <= 0 || msg.getTargetHostSshPort() > 65535) { + reply.setError(operr("targetHostSshPort must be in range 1-65535, but got [%d]", msg.getTargetHostSshPort())); + bus.reply(msg, reply); + return; + } + + if (!NetworkUtils.isValidIPAddress(msg.getTargetHostIp())) { + reply.setError(operr("targetHostIp [%s] is not a valid IPv4 or IPv6 address", msg.getTargetHostIp())); + bus.reply(msg, reply); + return; + } + + String usernameErr = RemotePathValidator.validateSshUsername(msg.getTargetHostSshUsername()); + if (usernameErr != null) { + reply.setError(operr(usernameErr)); + bus.reply(msg, reply); + return; + } + + // Validate all paths to prevent path traversal and injection attacks. + String pathErr = RemotePathValidator.validateRemotePath(msg.getUpgradePackagePath(), "upgradePackagePath"); + if (pathErr != null) { + reply.setError(operr(pathErr)); + bus.reply(msg, reply); + return; + } + if (msg.getUpgradePackageTargetPath() != null) { + pathErr = RemotePathValidator.validateRemotePath(msg.getUpgradePackageTargetPath(), "upgradePackageTargetPath"); + if (pathErr != null) { + reply.setError(operr(pathErr)); + bus.reply(msg, reply); + return; + } + } + if (msg.getUpgradeScriptPath() != null) { + pathErr = RemotePathValidator.validateRemotePath(msg.getUpgradeScriptPath(), "upgradeScriptPath"); + if (pathErr != null) { + reply.setError(operr(pathErr)); + bus.reply(msg, reply); + return; + } + } + + CephBackupStorageMonVO mon; + try { + mon = findMonByUuid(msg.getBackupStorageHostUuid()); + } catch (OperationFailureException e) { + reply.setError(e.getErrorCode()); + bus.reply(msg, reply); + return; + } + + SoftwareUpgradePackageCmd cmd = new SoftwareUpgradePackageCmd(); + cmd.upgradePackagePath = msg.getUpgradePackagePath(); + cmd.upgradePackageTargetPath = msg.getUpgradePackageTargetPath(); + cmd.softwareType = msg.getSoftwareType(); + cmd.upgradeScriptPath = msg.getUpgradeScriptPath(); + cmd.targetHostSshPort = msg.getTargetHostSshPort(); + cmd.targetHostSshUsername = msg.getTargetHostSshUsername(); + cmd.targetHostSshPassword = msg.getTargetHostSshPassword(); + cmd.targetHostIp = msg.getTargetHostIp(); + + CephBackupStorageMonBase monBase = new CephBackupStorageMonBase(mon); + monBase.httpCall(SOFTWARE_UPGRADE_PACKAGE_DEPLOY_PATH, cmd, SoftwareUpgradePackageResponse.class, new ReturnValueCompletion(msg) { + @Override + public void fail(ErrorCode err) { + reply.setError(err); + bus.reply(msg, reply); + } + + @Override + public void success(SoftwareUpgradePackageResponse rsp) { + bus.reply(msg, reply); + } + }); + } + + @Override + protected void handle(CancelDownloadFileOnBackupStorageHostMsg msg) { + CancelDownloadFileOnBackupStorageHostReply reply = new CancelDownloadFileOnBackupStorageHostReply(); + + if (StringUtils.isEmpty(msg.getCancellationApiId())) { + reply.setError(operr("cancellationApiId is required to cancel a download task")); + bus.reply(msg, reply); + return; + } + + CephBackupStorageMonVO mon; + try { + mon = findMonByUuid(msg.getBackupStorageHostUuid()); + } catch (OperationFailureException e) { + reply.setError(e.getErrorCode()); + bus.reply(msg, reply); + return; + } + + CancelCommand cmd = new CancelCommand(); + cmd.setCancellationApiId(msg.getCancellationApiId()); + + CephBackupStorageMonBase monBase = new CephBackupStorageMonBase(mon); + monBase.httpCall(AgentConstant.CANCEL_JOB, cmd, AgentResponse.class, new ReturnValueCompletion(msg) { + @Override + public void fail(ErrorCode err) { + reply.setError(err); + bus.reply(msg, reply); + } + + @Override + public void success(AgentResponse rsp) { + bus.reply(msg, reply); + } + }); + } } diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageSimulator.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageSimulator.java index 5fe7283f8db..a9b0d33bd80 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageSimulator.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageSimulator.java @@ -227,4 +227,82 @@ String getImagesMetadataToFile(HttpEntity entity) { return null; } + @RequestMapping(value=CephBackupStorageBase.GET_LOCAL_FILE_SIZE, method= RequestMethod.POST) + public @ResponseBody + String getLocalFileSize(HttpEntity entity) { + GetLocalFileSizeCmd cmd = JSONObjectUtil.toObject(entity.getBody(), GetLocalFileSizeCmd.class); + GetLocalFileSizeRsp rsp = new GetLocalFileSizeRsp(); + rsp.size = 0; + reply(entity, rsp); + return null; + } + + @RequestMapping(value=CephBackupStorageBase.FILE_DOWNLOAD_PATH, method= RequestMethod.POST) + public @ResponseBody + String fileDownload(HttpEntity entity) { + DownloadFileCmd cmd = JSONObjectUtil.toObject(entity.getBody(), DownloadFileCmd.class); + DownloadFileResponse rsp = new DownloadFileResponse(); + rsp.md5sum = "d41d8cd98f00b204e9800998ecf8427e"; + rsp.size = 0; + reply(entity, rsp); + return null; + } + + @RequestMapping(value=CephBackupStorageBase.FILE_UPLOAD_PATH, method= RequestMethod.POST) + public @ResponseBody + String fileUpload(HttpEntity entity) { + UploadFileCmd cmd = JSONObjectUtil.toObject(entity.getBody(), UploadFileCmd.class); + UploadFileResponse rsp = new UploadFileResponse(); + rsp.directUploadUrl = "http://127.0.0.1:7761/ceph/file/direct/upload"; + reply(entity, rsp); + return null; + } + + @RequestMapping(value=CephBackupStorageBase.FILE_DOWNLOAD_PROGRESS_PATH, method= RequestMethod.POST) + public @ResponseBody + String fileDownloadProgress(HttpEntity entity) { + GetDownloadFileProgressCmd cmd = JSONObjectUtil.toObject(entity.getBody(), GetDownloadFileProgressCmd.class); + GetDownloadFileProgressResponse rsp = new GetDownloadFileProgressResponse(); + rsp.completed = true; + rsp.progress = 100; + rsp.size = 0; + rsp.actualSize = 0; + rsp.installPath = "/tmp/test-software-package/unzipInstallPath"; + rsp.format = "qcow2"; + rsp.lastOpTime = System.currentTimeMillis(); + rsp.downloadSize = 0; + rsp.md5sum = "d41d8cd98f00b204e9800998ecf8427e"; + rsp.supportSuspend = true; + reply(entity, rsp); + return null; + } + + @RequestMapping(value=CephBackupStorageBase.DELETE_FILES_PATH, method= RequestMethod.POST) + public @ResponseBody + String deleteFiles(HttpEntity entity) { + DeleteFilesCmd cmd = JSONObjectUtil.toObject(entity.getBody(), DeleteFilesCmd.class); + DeleteFilesResponse rsp = new DeleteFilesResponse(); + reply(entity, rsp); + return null; + } + + @RequestMapping(value=CephBackupStorageBase.UNZIP_FILE_PATH, method= RequestMethod.POST) + public @ResponseBody + String unzipFile(HttpEntity entity) { + UnzipFileCmd cmd = JSONObjectUtil.toObject(entity.getBody(), UnzipFileCmd.class); + UnzipFileResponse rsp = new UnzipFileResponse(); + rsp.unzipInstallPath = "/tmp/test-software-package/unzipInstallPath"; + rsp.fileSizes = new java.util.HashMap<>(); + reply(entity, rsp); + return null; + } + + @RequestMapping(value=CephBackupStorageBase.SOFTWARE_UPGRADE_PACKAGE_DEPLOY_PATH, method= RequestMethod.POST) + public @ResponseBody + String softwareUpgradePackageDeploy(HttpEntity entity) { + SoftwareUpgradePackageCmd cmd = JSONObjectUtil.toObject(entity.getBody(), SoftwareUpgradePackageCmd.class); + SoftwareUpgradePackageResponse rsp = new SoftwareUpgradePackageResponse(); + reply(entity, rsp); + return null; + } } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java index fbc6b67b9bf..7e87bb6557f 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -4847,7 +4847,7 @@ public static class UploadFileCmd extends AgentCommand implements HasThreadConte } public static class UploadFileResponse extends AgentResponse { - public String directUploadPath; + public String directUploadUrl; } public static class GetDownloadFileProgressCmd extends AgentCommand { diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index 320679e2782..9bd8b6667da 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -7500,7 +7500,7 @@ public void success(UploadFileResponse rsp) { return; } - reply.setDirectUploadUrl(rsp.directUploadPath); + reply.setDirectUploadUrl(rsp.directUploadUrl); bus.reply(msg, reply); completion.done(); } diff --git a/sdk/src/main/java/org/zstack/sdk/AdditionalLicenseType.java b/sdk/src/main/java/org/zstack/sdk/AdditionalLicenseType.java index 6121f538d79..2c35a09e98a 100644 --- a/sdk/src/main/java/org/zstack/sdk/AdditionalLicenseType.java +++ b/sdk/src/main/java/org/zstack/sdk/AdditionalLicenseType.java @@ -5,4 +5,5 @@ public enum AdditionalLicenseType { zstone, zcex, zsv, + zmigrate, } diff --git a/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/CleanUpgradeSoftwarePackageAction.java b/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/CleanUpgradeSoftwarePackageAction.java new file mode 100644 index 00000000000..587fa3614ff --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/CleanUpgradeSoftwarePackageAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk.softwarePackage.header; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class CleanUpgradeSoftwarePackageAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.softwarePackage.header.CleanUpgradeSoftwarePackageResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @Param(required = false) + public java.lang.String deleteMode = "Permissive"; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.softwarePackage.header.CleanUpgradeSoftwarePackageResult value = res.getResult(org.zstack.sdk.softwarePackage.header.CleanUpgradeSoftwarePackageResult.class); + ret.value = value == null ? new org.zstack.sdk.softwarePackage.header.CleanUpgradeSoftwarePackageResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "DELETE"; + info.path = "/software-package/upgrade/packages/{uuid}"; + info.needSession = true; + info.needPoll = true; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/CleanUpgradeSoftwarePackageResult.java b/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/CleanUpgradeSoftwarePackageResult.java new file mode 100644 index 00000000000..b087b70f048 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/CleanUpgradeSoftwarePackageResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk.softwarePackage.header; + + + +public class CleanUpgradeSoftwarePackageResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/UploadAndExecuteSoftwareUpgradePackageAction.java b/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/UploadAndExecuteSoftwareUpgradePackageAction.java new file mode 100644 index 00000000000..07cbdd4fef7 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/UploadAndExecuteSoftwareUpgradePackageAction.java @@ -0,0 +1,113 @@ +package org.zstack.sdk.softwarePackage.header; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UploadAndExecuteSoftwareUpgradePackageAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.softwarePackage.header.UploadAndExecuteSoftwareUpgradePackageResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String backupStorageUuid; + + @Param(required = false, maxLength = 1024, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String url; + + @Param(required = false, maxLength = 1024, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String installPath; + + @Param(required = false, validValues = {"Normal","Reexecute"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String upgradeType = "Normal"; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.softwarePackage.header.UploadAndExecuteSoftwareUpgradePackageResult value = res.getResult(org.zstack.sdk.softwarePackage.header.UploadAndExecuteSoftwareUpgradePackageResult.class); + ret.value = value == null ? new org.zstack.sdk.softwarePackage.header.UploadAndExecuteSoftwareUpgradePackageResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "POST"; + info.path = "/software-packages/backup-storage/{uuid}/upgrade"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/UploadAndExecuteSoftwareUpgradePackageResult.java b/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/UploadAndExecuteSoftwareUpgradePackageResult.java new file mode 100644 index 00000000000..543e556d740 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/UploadAndExecuteSoftwareUpgradePackageResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.softwarePackage.header; + +import org.zstack.sdk.softwarePackage.header.SoftwarePackageInventory; + +public class UploadAndExecuteSoftwareUpgradePackageResult { + public SoftwarePackageInventory inventory; + public void setInventory(SoftwarePackageInventory inventory) { + this.inventory = inventory; + } + public SoftwarePackageInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/UploadSoftwarePackageToBackupStorageAction.java b/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/UploadSoftwarePackageToBackupStorageAction.java new file mode 100644 index 00000000000..301061c70b6 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/UploadSoftwarePackageToBackupStorageAction.java @@ -0,0 +1,119 @@ +package org.zstack.sdk.softwarePackage.header; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UploadSoftwarePackageToBackupStorageAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.softwarePackage.header.UploadSoftwarePackageToBackupStorageResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, maxLength = 255, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String name; + + @Param(required = true, maxLength = 255, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String type; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String backupStorageUuid; + + @Param(required = true, maxLength = 1024, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String url; + + @Param(required = true, maxLength = 1024, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String installPath; + + @Param(required = false) + public java.lang.String resourceUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List tagUuids; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.softwarePackage.header.UploadSoftwarePackageToBackupStorageResult value = res.getResult(org.zstack.sdk.softwarePackage.header.UploadSoftwarePackageToBackupStorageResult.class); + ret.value = value == null ? new org.zstack.sdk.softwarePackage.header.UploadSoftwarePackageToBackupStorageResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "POST"; + info.path = "/software-packages/backup-storage/upload"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/UploadSoftwarePackageToBackupStorageResult.java b/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/UploadSoftwarePackageToBackupStorageResult.java new file mode 100644 index 00000000000..89829806437 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/softwarePackage/header/UploadSoftwarePackageToBackupStorageResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.softwarePackage.header; + +import org.zstack.sdk.softwarePackage.header.SoftwarePackageInventory; + +public class UploadSoftwarePackageToBackupStorageResult { + public SoftwarePackageInventory inventory; + public void setInventory(SoftwarePackageInventory inventory) { + this.inventory = inventory; + } + public SoftwarePackageInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateGatewayVmInstancesAction.java b/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateGatewayVmInstancesAction.java new file mode 100644 index 00000000000..4d9fdef8f37 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateGatewayVmInstancesAction.java @@ -0,0 +1,92 @@ +package org.zstack.sdk.zmigrate.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class GetZMigrateGatewayVmInstancesAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.zmigrate.api.GetZMigrateGatewayVmInstancesResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.zmigrate.api.GetZMigrateGatewayVmInstancesResult value = res.getResult(org.zstack.sdk.zmigrate.api.GetZMigrateGatewayVmInstancesResult.class); + ret.value = value == null ? new org.zstack.sdk.zmigrate.api.GetZMigrateGatewayVmInstancesResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "GET"; + info.path = "/zmigrate/vm-instances"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateGatewayVmInstancesResult.java b/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateGatewayVmInstancesResult.java new file mode 100644 index 00000000000..853c8f9417d --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateGatewayVmInstancesResult.java @@ -0,0 +1,22 @@ +package org.zstack.sdk.zmigrate.api; + + + +public class GetZMigrateGatewayVmInstancesResult { + public java.lang.String managementVmInstanceUuid; + public void setManagementVmInstanceUuid(java.lang.String managementVmInstanceUuid) { + this.managementVmInstanceUuid = managementVmInstanceUuid; + } + public java.lang.String getManagementVmInstanceUuid() { + return this.managementVmInstanceUuid; + } + + public java.util.List gatewayVmInstances; + public void setGatewayVmInstances(java.util.List gatewayVmInstances) { + this.gatewayVmInstances = gatewayVmInstances; + } + public java.util.List getGatewayVmInstances() { + return this.gatewayVmInstances; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateImagesAction.java b/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateImagesAction.java new file mode 100644 index 00000000000..89d3060d7f8 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateImagesAction.java @@ -0,0 +1,92 @@ +package org.zstack.sdk.zmigrate.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class GetZMigrateImagesAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.zmigrate.api.GetZMigrateImagesResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.zmigrate.api.GetZMigrateImagesResult value = res.getResult(org.zstack.sdk.zmigrate.api.GetZMigrateImagesResult.class); + ret.value = value == null ? new org.zstack.sdk.zmigrate.api.GetZMigrateImagesResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "GET"; + info.path = "/zmigrate/images"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateImagesResult.java b/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateImagesResult.java new file mode 100644 index 00000000000..0e84b8cf33b --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateImagesResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.zmigrate.api; + + + +public class GetZMigrateImagesResult { + public java.util.Map images; + public void setImages(java.util.Map images) { + this.images = images; + } + public java.util.Map getImages() { + return this.images; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateInfosAction.java b/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateInfosAction.java new file mode 100644 index 00000000000..52efd803ef3 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateInfosAction.java @@ -0,0 +1,92 @@ +package org.zstack.sdk.zmigrate.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class GetZMigrateInfosAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.zmigrate.api.GetZMigrateInfosResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.zmigrate.api.GetZMigrateInfosResult value = res.getResult(org.zstack.sdk.zmigrate.api.GetZMigrateInfosResult.class); + ret.value = value == null ? new org.zstack.sdk.zmigrate.api.GetZMigrateInfosResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "GET"; + info.path = "/zmigrate/management/infos"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateInfosResult.java b/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateInfosResult.java new file mode 100644 index 00000000000..d32c418b869 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zmigrate/api/GetZMigrateInfosResult.java @@ -0,0 +1,54 @@ +package org.zstack.sdk.zmigrate.api; + + + +public class GetZMigrateInfosResult { + public java.lang.String zmigrateVmInstanceStatus; + public void setZmigrateVmInstanceStatus(java.lang.String zmigrateVmInstanceStatus) { + this.zmigrateVmInstanceStatus = zmigrateVmInstanceStatus; + } + public java.lang.String getZmigrateVmInstanceStatus() { + return this.zmigrateVmInstanceStatus; + } + + public java.lang.String version; + public void setVersion(java.lang.String version) { + this.version = version; + } + public java.lang.String getVersion() { + return this.version; + } + + public long platforms; + public void setPlatforms(long platforms) { + this.platforms = platforms; + } + public long getPlatforms() { + return this.platforms; + } + + public long gateways; + public void setGateways(long gateways) { + this.gateways = gateways; + } + public long getGateways() { + return this.gateways; + } + + public long migrateJobs; + public void setMigrateJobs(long migrateJobs) { + this.migrateJobs = migrateJobs; + } + public long getMigrateJobs() { + return this.migrateJobs; + } + + public long zmigrateStartTime; + public void setZmigrateStartTime(long zmigrateStartTime) { + this.zmigrateStartTime = zmigrateStartTime; + } + public long getZmigrateStartTime() { + return this.zmigrateStartTime; + } + +} diff --git a/storage/src/main/java/org/zstack/storage/backup/BackupStorageBase.java b/storage/src/main/java/org/zstack/storage/backup/BackupStorageBase.java index 7ff0eeda833..6777354bdfe 100755 --- a/storage/src/main/java/org/zstack/storage/backup/BackupStorageBase.java +++ b/storage/src/main/java/org/zstack/storage/backup/BackupStorageBase.java @@ -123,6 +123,30 @@ protected void handle(GetBackupStorageManagerHostnameMsg msg) { bus.dealWithUnknownMessage(msg); } + protected void handle(UploadFileToBackupStorageHostMsg msg) { + bus.dealWithUnknownMessage(msg); + } + + protected void handle(DeleteFilesOnBackupStorageHostMsg msg) { + bus.dealWithUnknownMessage(msg); + } + + protected void handle(GetFileDownloadProgressFromBackupStorageHostMsg msg) { + bus.dealWithUnknownMessage(msg); + } + + protected void handle(SoftwareUpgradePackageDeployMsg msg) { + bus.dealWithUnknownMessage(msg); + } + + protected void handle(CancelDownloadFileOnBackupStorageHostMsg msg) { + bus.dealWithUnknownMessage(msg); + } + + protected void handle(UnzipFileOnBackupStorageHostMsg msg) { + bus.dealWithUnknownMessage(msg); + } + public BackupStorageBase(BackupStorageVO self) { this.self = self; this.id = BackupStorage.buildId(self.getUuid()); @@ -274,8 +298,20 @@ protected void handleLocalMessage(Message msg) throws URISyntaxException { handle((RestoreImagesBackupStorageMetadataToDatabaseMsg) msg); } else if (msg instanceof CalculateImageHashOnBackupStorageMsg) { handle((CalculateImageHashOnBackupStorageMsg) msg); + } else if (msg instanceof UploadFileToBackupStorageHostMsg) { + handle((UploadFileToBackupStorageHostMsg) msg); + } else if (msg instanceof DeleteFilesOnBackupStorageHostMsg) { + handle((DeleteFilesOnBackupStorageHostMsg) msg); } else if (msg instanceof GetBackupStorageManagerHostnameMsg) { handle((GetBackupStorageManagerHostnameMsg) msg); + } else if (msg instanceof GetFileDownloadProgressFromBackupStorageHostMsg) { + handle((GetFileDownloadProgressFromBackupStorageHostMsg) msg); + } else if (msg instanceof SoftwareUpgradePackageDeployMsg) { + handle((SoftwareUpgradePackageDeployMsg) msg); + } else if (msg instanceof CancelDownloadFileOnBackupStorageHostMsg) { + handle((CancelDownloadFileOnBackupStorageHostMsg) msg); + } else if (msg instanceof UnzipFileOnBackupStorageHostMsg) { + handle((UnzipFileOnBackupStorageHostMsg) msg); } else { bus.dealWithUnknownMessage(msg); } diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index 98ab983f292..7160cd1d93d 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -3770,6 +3770,33 @@ abstract class ApiHelper { } + def changeAccountType(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.ChangeAccountTypeAction.class) Closure c) { + def a = new org.zstack.sdk.ChangeAccountTypeAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def changeAffinityGroupState(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.ChangeAffinityGroupStateAction.class) Closure c) { def a = new org.zstack.sdk.ChangeAffinityGroupStateAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -13496,20 +13523,20 @@ abstract class ApiHelper { c.resolveStrategy = Closure.OWNER_FIRST c.delegate = a c() - + if (System.getProperty("apipath") != null) { if (a.apiId == null) { a.apiId = Platform.uuid } - + def tracker = new ApiPathTracker(a.apiId) def out = errorOut(a.call()) def path = tracker.getApiPath() if (!path.isEmpty()) { Test.apiPaths[a.class.name] = path.join(" --->\n") } - + return out } else { return errorOut(a.call()) @@ -16946,6 +16973,33 @@ abstract class ApiHelper { } + def getOAuthClientSecret(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.GetOAuthClientSecretAction.class) Closure c) { + def a = new org.zstack.sdk.GetOAuthClientSecretAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def getPciDeviceCandidatesForAttachingVm(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.GetPciDeviceCandidatesForAttachingVmAction.class) Closure c) { def a = new org.zstack.sdk.GetPciDeviceCandidatesForAttachingVmAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -30598,20 +30652,20 @@ abstract class ApiHelper { c.resolveStrategy = Closure.OWNER_FIRST c.delegate = a c() - + if (System.getProperty("apipath") != null) { if (a.apiId == null) { a.apiId = Platform.uuid } - + def tracker = new ApiPathTracker(a.apiId) def out = errorOut(a.call()) def path = tracker.getApiPath() if (!path.isEmpty()) { Test.apiPaths[a.class.name] = path.join(" --->\n") } - + return out } else { return errorOut(a.call()) @@ -30943,33 +30997,6 @@ abstract class ApiHelper { } - def changeAccountType(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.ChangeAccountTypeAction.class) Closure c) { - def a = new org.zstack.sdk.ChangeAccountTypeAction() - a.sessionId = Test.currentEnvSpec?.session?.uuid - c.resolveStrategy = Closure.OWNER_FIRST - c.delegate = a - c() - - - if (System.getProperty("apipath") != null) { - if (a.apiId == null) { - a.apiId = Platform.uuid - } - - def tracker = new ApiPathTracker(a.apiId) - def out = errorOut(a.call()) - def path = tracker.getApiPath() - if (!path.isEmpty()) { - Test.apiPaths[a.class.name] = path.join(" --->\n") - } - - return out - } else { - return errorOut(a.call()) - } - } - - def updateAffinityGroup(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UpdateAffinityGroupAction.class) Closure c) { def a = new org.zstack.sdk.UpdateAffinityGroupAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -39130,6 +39157,33 @@ abstract class ApiHelper { } + def cleanUpgradeSoftwarePackage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.CleanUpgradeSoftwarePackageAction.class) Closure c) { + def a = new org.zstack.sdk.softwarePackage.header.CleanUpgradeSoftwarePackageAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def getDirectoryUsage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.GetDirectoryUsageAction.class) Closure c) { def a = new org.zstack.sdk.softwarePackage.header.GetDirectoryUsageAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -39267,6 +39321,33 @@ abstract class ApiHelper { } + def uploadAndExecuteSoftwareUpgradePackage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.UploadAndExecuteSoftwareUpgradePackageAction.class) Closure c) { + def a = new org.zstack.sdk.softwarePackage.header.UploadAndExecuteSoftwareUpgradePackageAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def uploadSoftwarePackage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.UploadSoftwarePackageAction.class) Closure c) { def a = new org.zstack.sdk.softwarePackage.header.UploadSoftwarePackageAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -39294,6 +39375,33 @@ abstract class ApiHelper { } + def uploadSoftwarePackageToBackupStorage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.UploadSoftwarePackageToBackupStorageAction.class) Closure c) { + def a = new org.zstack.sdk.softwarePackage.header.UploadSoftwarePackageToBackupStorageAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def addTpm(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.tpm.api.AddTpmAction.class) Closure c) { def a = new org.zstack.sdk.tpm.api.AddTpmAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -39871,6 +39979,87 @@ abstract class ApiHelper { } + def getZMigrateGatewayVmInstances(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zmigrate.api.GetZMigrateGatewayVmInstancesAction.class) Closure c) { + def a = new org.zstack.sdk.zmigrate.api.GetZMigrateGatewayVmInstancesAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def getZMigrateImages(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zmigrate.api.GetZMigrateImagesAction.class) Closure c) { + def a = new org.zstack.sdk.zmigrate.api.GetZMigrateImagesAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def getZMigrateInfos(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zmigrate.api.GetZMigrateInfosAction.class) Closure c) { + def a = new org.zstack.sdk.zmigrate.api.GetZMigrateInfosAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def addZStone(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zstone.api.AddZStoneAction.class) Closure c) { def a = new org.zstack.sdk.zstone.api.AddZStoneAction() a.sessionId = Test.currentEnvSpec?.session?.uuid diff --git a/testlib/src/main/java/org/zstack/testlib/CephBackupStorageSpec.groovy b/testlib/src/main/java/org/zstack/testlib/CephBackupStorageSpec.groovy index bc2d88a5e0e..a84f7670e27 100755 --- a/testlib/src/main/java/org/zstack/testlib/CephBackupStorageSpec.groovy +++ b/testlib/src/main/java/org/zstack/testlib/CephBackupStorageSpec.groovy @@ -230,6 +230,55 @@ class CephBackupStorageSpec extends BackupStorageSpec { simulator(CephBackupStorageBase.CEPH_TO_CEPH_MIGRATE_IMAGE_PATH) { HttpEntity entity -> return new CephBackupStorageBase.StorageMigrationRsp() } + + simulator(CephBackupStorageBase.FILE_DOWNLOAD_PATH) { HttpEntity entity -> + def rsp = new CephBackupStorageBase.DownloadFileResponse() + rsp.md5sum = "d41d8cd98f00b204e9800998ecf8427e" + rsp.size = 3L * 1024 * 1024 * 1024 + return rsp + } + + simulator(CephBackupStorageBase.FILE_UPLOAD_PATH) { HttpEntity entity -> + def rsp = new CephBackupStorageBase.UploadFileResponse() + rsp.directUploadUrl = "http://127.0.0.1:7761/ceph/file/direct/upload" + return rsp + } + + simulator(CephBackupStorageBase.FILE_DOWNLOAD_PROGRESS_PATH) { HttpEntity entity -> + def rsp = new CephBackupStorageBase.GetDownloadFileProgressResponse() + rsp.completed = true + rsp.progress = 100 + rsp.size = 3L * 1024 * 1024 * 1024 + rsp.actualSize = 3L * 1024 * 1024 * 1024 + rsp.installPath = "/tmp/test-software-package/unzipInstallPath" + rsp.format = "qcow2" + rsp.lastOpTime = System.currentTimeMillis() + rsp.downloadSize = 3L * 1024 * 1024 * 1024 + rsp.md5sum = "d41d8cd98f00b204e9800998ecf8427e" + rsp.supportSuspend = true + return rsp + } + + simulator(CephBackupStorageBase.DELETE_FILES_PATH) { HttpEntity entity -> + def rsp = new CephBackupStorageBase.DeleteFilesResponse() + return rsp + } + + simulator(CephBackupStorageBase.UNZIP_FILE_PATH) { HttpEntity entity -> + def rsp = new CephBackupStorageBase.UnzipFileResponse() + rsp.unzipInstallPath = "/tmp/test-software-package/unzipInstallPath" + rsp.fileSizes = [:] + rsp.fileSizes.put("/tmp/test-software-package/unzipInstallPath/Gateway_Linux_Server.qcow2", 1024L * 1024 * 1024) + rsp.fileSizes.put("/tmp/test-software-package/unzipInstallPath/BootImage_for_Linux.qcow2", 1024L * 1024 * 1024) + rsp.fileSizes.put("/tmp/test-software-package/unzipInstallPath/BootImage_for_Windows.qcow2", 1024L * 1024 * 1024) + rsp.fileSizes.put("/tmp/test-software-package/unzipInstallPath/TrekerInstallation.tar.gz", 1024) + return rsp + } + + simulator(CephBackupStorageBase.SOFTWARE_UPGRADE_PACKAGE_DEPLOY_PATH) { HttpEntity entity -> + def rsp = new CephBackupStorageBase.SoftwareUpgradePackageResponse() + return rsp + } } } diff --git a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy index e229b0fd4c1..e0ae1576003 100755 --- a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy +++ b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy @@ -725,7 +725,7 @@ class KVMSimulator implements Simulator { spec.simulator(KVMConstant.KVM_HOST_FILE_UPLOAD_PATH) { UploadFileResponse rsp = new UploadFileResponse() - rsp.directUploadPath = "http://172.1.1.1:7070/host/file/direct-upload" + rsp.directUploadUrl = "http://172.1.1.1:7070/host/file/direct-upload" return rsp } diff --git a/utils/src/main/java/org/zstack/utils/path/RemotePathValidator.java b/utils/src/main/java/org/zstack/utils/path/RemotePathValidator.java new file mode 100644 index 00000000000..6ab8699d23b --- /dev/null +++ b/utils/src/main/java/org/zstack/utils/path/RemotePathValidator.java @@ -0,0 +1,122 @@ +package org.zstack.utils.path; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.*; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class RemotePathValidator { + // Shell metacharacters that must not appear in paths sent to remote agents. + // Mirrors zstacklib/utils/linux.py _SHELL_UNSAFE_RE. + private static final Pattern SHELL_UNSAFE_PATTERN = + Pattern.compile("[;|&$`'\"\\\\(){}\\[\\]<>!#~\\n\\r\\x00*?]"); + + // Protected system directories that must never be used as a target path. + private static final Set PROTECTED_PATHS = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList( + "/", "/bin", "/boot", "/dev", "/etc", "/lib", "/lib64", + "/proc", "/run", "/sbin", "/srv", "/sys", "/usr", "/var"))); + + public static final Set ALLOWED_URL_SCHEMES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList("http", "https", "ftp", "sftp"))); + + // SSH username: only alphanumeric, dots, hyphens, underscores, optional trailing $ + private static final Pattern SSH_USERNAME_PATTERN = + Pattern.compile("^[a-zA-Z0-9._-]+\\$?$"); + + /** + * Validate a remote path to be sent to an agent. + * Returns null if valid, or an error message if invalid. + * + * Checks: non-empty, absolute, no traversal, no shell metacharacters, + * no protected system directory. + */ + public static String validateRemotePath(String path, String paramName) { + if (path == null || path.isEmpty()) { + return String.format("%s cannot be null or empty", paramName); + } + if (!path.startsWith("/")) { + return String.format("%s [%s] must be an absolute path", paramName, path); + } + // Canonicalize by path components: collapse slashes, remove ".", reject ".." + List segments = new ArrayList<>(); + for (String component : path.split("/+")) { + if (component.isEmpty() || ".".equals(component)) { + continue; + } + if ("..".equals(component)) { + return String.format("%s [%s] contains path traversal sequence", paramName, path); + } + segments.add(component); + } + String normalized = segments.isEmpty() ? "/" : "/" + String.join("/", segments); + // Reject shell metacharacters (command injection via agent shell calls) + Matcher m = SHELL_UNSAFE_PATTERN.matcher(path); + if (m.find()) { + return String.format("%s [%s] contains unsafe character '%s'", paramName, path, m.group()); + } + // Reject protected system directories as direct targets + if (PROTECTED_PATHS.contains(normalized)) { + return String.format("%s [%s] targets a protected system directory", paramName, path); + } + return null; + } + + /** + * Validate a URL scheme against the allowed list and return the lowercase scheme. + * Returns a two-element array: [0] = error message (null if valid), [1] = scheme (null if invalid). + * Parses the URI only once, combining validation and extraction. + */ + public static String[] validateAndExtractUrlScheme(String url) { + if (url == null || url.isEmpty()) { + return new String[]{"URL cannot be null or empty", null}; + } + URI uri; + try { + uri = new URI(url); + } catch (URISyntaxException e) { + return new String[]{String.format("failed to parse URL [%s]: %s", url, e.getMessage()), null}; + } + String scheme = uri.getScheme(); + if (scheme == null || scheme.isEmpty()) { + return new String[]{String.format("URL [%s] is missing a protocol prefix", url), null}; + } + String lowerScheme = scheme.toLowerCase(Locale.ROOT); + if (!ALLOWED_URL_SCHEMES.contains(lowerScheme)) { + return new String[]{String.format("URL [%s] uses unsupported protocol [%s], only %s are allowed", + url, scheme, ALLOWED_URL_SCHEMES), null}; + } + return new String[]{null, lowerScheme}; + } + + /** + * Validate an SSH username. + * Returns null if valid, or an error message if invalid. + */ + public static String validateSshUsername(String username) { + if (username == null || !SSH_USERNAME_PATTERN.matcher(username).matches()) { + return String.format("SSH username [%s] is invalid, only alphanumeric characters, dots, hyphens, underscores and trailing dollar sign are allowed", + username); + } + return null; + } + + /** + * Validate a list of remote file paths. + * Returns null if all valid, or the first error message encountered. + */ + public static String validateFilePaths(List filePaths) { + if (filePaths == null || filePaths.isEmpty()) { + return null; + } + for (String filePath : filePaths) { + String err = validateRemotePath(filePath, "filePath"); + if (err != null) { + return err; + } + } + return null; + } +}