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;
+ }
+}