Scert/backend/internal/adminconsole/recovery_disposition.go

170 lines
7.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package adminconsole
import (
"context"
"fmt"
"strings"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
"git.pcux.cn/xuans/Scert/backend/internal/localreset"
)
type recoveryDispositionClient interface {
InspectRecovery(string) (deployment.RecoveryInspection, error)
PrepareRecovery(string, deployment.RecoveryAction) (localreset.RecoveryPreparation, error)
ExecuteRecovery(string, string) (deployment.RecoveryDispositionSummary, error)
GetRecoveryDisposition(string) (deployment.RecoveryDispositionSummary, error)
}
func recoveryDispositionFlow(ctx context.Context, client recoveryDispositionClient, screen terminal) error {
id, err := screen.Read(ctx, "请输入待处置事务编号(可先使用只读核查获取);输入 q 退出", false)
if err != nil {
return err
}
id = strings.TrimSpace(id)
if id == "q" {
return nil
}
if err := ctx.Err(); err != nil {
return err
}
inspection, err := client.InspectRecovery(id)
if err != nil {
_ = screen.Show("无法核查事务,未提交人工处置请求。请重新启动本机管理。")
return err
}
if err := showRecovery(screen, inspection); err != nil {
return err
}
choice, err := screen.Read(ctx, "选择明确动作:1 仅记录核查;2 完成提交(保留新证书并清理备份);3 恢复基线(恢复旧证书并重载);其他输入取消", false)
if err != nil {
return err
}
actions := map[string]deployment.RecoveryAction{
"1": deployment.RecoveryActionRecord, "2": deployment.RecoveryActionCommit, "3": deployment.RecoveryActionRollback,
}
action, ok := actions[strings.TrimSpace(choice)]
if !ok {
return screen.Show("已取消,未提交人工处置请求。")
}
if err := ctx.Err(); err != nil {
return err
}
prepared, err := client.PrepareRecovery(id, action)
if err != nil {
_ = screen.Show("准备失败或历史证据不足,未提交执行请求。请重新核查,不要手动删锁。")
return err
}
defer func() { prepared.Token = "" }()
if prepared.Summary.Validate() != nil || prepared.Summary.TransactionID != id || prepared.Summary.Action != action || !time.Now().Before(prepared.Summary.ExpiresAt) {
return localreset.ErrProtocol
}
lines := []string{
fmt.Sprintf("处置编号:%s(请保留,用于断连后查询结果)", terminalValue(prepared.DispositionID)),
fmt.Sprintf("事务:%s;服务器:%s;阶段:%s;动作:%s", terminalValue(id), terminalValue(prepared.Summary.ServerID), recoveryLabel(prepared.Summary.Phase), dispositionActionLabel(action)),
"确认截止时间:" + prepared.Summary.ExpiresAt.Local().Format("2006-01-02 15:04:05 MST"),
"仅记录只保存核查审计,不结束原事务;提交或回滚会修改远端文件,可能清理备份或重载服务。动作失败不会自动切换方向。",
"断连或结果不确定时,禁止重复执行;重新启动本机管理,选择查询人工处置结果。",
}
for _, line := range lines {
if err := screen.Show(line); err != nil {
return err
}
}
expected := "确认 " + id + " " + dispositionActionLabel(action)
confirmation, err := screen.Read(ctx, "如确认执行,请完整输入:"+expected+";其他输入取消", false)
if err != nil {
return err
}
if confirmation != expected {
return screen.Show("已取消,未发送执行请求。")
}
if err := ctx.Err(); err != nil {
return err
}
if !time.Now().Before(prepared.Summary.ExpiresAt) {
return screen.Show("确认已过期,未发送执行请求;请重新核查。")
}
result, err := client.ExecuteRecovery(prepared.DispositionID, prepared.Token)
prepared.Token = ""
if err != nil {
_ = screen.Show("未取得可靠结果,不能断言未执行或执行成功。请重新连接,按处置编号查询;不要重复提交或删除远端锁。")
return err
}
if result.ID != prepared.DispositionID || result.TransactionID != id || result.ServerID != prepared.Summary.ServerID || result.Action != action {
_ = screen.Show("响应与确认内容不一致,不能认定成功;请按处置编号查询。")
return localreset.ErrProtocol
}
return showRecoveryDisposition(screen, result)
}
func recoveryResultFlow(ctx context.Context, client recoveryDispositionClient, screen terminal) error {
id, err := screen.Read(ctx, "请输入人工处置编号(不是事务编号);本操作只查询,不重放、不释放占用", false)
if err != nil {
return err
}
if err := ctx.Err(); err != nil {
return err
}
id = strings.TrimSpace(id)
result, err := client.GetRecoveryDisposition(id)
if err != nil || result.ID != id {
_ = screen.Show("未取得有效处置结果;记录不存在或通道不可用不代表未执行,请保留编号继续核查。")
return localreset.ErrUnavailable
}
return showRecoveryDisposition(screen, result)
}
func showRecoveryDisposition(screen terminal, result deployment.RecoveryDispositionSummary) error {
if result.Validate() != nil {
return localreset.ErrProtocol
}
message := "处置仍在运行或结束审计尚未完成;保留占用,禁止重复执行。"
switch result.Status {
case "succeeded":
switch result.Action {
case deployment.RecoveryActionRecord:
message = "核查结论已记录;未改变原事务终态,不代表历史部署成功。"
case deployment.RecoveryActionCommit:
message = "本次人工提交核验及结束审计完成;不代表项目全部生产验收通过。"
case deployment.RecoveryActionRollback:
message = "本次恢复基线核验及结束审计完成;不代表项目全部生产验收通过。"
case deployment.RecoveryActionVerifyCommit, deployment.RecoveryActionVerifyRollback:
message = "重新核查与新审计已完成,占用已安全释放;原不确定处置历史保留,本次未重放远端动作。"
}
case "rejected":
message = "处置已拒绝,未执行本次远端变更;请重新核查拒绝原因。"
if result.SourceDispositionID != "" {
message = "重新核查未通过,原占用继续保留;本次未执行远端变更,不得手动删锁或重放原动作。"
}
case "uncertain":
message = "处置结果不确定,可能已修改远端;占用继续保留,禁止重复执行或手动删锁。"
}
if result.SourceDispositionID != "" {
if err := screen.Show("关联原处置:" + terminalValue(result.SourceDispositionID)); err != nil {
return err
}
}
if err := screen.Show(fmt.Sprintf("处置:%s;事务:%s;服务器:%s;动作:%s", terminalValue(result.ID), terminalValue(result.TransactionID), terminalValue(result.ServerID), dispositionActionLabel(result.Action))); err != nil {
return err
}
return screen.Show(message)
}
func dispositionActionLabel(action deployment.RecoveryAction) string {
switch action {
case deployment.RecoveryActionRecord:
return "仅记录核查"
case deployment.RecoveryActionCommit:
return "完成提交"
case deployment.RecoveryActionRollback:
return "恢复基线"
case deployment.RecoveryActionVerifyCommit:
return "重新核查提交结果"
case deployment.RecoveryActionVerifyRollback:
return "重新核查回滚结果"
}
return "未知动作"
}