fix:设备信息

This commit is contained in:
waner
2026-05-13 15:33:29 +08:00
parent 4b886aa9c2
commit c3465dbb2a
19 changed files with 333 additions and 42 deletions
@@ -95,12 +95,18 @@ public class WebMvcConfig implements WebMvcConfigurer {
.addPathPatterns("/api/**")
.excludePathPatterns(
"/api/v1/device/status",
"/api/v1/device/info",
"/api/v1/masterKey/status",
"/api/v1/masterKey/init",
"/api/v1/masterKey/recover",
"/api/v1/auth/password-login",
"/api/v1/system/health",
"/api/v1/device/time-config",
"/api/v1/auth/ukey-login",
"/api/v1/auth/ukey-login/randoms",
"/api/v1/auth/captcha",
"/api/v1/auth/super-admin/ukeys/issue-sign"
"/api/v1/auth/super-admin/ukeys/issue-sign",
"/api/v1/device/network/**"
);
registry.addInterceptor(internalApiReplayInterceptor)
@@ -33,7 +33,6 @@ public class ResourceBackupProperties {
"tms_auth_session",
"tms_auth_challenge",
"tms_auth_audit_log",
"tms_operation_audit_log",
"tms_resource_backup_task",
"tms_resource_restore_task"
);
@@ -69,7 +69,7 @@ public class ResourceRestoreController {
}
@GetMapping("/latest")
@RequireInternalAuth(anyRole = RoleCode.OPS_ADMIN, authLevel = AuthLevel.FULL)
@RequireInternalAuth(anyRole = RoleCode.OPS_ADMIN, authLevel = AuthLevel.LIMITED)
@Operation(summary = "查询最近资源恢复结果", description = "TMS 重启后用于恢复页面展示最近一次恢复时间、结果和失败原因。")
public ApiResponse<ResourceRestoreLatestResponse> latestRestore() {
return ApiResponse.success(resourceTaskQueryService.getLatestRestore());
@@ -16,6 +16,9 @@ import com.cisd.tms.modules.device.support.SystemHardwareProbe;
import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe;
import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse;
import com.cisd.tms.modules.init.service.InitService;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -49,7 +52,7 @@ public class DeviceProfileServiceImpl implements DeviceProfileService {
section.setModel(deviceProfileProperties.getModel());
section.setName(deviceProfileProperties.getName());
section.setSerialNumber(trim(info.getDeviceSerial()));
section.setDeviceVersion(formatVersion(info.getStandardVersion()));
section.setDeviceVersion(firstNonBlank(info.getDeviceVersionText(), formatVersion(info.getDeviceVersion())));
return section;
}
@@ -136,12 +139,34 @@ public class DeviceProfileServiceImpl implements DeviceProfileService {
if (version <= 0) {
return "";
}
String decoded = decodeAsciiDeviceVersion(version);
if (!decoded.isBlank()) {
return decoded.startsWith("V") ? decoded : "V" + decoded;
}
if (version >= 100) {
return "V" + (version / 100) + "." + String.format("%02d", version % 100);
}
return "V" + version;
}
private String decodeAsciiDeviceVersion(int version) {
byte[] bytes = ByteBuffer.allocate(Integer.BYTES)
.order(ByteOrder.LITTLE_ENDIAN)
.putInt(version)
.array();
String decoded = new String(bytes, StandardCharsets.US_ASCII)
.replace("\u0000", "")
.trim();
if (!decoded.isBlank() && decoded.chars().allMatch(DeviceProfileServiceImpl::isPrintableVersionChar)) {
return decoded;
}
return "";
}
private static boolean isPrintableVersionChar(int value) {
return (value >= '0' && value <= '9') || value == '.' || value == 'V' || value == 'v';
}
private static String firstNonBlank(String... values) {
if (values == null) {
return "";
@@ -3,12 +3,29 @@ package com.cisd.tms.modules.device.support;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class DefaultHardwareStatusProbe implements HardwareStatusProbe {
private final String diskUsagePath;
private final CommandRunner commandRunner;
@Autowired
public DefaultHardwareStatusProbe(@Value("${tms.device.runtime-status.disk-usage-path:/home/tms}") String diskUsagePath) {
this(diskUsagePath, DefaultHardwareStatusProbe::runShellCommand);
}
DefaultHardwareStatusProbe(String diskUsagePath, CommandRunner commandRunner) {
this.diskUsagePath = firstNonBlank(diskUsagePath, "/home/tms");
this.commandRunner = commandRunner == null ? DefaultHardwareStatusProbe::runShellCommand : commandRunner;
}
@Override
public Double cpuUsagePercent() {
CpuTimes before = readCpuTimes();
@@ -52,23 +69,66 @@ public class DefaultHardwareStatusProbe implements HardwareStatusProbe {
@Override
public UsageSnapshot diskUsage() {
double totalGb = physicalDiskTotalGb();
double usedGb = filesystemUsedGb();
return new UsageSnapshot(round(usedGb), round(totalGb));
}
private double physicalDiskTotalGb() {
try {
List<String> lines = runCommand("df -k /");
if (lines.size() < 2) {
throw new IllegalStateException("磁盘探测未返回数据");
List<String> lines = commandRunner.run("lsblk -b -d -n -e 7,11 -o TYPE,SIZE");
long totalBytes = 0L;
for (String line : lines) {
String[] parts = line.trim().split("\\s+");
if (parts.length >= 2 && "disk".equals(parts[0])) {
totalBytes += Long.parseLong(parts[1]);
}
}
String[] parts = lines.get(1).trim().split("\\s+");
if (parts.length < 5) {
throw new IllegalStateException("磁盘探测返回无效数据");
if (totalBytes <= 0L) {
throw new IllegalStateException("物理磁盘探测未返回数据");
}
double totalGb = Long.parseLong(parts[1]) / 1024D / 1024D;
double usedGb = Long.parseLong(parts[2]) / 1024D / 1024D;
return new UsageSnapshot(round(usedGb), round(totalGb));
return totalBytes / 1024D / 1024D / 1024D;
} catch (IOException ex) {
throw new IllegalStateException("disk probe 执行失败", ex);
throw new IllegalStateException("physical disk probe 执行失败", ex);
}
}
private double filesystemUsedGb() {
RuntimeException lastFailure = null;
for (String path : diskUsagePaths()) {
try {
return parseFilesystemUsedGb(commandRunner.run("df -kP " + shellQuote(path)));
} catch (IOException ex) {
lastFailure = new IllegalStateException("disk probe 执行失败", ex);
} catch (RuntimeException ex) {
lastFailure = ex;
}
}
if (lastFailure != null) {
throw lastFailure;
}
throw new IllegalStateException("disk probe 执行失败");
}
private double parseFilesystemUsedGb(List<String> lines) {
if (lines.size() < 2) {
throw new IllegalStateException("磁盘探测未返回数据");
}
String[] parts = lines.get(1).trim().split("\\s+");
if (parts.length < 5) {
throw new IllegalStateException("磁盘探测返回无效数据");
}
return Long.parseLong(parts[2]) / 1024D / 1024D;
}
private List<String> diskUsagePaths() {
LinkedHashSet<String> paths = new LinkedHashSet<>();
paths.add(firstNonBlank(diskUsagePath, "/home/tms"));
paths.add("/home");
paths.add("/");
return new ArrayList<>(paths);
}
private CpuTimes readCpuTimes() {
try {
String line = Files.readAllLines(Path.of("/proc/stat")).get(0);
@@ -92,7 +152,7 @@ public class DefaultHardwareStatusProbe implements HardwareStatusProbe {
return Long.parseLong(parts[1]);
}
private List<String> runCommand(String command) throws IOException {
private static List<String> runShellCommand(String command) throws IOException {
Process process = new ProcessBuilder("bash", "-lc", command).start();
try {
int exitCode = process.waitFor();
@@ -107,10 +167,23 @@ public class DefaultHardwareStatusProbe implements HardwareStatusProbe {
}
}
private static String firstNonBlank(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value.trim();
}
private static String shellQuote(String value) {
return "'" + value.replace("'", "'\"'\"'") + "'";
}
private double round(double value) {
return Math.round(value * 100D) / 100D;
}
@FunctionalInterface
interface CommandRunner {
List<String> run(String command) throws IOException;
}
private record CpuTimes(long idle, long total) {
}
}
@@ -1,16 +1,25 @@
package com.cisd.tms.modules.device.support;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import org.springframework.stereotype.Component;
@Component
public class DefaultSystemHardwareProbe implements SystemHardwareProbe {
private final CommandRunner commandRunner;
public DefaultSystemHardwareProbe() {
this(DefaultSystemHardwareProbe::runShellCommand);
}
DefaultSystemHardwareProbe(CommandRunner commandRunner) {
this.commandRunner = commandRunner == null ? DefaultSystemHardwareProbe::runShellCommand : commandRunner;
}
@Override
public String cpuModel() {
return firstLine("lscpu | awk -F: '/Model name/ {print $2}'");
@@ -39,17 +48,32 @@ public class DefaultSystemHardwareProbe implements SystemHardwareProbe {
@Override
public String diskTotal() {
return firstLine("df -h / | awk 'NR==2 {print $2}'");
try {
List<String> lines = commandRunner.run("lsblk -b -d -n -e 7,11 -o TYPE,SIZE");
long totalBytes = 0L;
for (String line : lines) {
String[] parts = line.trim().split("\\s+");
if (parts.length >= 2 && "disk".equals(parts[0])) {
totalBytes += Long.parseLong(parts[1]);
}
}
if (totalBytes <= 0L) {
return "";
}
double gb = totalBytes / 1024.0 / 1024.0 / 1024.0;
return String.format(Locale.ROOT, "%.2fGB", gb);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
return "";
} catch (IOException | NumberFormatException ignored) {
return "";
}
}
private String firstLine(String command) {
try {
Process process = new ProcessBuilder("bash", "-lc", command).start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line = reader.readLine();
process.waitFor();
return line == null ? "" : line.trim();
}
List<String> lines = commandRunner.run(command);
return lines.isEmpty() ? "" : lines.get(0).trim();
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
return "";
@@ -57,4 +81,19 @@ public class DefaultSystemHardwareProbe implements SystemHardwareProbe {
return "";
}
}
private static List<String> runShellCommand(String command) throws IOException, InterruptedException {
Process process = new ProcessBuilder("bash", "-lc", command).start();
int exitCode = process.waitFor();
List<String> lines = new String(process.getInputStream().readAllBytes()).lines().toList();
if (exitCode != 0) {
throw new IOException("命令执行失败: " + command);
}
return lines;
}
@FunctionalInterface
interface CommandRunner {
List<String> run(String command) throws IOException, InterruptedException;
}
}
@@ -4,6 +4,7 @@ import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.file.dto.FileDetailResponse;
import com.cisd.tms.modules.file.dto.FileUploadResponse;
import com.cisd.tms.modules.file.service.FileService;
import com.cisd.tms.security.internal.ReplayProtected;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -26,6 +27,7 @@ public class FileController {
// 内部上传入口,前端先拿到 fileId,再把 fileId 传给初始化相关接口。
@PostMapping("/upload")
@ReplayProtected
@Operation(summary = "上传初始化文件", description = "上传许可证、配置包等初始化依赖文件,返回 fileId 供后续任务引用。")
public ApiResponse<FileUploadResponse> upload(
@Parameter(description = "待上传的初始化文件")
@@ -37,7 +37,6 @@ public class LmkController {
@PostMapping("/masterKey/init")
@Operation(summary = "初始化主密钥", description = "执行密码卡初始化识别、主密钥生成加载、内部密钥生成,并返回主密钥状态与校验值。")
@ReplayProtected
public ApiResponse<MasterKeyStateResult> initMasterKey() {
return ApiResponse.success(lmkService.initMasterKey());
}
@@ -65,7 +64,6 @@ public class LmkController {
*/
@PostMapping("/masterKey/recover")
@Operation(summary = "恢复全部密钥", description = "请求体需传入两份按 packetIndex 标识的备份包。两份备份包必须分别对应 packetIndex=1、2。服务端会依次恢复 LMK、IK 和用户密钥。")
@ReplayProtected
public ApiResponse<Void> recoverLmkEx(@Valid @RequestBody MasterKeyRecoverPacketsRequest request) {
validatePacketIndexes(request.getPackets());
lmkService.recoverKeyPackets(request.getPackets());
@@ -1,5 +1,7 @@
package com.cisd.tms.modules.mk.service.impl;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.common.util.DivisionUtils;
import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds;
import com.cisd.tms.integration.crypto.pcie.PcieUserKeyType;
@@ -70,6 +72,9 @@ public class LmkServiceImpl implements LmkService {
@Override
public MasterKeyStateResult initMasterKey() {
if (pcieCryptoService.checkLmk()) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "主密钥已存在,拒绝重新初始化");
}
// 密钥体系一旦重建,之前导出的两份备份包就全部失效,必须清空当前轮次缓存。
clearCachedBackupRound();
byte[] oldPin = decodeConfiguredPin("tms.mk.init-identify.old-pin-base64", masterKeyInitProperties.getOldPinBase64());
@@ -171,6 +176,9 @@ public class LmkServiceImpl implements LmkService {
@Override
public MasterKeyStateResult recoverKeyPackets(List<MasterKeyBackupPacket> packets) {
if (pcieCryptoService.checkLmk()) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "主密钥已存在,拒绝重新初始化");
}
// 进入恢复流程后,旧的备份轮次缓存不再可信,避免和恢复后的新密钥体系混用。
clearCachedBackupRound();
// 调用方可以任意顺序传入两份包,这里统一按 packetIndex 排序并校验必须是 1、2 两包。