升级管理

This commit is contained in:
waner
2026-04-07 09:45:27 +08:00
parent 738c2d4ae6
commit 93539ccd97
43 changed files with 1647 additions and 483 deletions
@@ -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,
@@ -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());
@@ -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");
}
@@ -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();
}
}
@@ -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,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);
}
}
+2 -2
View File
@@ -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 = '';
@@ -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}