升级管理
This commit is contained in:
@@ -363,12 +363,12 @@ public interface PcieCryptoService {
|
||||
|
||||
//----------------------------------------SDF通用算法运算类接口------------------------------------------
|
||||
/**
|
||||
* 使用通用内部 ECC 密钥执行签名。
|
||||
* 使用通用内部 ECC 密钥执行签名。(用户密钥使用)
|
||||
*/
|
||||
BackupDataResult eccInternalSign(EccInternalSignRequest request);
|
||||
|
||||
/**
|
||||
* 使用外部 ECC 私钥执行签名。
|
||||
* 使用外部 ECC 私钥执行签名。(用户密钥使用)
|
||||
*/
|
||||
BackupDataResult eccExternalSign(EccExternalSignRequest request);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ public class UpgradeProperties {
|
||||
|
||||
private String stagingRootDir = "/home/tmp/tms-upgrade-staging";
|
||||
private String logDir = "/home/tms/tmp/tms-upgrade-logs";
|
||||
private String signaturePublicKeyBase64 = "";
|
||||
private String signaturePublicKeyPemPath = "";
|
||||
|
||||
public String getStagingRootDir() {
|
||||
return stagingRootDir;
|
||||
@@ -25,11 +25,11 @@ public class UpgradeProperties {
|
||||
this.logDir = logDir;
|
||||
}
|
||||
|
||||
public String getSignaturePublicKeyBase64() {
|
||||
return signaturePublicKeyBase64;
|
||||
public String getSignaturePublicKeyPemPath() {
|
||||
return signaturePublicKeyPemPath;
|
||||
}
|
||||
|
||||
public void setSignaturePublicKeyBase64(String signaturePublicKeyBase64) {
|
||||
this.signaturePublicKeyBase64 = signaturePublicKeyBase64;
|
||||
public void setSignaturePublicKeyPemPath(String signaturePublicKeyPemPath) {
|
||||
this.signaturePublicKeyPemPath = signaturePublicKeyPemPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,15 +67,24 @@ public class UpgradeController {
|
||||
return ApiResponse.success(upgradeService.executeTask(taskId));
|
||||
}
|
||||
|
||||
@PostMapping("/upgrades/{taskId}/rollback")
|
||||
@Operation(summary = "回滚离线升级任务", description = "异步受理升级回滚请求,立即返回任务当前快照。")
|
||||
public ApiResponse<UpgradeTaskResponse> rollback(
|
||||
@Parameter(description = "升级任务号")
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
return ApiResponse.success(upgradeService.rollbackTask(taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/upgrades")
|
||||
@Operation(summary = "查询升级记录列表", description = "分页查询升级任务列表,支持按升级对象和状态筛选。")
|
||||
public ApiResponse<UpgradeTaskPageResponse> list(
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "10") int pageSize,
|
||||
@RequestParam(required = false) String targetComponent,
|
||||
@RequestParam(required = false) String taskType,
|
||||
@RequestParam(required = false) String status
|
||||
) {
|
||||
return ApiResponse.success(upgradeService.listTasks(page, pageSize, targetComponent, status));
|
||||
return ApiResponse.success(upgradeService.listTasks(page, pageSize, taskType, status));
|
||||
}
|
||||
|
||||
@GetMapping("/upgrades/{taskId}")
|
||||
|
||||
@@ -9,8 +9,10 @@ public class UpgradeCreateTaskResponse {
|
||||
private String taskId;
|
||||
@Schema(description = "升级任务状态", example = "PENDING_CONFIRM")
|
||||
private String status;
|
||||
@Schema(description = "升级对象", example = "STANDARD_APP")
|
||||
private String targetComponent;
|
||||
@Schema(description = "升级任务类型", example = "RECEIVER")
|
||||
private String taskType;
|
||||
@Schema(description = "设备产品类型", example = "DIRECT")
|
||||
private String productType;
|
||||
@Schema(description = "当前版本", example = "V1.3.1")
|
||||
private String currentVersion;
|
||||
@Schema(description = "目标版本", example = "V1.3.2")
|
||||
@@ -32,12 +34,20 @@ public class UpgradeCreateTaskResponse {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getTargetComponent() {
|
||||
return targetComponent;
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTargetComponent(String targetComponent) {
|
||||
this.targetComponent = targetComponent;
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
|
||||
@@ -7,18 +7,18 @@ public class UpgradeManifest {
|
||||
|
||||
@Schema(description = "升级包唯一标识", example = "PKG_20260319_001")
|
||||
private String packageId;
|
||||
@Schema(description = "升级对象", example = "STANDARD_APP")
|
||||
private String targetComponent;
|
||||
@Schema(description = "目标版本", example = "V1.3.2")
|
||||
private String targetVersion;
|
||||
@Schema(description = "升级任务类型", example = "RECEIVER")
|
||||
private String taskType;
|
||||
@Schema(description = "设备产品类型", example = "DIRECT")
|
||||
private String productType;
|
||||
@Schema(description = "升级版本", example = "V1.3.2")
|
||||
private String version;
|
||||
@Schema(description = "兼容的最低旧版本", example = "V1.3.0")
|
||||
private String minCompatibleVersion;
|
||||
@Schema(description = "升级包描述", example = "修复标准收发器已知问题")
|
||||
private String description;
|
||||
@Schema(description = "升级执行脚本路径", example = "scripts/orchestrator.sh")
|
||||
private String orchestrator;
|
||||
@Schema(description = "升级包校验信息")
|
||||
private FileChecksums fileChecksums = new FileChecksums();
|
||||
@Schema(description = "升级脚本入口")
|
||||
private Entrypoints entrypoints = new Entrypoints();
|
||||
|
||||
public String getPackageId() {
|
||||
return packageId;
|
||||
@@ -28,20 +28,28 @@ public class UpgradeManifest {
|
||||
this.packageId = packageId;
|
||||
}
|
||||
|
||||
public String getTargetComponent() {
|
||||
return targetComponent;
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTargetComponent(String targetComponent) {
|
||||
this.targetComponent = targetComponent;
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public String getTargetVersion() {
|
||||
return targetVersion;
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setTargetVersion(String targetVersion) {
|
||||
this.targetVersion = targetVersion;
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getMinCompatibleVersion() {
|
||||
@@ -60,31 +68,50 @@ public class UpgradeManifest {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getOrchestrator() {
|
||||
return orchestrator;
|
||||
public Entrypoints getEntrypoints() {
|
||||
return entrypoints;
|
||||
}
|
||||
|
||||
public void setOrchestrator(String orchestrator) {
|
||||
this.orchestrator = orchestrator;
|
||||
public void setEntrypoints(Entrypoints entrypoints) {
|
||||
this.entrypoints = entrypoints;
|
||||
}
|
||||
|
||||
public FileChecksums getFileChecksums() {
|
||||
return fileChecksums;
|
||||
}
|
||||
public static class Entrypoints {
|
||||
private String execute;
|
||||
private String precheck;
|
||||
private String verify;
|
||||
private String rollback;
|
||||
|
||||
public void setFileChecksums(FileChecksums fileChecksums) {
|
||||
this.fileChecksums = fileChecksums;
|
||||
}
|
||||
|
||||
public static class FileChecksums {
|
||||
private String payloadZip;
|
||||
|
||||
public String getPayloadZip() {
|
||||
return payloadZip;
|
||||
public String getExecute() {
|
||||
return execute;
|
||||
}
|
||||
|
||||
public void setPayloadZip(String payloadZip) {
|
||||
this.payloadZip = payloadZip;
|
||||
public void setExecute(String execute) {
|
||||
this.execute = execute;
|
||||
}
|
||||
|
||||
public String getPrecheck() {
|
||||
return precheck;
|
||||
}
|
||||
|
||||
public void setPrecheck(String precheck) {
|
||||
this.precheck = precheck;
|
||||
}
|
||||
|
||||
public String getVerify() {
|
||||
return verify;
|
||||
}
|
||||
|
||||
public void setVerify(String verify) {
|
||||
this.verify = verify;
|
||||
}
|
||||
|
||||
public String getRollback() {
|
||||
return rollback;
|
||||
}
|
||||
|
||||
public void setRollback(String rollback) {
|
||||
this.rollback = rollback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@Schema(description = "离线升级包预检响应")
|
||||
public class UpgradePreviewResponse {
|
||||
|
||||
@Schema(description = "升级对象", example = "STANDARD_APP")
|
||||
private String targetComponent;
|
||||
@Schema(description = "升级任务类型", example = "RECEIVER")
|
||||
private String taskType;
|
||||
@Schema(description = "设备产品类型", example = "DIRECT")
|
||||
private String productType;
|
||||
@Schema(description = "当前版本", example = "V1.3.1")
|
||||
private String currentVersion;
|
||||
@Schema(description = "目标版本", example = "V1.3.2")
|
||||
@@ -24,12 +26,20 @@ public class UpgradePreviewResponse {
|
||||
@Schema(description = "升级包说明", example = "修复标准收发器已知问题")
|
||||
private String description;
|
||||
|
||||
public String getTargetComponent() {
|
||||
return targetComponent;
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTargetComponent(String targetComponent) {
|
||||
this.targetComponent = targetComponent;
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
|
||||
@@ -9,8 +9,10 @@ public class UpgradeTaskResponse {
|
||||
private String taskId;
|
||||
@Schema(description = "升级方式", example = "OFFLINE")
|
||||
private String upgradeMode;
|
||||
@Schema(description = "升级对象", example = "STANDARD_APP")
|
||||
private String targetComponent;
|
||||
@Schema(description = "升级任务类型", example = "RECEIVER")
|
||||
private String taskType;
|
||||
@Schema(description = "设备产品类型", example = "DIRECT")
|
||||
private String productType;
|
||||
@Schema(description = "升级包文件标识", example = "FILE-20260319-000001")
|
||||
private String packageFileId;
|
||||
@Schema(description = "升级包名称", example = "upgrade-package.zip")
|
||||
@@ -48,12 +50,20 @@ public class UpgradeTaskResponse {
|
||||
this.upgradeMode = upgradeMode;
|
||||
}
|
||||
|
||||
public String getTargetComponent() {
|
||||
return targetComponent;
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTargetComponent(String targetComponent) {
|
||||
this.targetComponent = targetComponent;
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getPackageFileId() {
|
||||
|
||||
@@ -9,7 +9,8 @@ public class UpgradeTaskEntity extends BaseEntity {
|
||||
|
||||
private String taskId;
|
||||
private String upgradeMode;
|
||||
private String targetComponent;
|
||||
private String taskType;
|
||||
private String productType;
|
||||
private String packageFileId;
|
||||
private String packageName;
|
||||
private String packageHash;
|
||||
@@ -41,12 +42,20 @@ public class UpgradeTaskEntity extends BaseEntity {
|
||||
this.upgradeMode = upgradeMode;
|
||||
}
|
||||
|
||||
public String getTargetComponent() {
|
||||
return targetComponent;
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTargetComponent(String targetComponent) {
|
||||
this.targetComponent = targetComponent;
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getPackageFileId() {
|
||||
|
||||
@@ -2,6 +2,6 @@ package com.cisd.tms.modules.upgrade.enums;
|
||||
|
||||
public enum UpgradeTargetComponent {
|
||||
TMS,
|
||||
STANDARD_APP,
|
||||
DIRECT_APP
|
||||
RECEIVER,
|
||||
FIRMWARE
|
||||
}
|
||||
|
||||
@@ -4,5 +4,8 @@ public enum UpgradeTaskStatus {
|
||||
PENDING_CONFIRM,
|
||||
RUNNING,
|
||||
SUCCESS,
|
||||
FAILED
|
||||
FAILED,
|
||||
ROLLING_BACK,
|
||||
ROLLBACK_SUCCESS,
|
||||
ROLLBACK_FAILED
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.lang.ProcessBuilder.Redirect;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@@ -55,23 +56,40 @@ public class UpgradeTaskRunner {
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + task.getPackageFileId()));
|
||||
Path stagedDir = upgradePackageStagingService.stage(task.getTaskId(), Path.of(record.getStoragePath()));
|
||||
UpgradeManifest manifest = objectMapper.readValue(Files.readString(stagedDir.resolve("manifest.json")), UpgradeManifest.class);
|
||||
Path payloadDir = upgradePackageStagingService.stage(task.getTaskId() + "-payload", stagedDir.resolve("payload.zip"));
|
||||
Path orchestrator = payloadDir.resolve(manifest.getOrchestrator()).normalize().toAbsolutePath();
|
||||
if (!Files.exists(orchestrator)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade orchestrator not found");
|
||||
}
|
||||
Path root = stagedDir.normalize().toAbsolutePath();
|
||||
|
||||
Process process = new ProcessBuilder("bash", orchestrator.toString(), task.getTaskId())
|
||||
.directory(payloadDir.toFile())
|
||||
.redirectErrorStream(true)
|
||||
.redirectOutput(logPath.toFile())
|
||||
.start();
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
int precheckCode = runOptionalScript("precheck", root, manifest.getEntrypoints() == null ? "" : manifest.getEntrypoints().getPrecheck(), task, logPath);
|
||||
if (precheckCode != 0) {
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"FAILED",
|
||||
"orchestrator exited with " + exitCode,
|
||||
"precheck exited with " + precheckCode,
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
int executeCode = runRequiredScript("execute", root, manifest.getEntrypoints() == null ? "" : manifest.getEntrypoints().getExecute(), task, logPath);
|
||||
if (executeCode != 0) {
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"FAILED",
|
||||
"execute exited with " + executeCode,
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
int verifyCode = runOptionalScript("verify", root, manifest.getEntrypoints() == null ? "" : manifest.getEntrypoints().getVerify(), task, logPath);
|
||||
if (verifyCode != 0) {
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"FAILED",
|
||||
"verify exited with " + verifyCode,
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
@@ -80,8 +98,8 @@ public class UpgradeTaskRunner {
|
||||
}
|
||||
|
||||
DeviceSoftwareVersionEntity versionEntity = new DeviceSoftwareVersionEntity();
|
||||
versionEntity.setComponentCode(task.getTargetComponent());
|
||||
versionEntity.setComponentName(componentName(task.getTargetComponent()));
|
||||
versionEntity.setComponentCode(task.getTaskType());
|
||||
versionEntity.setComponentName(componentName(task.getTaskType()));
|
||||
versionEntity.setCurrentVersion(task.getTargetVersion());
|
||||
versionEntity.setSourceType("UPGRADE");
|
||||
versionEntity.setDetectedAt(LocalDateTime.now());
|
||||
@@ -113,15 +131,88 @@ public class UpgradeTaskRunner {
|
||||
}
|
||||
}
|
||||
|
||||
public void rollback(UpgradeTaskEntity task) {
|
||||
LocalDateTime startedAt = LocalDateTime.now();
|
||||
Path logPath = logPath(task.getTaskId() + "-rollback");
|
||||
try {
|
||||
Files.createDirectories(logPath.getParent());
|
||||
Files.writeString(logPath, "");
|
||||
|
||||
FileRecordEntity record = fileRecordRepository.findByFileId(task.getPackageFileId())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + task.getPackageFileId()));
|
||||
Path stagedDir = upgradePackageStagingService.stage(task.getTaskId() + "-rollback", Path.of(record.getStoragePath()));
|
||||
UpgradeManifest manifest = objectMapper.readValue(Files.readString(stagedDir.resolve("manifest.json")), UpgradeManifest.class);
|
||||
String rollbackPath = manifest.getEntrypoints() == null ? "" : trim(manifest.getEntrypoints().getRollback());
|
||||
if (rollbackPath.isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade rollback script not found");
|
||||
}
|
||||
int exitCode = runRequiredScript("rollback", stagedDir.normalize().toAbsolutePath(), rollbackPath, task, logPath);
|
||||
if (exitCode != 0) {
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"ROLLBACK_FAILED",
|
||||
"rollback exited with " + exitCode,
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"ROLLBACK_SUCCESS",
|
||||
"",
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
} catch (Exception ex) {
|
||||
try {
|
||||
Files.createDirectories(logPath.getParent());
|
||||
Files.writeString(logPath, ex.getMessage() == null ? "upgrade rollback failed" : ex.getMessage());
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"ROLLBACK_FAILED",
|
||||
ex.getMessage(),
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private Path logPath(String taskId) {
|
||||
return Path.of(trim(upgradeProperties.getLogDir())).normalize().toAbsolutePath().resolve(taskId + ".log");
|
||||
}
|
||||
|
||||
private int runOptionalScript(String phase, Path stagedDir, String scriptPath, UpgradeTaskEntity task, Path logPath) throws IOException, InterruptedException {
|
||||
if (trim(scriptPath).isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return runRequiredScript(phase, stagedDir, scriptPath, task, logPath);
|
||||
}
|
||||
|
||||
private int runRequiredScript(String phase, Path stagedDir, String scriptPath, UpgradeTaskEntity task, Path logPath) throws IOException, InterruptedException {
|
||||
Path resolved = stagedDir.resolve(trim(scriptPath)).normalize().toAbsolutePath();
|
||||
if (!resolved.startsWith(stagedDir) || !Files.exists(resolved)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade " + phase + " script not found");
|
||||
}
|
||||
Process process = new ProcessBuilder("bash", resolved.toString(), task.getTaskId())
|
||||
.directory(stagedDir.toFile())
|
||||
.redirectErrorStream(true)
|
||||
.redirectOutput(Redirect.appendTo(logPath.toFile()))
|
||||
.start();
|
||||
return process.waitFor();
|
||||
}
|
||||
|
||||
private String componentName(String componentCode) {
|
||||
return switch (trim(componentCode)) {
|
||||
case "TMS" -> "设备管理软件";
|
||||
case "STANDARD_APP" -> "标准收发器";
|
||||
case "DIRECT_APP" -> "直参轻量化终端";
|
||||
case "RECEIVER" -> "标准收发器";
|
||||
case "FIRMWARE" -> "固件";
|
||||
default -> componentCode;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,14 +15,14 @@ public interface UpgradeTaskMapper extends BaseMapperX<UpgradeTaskEntity> {
|
||||
UpgradeTaskEntity selectRunningTask();
|
||||
|
||||
List<UpgradeTaskEntity> selectPage(
|
||||
@Param("targetComponent") String targetComponent,
|
||||
@Param("taskType") String taskType,
|
||||
@Param("status") String status,
|
||||
@Param("offset") int offset,
|
||||
@Param("limit") int limit
|
||||
);
|
||||
|
||||
long countPage(
|
||||
@Param("targetComponent") String targetComponent,
|
||||
@Param("taskType") String taskType,
|
||||
@Param("status") String status
|
||||
);
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ public interface UpgradeTaskRepository {
|
||||
|
||||
Optional<UpgradeTaskEntity> findRunningTask();
|
||||
|
||||
List<UpgradeTaskEntity> findPage(String targetComponent, String status, int offset, int limit);
|
||||
List<UpgradeTaskEntity> findPage(String taskType, String status, int offset, int limit);
|
||||
|
||||
long countPage(String targetComponent, String status);
|
||||
long countPage(String taskType, String status);
|
||||
|
||||
void updateStatus(
|
||||
String taskId,
|
||||
|
||||
+4
-4
@@ -33,13 +33,13 @@ public class UpgradeTaskRepositoryImpl implements UpgradeTaskRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UpgradeTaskEntity> findPage(String targetComponent, String status, int offset, int limit) {
|
||||
return upgradeTaskMapper.selectPage(targetComponent, status, offset, limit);
|
||||
public List<UpgradeTaskEntity> findPage(String taskType, String status, int offset, int limit) {
|
||||
return upgradeTaskMapper.selectPage(taskType, status, offset, limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countPage(String targetComponent, String status) {
|
||||
return upgradeTaskMapper.countPage(targetComponent, status);
|
||||
public long countPage(String taskType, String status) {
|
||||
return upgradeTaskMapper.countPage(taskType, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.cisd.tms.modules.upgrade.service;
|
||||
|
||||
import com.cisd.tms.common.config.properties.CisdPresetProperties;
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
|
||||
@@ -13,12 +14,8 @@ import com.cisd.tms.modules.upgrade.support.UpgradePackageStagingService;
|
||||
import com.cisd.tms.modules.upgrade.support.UpgradeVersionComparator;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -30,6 +27,7 @@ public class UpgradePackageService {
|
||||
private final UpgradePackageStagingService upgradePackageStagingService;
|
||||
private final UpgradePackageSignatureVerifier signatureVerifier;
|
||||
private final UpgradeVersionComparator versionComparator;
|
||||
private final CisdPresetProperties cisdPresetProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public UpgradePackageService(
|
||||
@@ -38,6 +36,7 @@ public class UpgradePackageService {
|
||||
UpgradePackageStagingService upgradePackageStagingService,
|
||||
UpgradePackageSignatureVerifier signatureVerifier,
|
||||
UpgradeVersionComparator versionComparator,
|
||||
CisdPresetProperties cisdPresetProperties,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this.fileRecordRepository = fileRecordRepository;
|
||||
@@ -45,32 +44,20 @@ public class UpgradePackageService {
|
||||
this.upgradePackageStagingService = upgradePackageStagingService;
|
||||
this.signatureVerifier = signatureVerifier;
|
||||
this.versionComparator = versionComparator;
|
||||
this.cisdPresetProperties = cisdPresetProperties;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public UpgradePreviewResponse preview(String fileId) {
|
||||
FileRecordEntity record = fileRecordRepository.findByFileId(normalize(fileId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + normalize(fileId)));
|
||||
Path packagePath = Path.of(record.getStoragePath()).normalize().toAbsolutePath();
|
||||
if (!Files.exists(packagePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found on disk");
|
||||
}
|
||||
StagedUpgradePackage stagedPackage = stagePackage(fileId);
|
||||
UpgradeManifest manifest = stagedPackage.manifest();
|
||||
validateManifest(manifest);
|
||||
validateProductType(manifest.getProductType());
|
||||
signatureVerifier.verify(manifest, stagedPackage.manifestPath(), stagedPackage.signaturePath());
|
||||
validateExecuteScript(stagedPackage.stagedDir(), manifest);
|
||||
|
||||
Path stagedDir = upgradePackageStagingService.stage(record.getFileId(), packagePath);
|
||||
Path manifestPath = stagedDir.resolve("manifest.json");
|
||||
Path payloadZipPath = stagedDir.resolve("payload.zip");
|
||||
Path signaturePath = stagedDir.resolve("signature.sig");
|
||||
|
||||
if (!Files.exists(manifestPath) || !Files.exists(payloadZipPath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package structure invalid");
|
||||
}
|
||||
|
||||
UpgradeManifest manifest = readManifest(manifestPath);
|
||||
verifyPayloadChecksum(manifest, payloadZipPath);
|
||||
signatureVerifier.verify(manifest, manifestPath, payloadZipPath, signaturePath);
|
||||
|
||||
String currentVersion = currentVersion(manifest.getTargetComponent());
|
||||
String targetVersion = normalize(manifest.getTargetVersion());
|
||||
String currentVersion = currentVersion(manifest.getTaskType());
|
||||
String targetVersion = normalize(manifest.getVersion());
|
||||
|
||||
if (!currentVersion.isEmpty() && versionComparator.compare(targetVersion, currentVersion) < 0) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "target version is lower than current version");
|
||||
@@ -81,10 +68,11 @@ public class UpgradePackageService {
|
||||
}
|
||||
|
||||
UpgradePreviewResponse response = new UpgradePreviewResponse();
|
||||
response.setTargetComponent(normalize(manifest.getTargetComponent()));
|
||||
response.setTaskType(normalize(manifest.getTaskType()));
|
||||
response.setProductType(normalize(manifest.getProductType()));
|
||||
response.setCurrentVersion(currentVersion);
|
||||
response.setTargetVersion(targetVersion);
|
||||
response.setVersionChangeSummary(buildVersionSummary(manifest.getTargetComponent(), currentVersion, targetVersion));
|
||||
response.setVersionChangeSummary(buildVersionSummary(currentVersion, targetVersion));
|
||||
response.setVerifyPassed(true);
|
||||
response.setVerifyMessage("校验通过");
|
||||
response.setAllowed(true);
|
||||
@@ -93,6 +81,10 @@ public class UpgradePackageService {
|
||||
return response;
|
||||
}
|
||||
|
||||
public UpgradeManifest loadManifest(String fileId) {
|
||||
return stagePackage(fileId).manifest();
|
||||
}
|
||||
|
||||
private UpgradeManifest readManifest(Path manifestPath) {
|
||||
try {
|
||||
return objectMapper.readValue(Files.readString(manifestPath), UpgradeManifest.class);
|
||||
@@ -101,30 +93,37 @@ public class UpgradePackageService {
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyPayloadChecksum(UpgradeManifest manifest, Path payloadZipPath) {
|
||||
String expected = normalize(manifest.getFileChecksums() == null ? "" : manifest.getFileChecksums().getPayloadZip());
|
||||
if (expected.isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "payload checksum missing");
|
||||
private StagedUpgradePackage stagePackage(String fileId) {
|
||||
FileRecordEntity record = fileRecordRepository.findByFileId(normalize(fileId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + normalize(fileId)));
|
||||
Path packagePath = Path.of(record.getStoragePath()).normalize().toAbsolutePath();
|
||||
if (!Files.exists(packagePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found on disk");
|
||||
}
|
||||
String actual = sha256Hex(payloadZipPath);
|
||||
if (!expected.equalsIgnoreCase(actual)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "payload checksum mismatch");
|
||||
|
||||
Path stagedDir = upgradePackageStagingService.stage(record.getFileId(), packagePath);
|
||||
Path manifestPath = stagedDir.resolve("manifest.json");
|
||||
Path signaturePath = stagedDir.resolve("signature.sig");
|
||||
if (!Files.exists(manifestPath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package structure invalid");
|
||||
}
|
||||
return new StagedUpgradePackage(stagedDir, manifestPath, signaturePath, readManifest(manifestPath));
|
||||
}
|
||||
|
||||
private String sha256Hex(Path file) {
|
||||
try (InputStream inputStream = Files.newInputStream(file)) {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] buffer = new byte[8192];
|
||||
int read;
|
||||
while ((read = inputStream.read(buffer)) >= 0) {
|
||||
if (read > 0) {
|
||||
digest.update(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
} catch (IOException | NoSuchAlgorithmException ex) {
|
||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to calculate payload checksum");
|
||||
private void validateManifest(UpgradeManifest manifest) {
|
||||
if (manifest == null) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "manifest.json is invalid");
|
||||
}
|
||||
if (normalize(manifest.getPackageId()).isEmpty()
|
||||
|| normalize(manifest.getTaskType()).isEmpty()
|
||||
|| normalize(manifest.getProductType()).isEmpty()
|
||||
|| normalize(manifest.getVersion()).isEmpty()
|
||||
|| normalize(manifest.getMinCompatibleVersion()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "manifest.json is invalid");
|
||||
}
|
||||
UpgradeManifest.Entrypoints entrypoints = manifest.getEntrypoints();
|
||||
if (entrypoints == null || normalize(entrypoints.getExecute()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade execute script not found");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,19 +132,40 @@ public class UpgradePackageService {
|
||||
return cached.map(DeviceSoftwareVersionEntity::getCurrentVersion).map(this::normalize).orElse("");
|
||||
}
|
||||
|
||||
private String buildVersionSummary(String componentCode, String currentVersion, String targetVersion) {
|
||||
String label = switch (normalize(componentCode)) {
|
||||
case "TMS" -> "设备管理软件";
|
||||
case "STANDARD_APP" -> "标准收发器";
|
||||
case "DIRECT_APP" -> "直参轻量化终端";
|
||||
default -> "目标组件";
|
||||
};
|
||||
private void validateProductType(String packageProductType) {
|
||||
String currentProductType = normalizeUpper(cisdPresetProperties.getProductType());
|
||||
String packageType = normalizeUpper(packageProductType);
|
||||
if (!currentProductType.isEmpty() && !packageType.equals(currentProductType)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package product type does not match current device");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateExecuteScript(Path stagedDir, UpgradeManifest manifest) {
|
||||
Path executePath = stagedDir.resolve(normalize(manifest.getEntrypoints().getExecute())).normalize().toAbsolutePath();
|
||||
if (!executePath.startsWith(stagedDir.normalize().toAbsolutePath()) || !Files.exists(executePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade execute script not found");
|
||||
}
|
||||
}
|
||||
|
||||
private String buildVersionSummary(String currentVersion, String targetVersion) {
|
||||
String from = currentVersion.isEmpty() ? "未记录版本" : currentVersion;
|
||||
String to = targetVersion.isEmpty() ? "未知目标版本" : targetVersion;
|
||||
return label + "将从 " + from + " 升级到 " + to;
|
||||
return "当前设备将从 " + from + " 升级到 " + to;
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private String normalizeUpper(String value) {
|
||||
return normalize(value).toUpperCase();
|
||||
}
|
||||
|
||||
private record StagedUpgradePackage(
|
||||
Path stagedDir,
|
||||
Path manifestPath,
|
||||
Path signaturePath,
|
||||
UpgradeManifest manifest
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskRequest;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeLogResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradePreviewRequest;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradePreviewResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeTaskPageResponse;
|
||||
@@ -102,7 +103,8 @@ public class UpgradeService {
|
||||
UpgradeTaskEntity entity = new UpgradeTaskEntity();
|
||||
entity.setTaskId(buildTaskId());
|
||||
entity.setUpgradeMode("OFFLINE");
|
||||
entity.setTargetComponent(preview.getTargetComponent());
|
||||
entity.setTaskType(preview.getTaskType());
|
||||
entity.setProductType(preview.getProductType());
|
||||
entity.setPackageFileId(request.getFileId().trim());
|
||||
entity.setPackageName(fileRecord.getOriginalFilename());
|
||||
entity.setPackageHash("");
|
||||
@@ -120,7 +122,8 @@ public class UpgradeService {
|
||||
UpgradeCreateTaskResponse response = new UpgradeCreateTaskResponse();
|
||||
response.setTaskId(entity.getTaskId());
|
||||
response.setStatus(entity.getStatus());
|
||||
response.setTargetComponent(entity.getTargetComponent());
|
||||
response.setTaskType(entity.getTaskType());
|
||||
response.setProductType(entity.getProductType());
|
||||
response.setCurrentVersion(entity.getCurrentVersion());
|
||||
response.setTargetVersion(entity.getTargetVersion());
|
||||
return response;
|
||||
@@ -150,12 +153,37 @@ public class UpgradeService {
|
||||
return toResponse(loadTask(taskId));
|
||||
}
|
||||
|
||||
public UpgradeTaskPageResponse listTasks(int page, int pageSize, String targetComponent, String status) {
|
||||
public UpgradeTaskResponse rollbackTask(String taskId) {
|
||||
UpgradeTaskEntity task = loadTask(taskId);
|
||||
UpgradeManifest manifest = upgradePackageService.loadManifest(task.getPackageFileId());
|
||||
if (manifest.getEntrypoints() == null || trim(manifest.getEntrypoints().getRollback()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade rollback script not found");
|
||||
}
|
||||
UpgradeTaskEntity runningTask = upgradeTaskRepository.findRunningTask().orElse(null);
|
||||
if (runningTask != null && !runningTask.getTaskId().equals(taskId)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "another upgrade task is running");
|
||||
}
|
||||
if (activeExecutions.containsKey(taskId)) {
|
||||
return toResponse(loadTask(taskId));
|
||||
}
|
||||
upgradeTaskRepository.updateStatus(taskId, "ROLLING_BACK", "", task.getDetailLogPath(), LocalDateTime.now(), null);
|
||||
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
upgradeTaskRunner.rollback(loadTask(taskId));
|
||||
} finally {
|
||||
activeExecutions.remove(taskId);
|
||||
}
|
||||
}, backgroundExecutor);
|
||||
activeExecutions.put(taskId, future);
|
||||
return toResponse(loadTask(taskId));
|
||||
}
|
||||
|
||||
public UpgradeTaskPageResponse listTasks(int page, int pageSize, String taskType, String status) {
|
||||
int normalizedPage = Math.max(1, page);
|
||||
int normalizedPageSize = Math.max(1, pageSize);
|
||||
int offset = (normalizedPage - 1) * normalizedPageSize;
|
||||
List<UpgradeTaskEntity> entities = upgradeTaskRepository.findPage(trim(targetComponent), trim(status), offset, normalizedPageSize);
|
||||
long total = upgradeTaskRepository.countPage(trim(targetComponent), trim(status));
|
||||
List<UpgradeTaskEntity> entities = upgradeTaskRepository.findPage(trim(taskType), trim(status), offset, normalizedPageSize);
|
||||
long total = upgradeTaskRepository.countPage(trim(taskType), trim(status));
|
||||
|
||||
UpgradeTaskPageResponse response = new UpgradeTaskPageResponse();
|
||||
response.setPage(normalizedPage);
|
||||
@@ -203,7 +231,8 @@ public class UpgradeService {
|
||||
UpgradeTaskResponse response = new UpgradeTaskResponse();
|
||||
response.setTaskId(entity.getTaskId());
|
||||
response.setUpgradeMode(entity.getUpgradeMode());
|
||||
response.setTargetComponent(entity.getTargetComponent());
|
||||
response.setTaskType(entity.getTaskType());
|
||||
response.setProductType(entity.getProductType());
|
||||
response.setPackageFileId(entity.getPackageFileId());
|
||||
response.setPackageName(entity.getPackageName());
|
||||
response.setCurrentVersion(entity.getCurrentVersion());
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import java.nio.file.Path;
|
||||
public class NoopUpgradePackageSignatureVerifier implements UpgradePackageSignatureVerifier {
|
||||
|
||||
@Override
|
||||
public void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath) {
|
||||
public void verify(UpgradeManifest manifest, Path manifestPath, Path signaturePath) {
|
||||
if (!Files.exists(signaturePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file not found");
|
||||
}
|
||||
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
package com.cisd.tms.modules.upgrade.support;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccExternalVerifyRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.upgrade.config.UpgradeProperties;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Base64;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class PcieUpgradePackageSignatureVerifier implements UpgradePackageSignatureVerifier {
|
||||
|
||||
private final UpgradeProperties upgradeProperties;
|
||||
private final PcieCryptoService pcieCryptoService;
|
||||
|
||||
public PcieUpgradePackageSignatureVerifier(UpgradeProperties upgradeProperties, PcieCryptoService pcieCryptoService) {
|
||||
this.upgradeProperties = upgradeProperties;
|
||||
this.pcieCryptoService = pcieCryptoService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath) {
|
||||
if (!Files.exists(signaturePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file not found");
|
||||
}
|
||||
|
||||
byte[] publicKeyBlob = decodeConfiguredPublicKey();
|
||||
byte[] signature = loadSignature(signaturePath);
|
||||
byte[] manifestBytes = readBytes(manifestPath, "failed to read manifest for signature verification");
|
||||
|
||||
EccExternalVerifyRequest request = new EccExternalVerifyRequest();
|
||||
request.setAlgId(Gm0018AlgorithmIds.SM2_SIGN);
|
||||
request.setPublicKeyBlob(publicKeyBlob);
|
||||
request.setData(manifestBytes);
|
||||
request.setSignature(signature);
|
||||
|
||||
try {
|
||||
pcieCryptoService.eccExternalVerify(request);
|
||||
} catch (BizException ex) {
|
||||
throw ex;
|
||||
} catch (RuntimeException ex) {
|
||||
throw new BizException(ErrorCode.CRYPTO_CARD_ERROR.getCode(), "upgrade package signature verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] decodeConfiguredPublicKey() {
|
||||
String configured = normalize(upgradeProperties.getSignaturePublicKeyBase64());
|
||||
if (configured.isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key not configured");
|
||||
}
|
||||
try {
|
||||
byte[] decoded = Base64.getDecoder().decode(configured);
|
||||
if (decoded.length == 0) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key is invalid");
|
||||
}
|
||||
return decoded;
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] loadSignature(Path signaturePath) {
|
||||
byte[] raw = readBytes(signaturePath, "failed to read upgrade signature file");
|
||||
if (raw.length == 0) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file is empty");
|
||||
}
|
||||
|
||||
String text = new String(raw, StandardCharsets.UTF_8).trim();
|
||||
if (!text.isEmpty()) {
|
||||
try {
|
||||
byte[] decoded = Base64.getDecoder().decode(text);
|
||||
if (decoded.length > 0) {
|
||||
return decoded;
|
||||
}
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// Keep backward compatibility with raw binary signature files.
|
||||
}
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
private byte[] readBytes(Path path, String failureMessage) {
|
||||
try {
|
||||
return Files.readAllBytes(path);
|
||||
} catch (IOException ex) {
|
||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), failureMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package com.cisd.tms.modules.upgrade.support;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.upgrade.config.UpgradeProperties;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Security;
|
||||
import java.security.Signature;
|
||||
import java.util.Base64;
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SoftUpgradePackageSignatureVerifier implements UpgradePackageSignatureVerifier {
|
||||
|
||||
private static final String PROVIDER = "BC";
|
||||
private static final String SIGNATURE_ALGORITHM = "SM3withSM2";
|
||||
|
||||
static {
|
||||
if (Security.getProvider(PROVIDER) == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
}
|
||||
|
||||
private final UpgradeProperties upgradeProperties;
|
||||
|
||||
public SoftUpgradePackageSignatureVerifier(UpgradeProperties upgradeProperties) {
|
||||
this.upgradeProperties = upgradeProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(UpgradeManifest manifest, Path manifestPath, Path signaturePath) {
|
||||
if (!Files.exists(signaturePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file not found");
|
||||
}
|
||||
|
||||
PublicKey publicKey = loadPublicKey();
|
||||
byte[] manifestBytes = readBytes(manifestPath, "failed to read manifest for signature verification");
|
||||
byte[] signatureBytes = loadSignature(signaturePath);
|
||||
|
||||
try {
|
||||
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM, PROVIDER);
|
||||
signature.initVerify(publicKey);
|
||||
signature.update(manifestBytes);
|
||||
if (!signature.verify(signatureBytes)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature verification failed");
|
||||
}
|
||||
} catch (BizException ex) {
|
||||
throw ex;
|
||||
} catch (Exception ex) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
private PublicKey loadPublicKey() {
|
||||
Path pemPath = Path.of(normalize(upgradeProperties.getSignaturePublicKeyPemPath())).normalize().toAbsolutePath();
|
||||
if (normalize(upgradeProperties.getSignaturePublicKeyPemPath()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key pem path not configured");
|
||||
}
|
||||
if (!Files.exists(pemPath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key pem file not found");
|
||||
}
|
||||
try {
|
||||
String pemContent = Files.readString(pemPath, StandardCharsets.UTF_8);
|
||||
try (Reader reader = new StringReader(pemContent); PEMParser parser = new PEMParser(reader)) {
|
||||
Object parsed = parser.readObject();
|
||||
if (!(parsed instanceof SubjectPublicKeyInfo publicKeyInfo)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key pem is invalid");
|
||||
}
|
||||
return new JcaPEMKeyConverter().setProvider(PROVIDER).getPublicKey(publicKeyInfo);
|
||||
}
|
||||
} catch (BizException ex) {
|
||||
throw ex;
|
||||
} catch (IOException ex) {
|
||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to read upgrade signature public key pem");
|
||||
} catch (Exception ex) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key pem is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] loadSignature(Path signaturePath) {
|
||||
byte[] raw = readBytes(signaturePath, "failed to read upgrade signature file");
|
||||
if (raw.length == 0) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file is empty");
|
||||
}
|
||||
|
||||
String text = new String(raw, StandardCharsets.UTF_8).trim();
|
||||
if (!text.isEmpty()) {
|
||||
try {
|
||||
byte[] decoded = Base64.getDecoder().decode(text);
|
||||
if (decoded.length > 0) {
|
||||
return decoded;
|
||||
}
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// Keep compatibility with raw binary signature files.
|
||||
}
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
private byte[] readBytes(Path path, String failureMessage) {
|
||||
try {
|
||||
return Files.readAllBytes(path);
|
||||
} catch (IOException ex) {
|
||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), failureMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -5,5 +5,9 @@ import java.nio.file.Path;
|
||||
|
||||
public interface UpgradePackageSignatureVerifier {
|
||||
|
||||
void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath);
|
||||
void verify(UpgradeManifest manifest, Path manifestPath, Path signaturePath);
|
||||
|
||||
default void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath) {
|
||||
verify(manifest, manifestPath, signaturePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,8 +172,8 @@ tms:
|
||||
staging-root-dir: ${TMS_UPGRADE_STAGING_ROOT_DIR:/home/tmp/tms-upgrade-staging}
|
||||
# 升级任务统一日志目录。
|
||||
log-dir: ${TMS_UPGRADE_LOG_DIR:/home/tms/tmp/tms-upgrade-logs}
|
||||
# 升级包验签使用的“固件完整性密钥”公钥 blob(Base64 编码);为空时升级预检会拒绝通过。
|
||||
signature-public-key-base64: ${TMS_UPGRADE_SIGNATURE_PUBLIC_KEY_BASE64:}
|
||||
# 升级包验签使用的公钥 PEM 文件路径;为空时升级预检会拒绝通过。
|
||||
signature-public-key-pem-path: ${TMS_UPGRADE_SIGNATURE_PUBLIC_KEY_PEM_PATH:}
|
||||
mk:
|
||||
init-identify:
|
||||
# SDFE_InitIdentify 旧 PIN(Base64 编码);主密钥初始化接口会读取该值。
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE t_upgrade_task
|
||||
ADD COLUMN product_type VARCHAR(16) NULL AFTER target_component;
|
||||
|
||||
UPDATE t_upgrade_task
|
||||
SET product_type = 'ENTERPRISE'
|
||||
WHERE product_type IS NULL OR product_type = '';
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE t_upgrade_task
|
||||
CHANGE COLUMN target_component task_type VARCHAR(32) NOT NULL;
|
||||
|
||||
ALTER TABLE t_upgrade_task
|
||||
DROP INDEX idx_t_upgrade_task_component,
|
||||
ADD INDEX idx_t_upgrade_task_task_type (task_type);
|
||||
@@ -8,7 +8,8 @@
|
||||
<id property="id" column="id"/>
|
||||
<result property="taskId" column="task_id"/>
|
||||
<result property="upgradeMode" column="upgrade_mode"/>
|
||||
<result property="targetComponent" column="target_component"/>
|
||||
<result property="taskType" column="task_type"/>
|
||||
<result property="productType" column="product_type"/>
|
||||
<result property="packageFileId" column="package_file_id"/>
|
||||
<result property="packageName" column="package_name"/>
|
||||
<result property="packageHash" column="package_hash"/>
|
||||
@@ -28,7 +29,7 @@
|
||||
</resultMap>
|
||||
|
||||
<sql id="UpgradeTaskColumns">
|
||||
id, task_id, upgrade_mode, target_component, package_file_id, package_name, package_hash,
|
||||
id, task_id, upgrade_mode, task_type, product_type, package_file_id, package_name, package_hash,
|
||||
current_version, target_version, status, verify_passed, verify_message, manifest_json,
|
||||
detail_log_path, error_message, operator, started_at, finished_at, create_time, update_time
|
||||
</sql>
|
||||
@@ -52,8 +53,8 @@
|
||||
SELECT <include refid="UpgradeTaskColumns"/>
|
||||
FROM t_upgrade_task
|
||||
<where>
|
||||
<if test="targetComponent != null and targetComponent != ''">
|
||||
AND target_component = #{targetComponent}
|
||||
<if test="taskType != null and taskType != ''">
|
||||
AND task_type = #{taskType}
|
||||
</if>
|
||||
<if test="status != null and status != ''">
|
||||
AND status = #{status}
|
||||
@@ -67,8 +68,8 @@
|
||||
SELECT COUNT(1)
|
||||
FROM t_upgrade_task
|
||||
<where>
|
||||
<if test="targetComponent != null and targetComponent != ''">
|
||||
AND target_component = #{targetComponent}
|
||||
<if test="taskType != null and taskType != ''">
|
||||
AND task_type = #{taskType}
|
||||
</if>
|
||||
<if test="status != null and status != ''">
|
||||
AND status = #{status}
|
||||
|
||||
Reference in New Issue
Block a user