114 lines
5.0 KiB
Go
114 lines
5.0 KiB
Go
package deployment
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"time"
|
|
)
|
|
|
|
func isRecoveryVerification(action RecoveryAction) bool {
|
|
return action == RecoveryActionVerifyCommit || action == RecoveryActionVerifyRollback
|
|
}
|
|
|
|
// Reconciles 仅允许已持久化结束的不确定原动作;不接管运行中处置或再次执行文件动作。
|
|
func (i RecoveryDispositionIntent) Reconciles(source RecoveryDisposition) bool {
|
|
if i.SourceDispositionID == "" || i.SourceDispositionID != source.Intent.ID || source.Intent.Validate() != nil ||
|
|
source.Status != RecoveryResultUncertain || source.ResultCode != RecoveryResultUncertain ||
|
|
i.TransactionID != source.Intent.TransactionID || i.ServerID != source.Intent.ServerID || i.ServerDigest != source.Intent.ServerDigest {
|
|
return false
|
|
}
|
|
return (i.Action == RecoveryActionVerifyCommit && source.Intent.Action == RecoveryActionCommit) ||
|
|
(i.Action == RecoveryActionVerifyRollback && source.Intent.Action == RecoveryActionRollback)
|
|
}
|
|
|
|
// PrepareRecoveryReconciliation 只供受保护本机通道调用,必须明确指定原处置编号并重新确认。
|
|
func (s *Service) PrepareRecoveryReconciliation(ctx context.Context, sourceID, operator string, resolve func(context.Context, string) (Server, error)) (RecoveryConfirmation, string, error) {
|
|
review, err := s.prepareRecoveryReconciliation(ctx, sourceID, operator, resolve)
|
|
if err != nil {
|
|
return RecoveryConfirmation{}, "", err
|
|
}
|
|
return s.newRecoveryConfirmation(ctx, review)
|
|
}
|
|
|
|
func (s *Service) prepareRecoveryReconciliation(ctx context.Context, sourceID, operator string, resolve func(context.Context, string) (Server, error)) (RecoveryReview, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
if s == nil || !validDispositionID(sourceID) || !validReviewOperator(operator) {
|
|
return RecoveryReview{}, ErrRecoveryReviewInvalid
|
|
}
|
|
journal, ok := s.journal.(RecoveryDispositionJournal)
|
|
if !ok {
|
|
return RecoveryReview{}, ErrRecoveryDispositionUnavailable
|
|
}
|
|
source, err := journal.GetRecoveryDisposition(ctx, sourceID)
|
|
if err != nil || source.Intent.ID != sourceID || source.Intent.Validate() != nil || source.Summary().Validate() != nil ||
|
|
source.Status != RecoveryResultUncertain || source.ResultCode != RecoveryResultUncertain {
|
|
return RecoveryReview{}, ErrRecoveryActionRejected
|
|
}
|
|
var action RecoveryAction
|
|
switch source.Intent.Action {
|
|
case RecoveryActionCommit:
|
|
action = RecoveryActionVerifyCommit
|
|
case RecoveryActionRollback:
|
|
action = RecoveryActionVerifyRollback
|
|
default:
|
|
return RecoveryReview{}, ErrRecoveryActionRejected
|
|
}
|
|
review, err := s.prepareRecoveryReview(ctx, source.Intent.TransactionID, operator, action, resolve)
|
|
if err != nil {
|
|
return RecoveryReview{}, err
|
|
}
|
|
review.summary.SourceDispositionID = sourceID
|
|
intent := RecoveryDispositionIntent{SourceDispositionID: sourceID, TransactionID: review.summary.TransactionID,
|
|
ServerID: review.summary.ServerID, ServerDigest: hex.EncodeToString(review.serverDigest[:]), Action: action}
|
|
if !intent.Reconciles(source) || !verificationPhase(review.summary.Phase, action) || s.VerifyRecoveryTarget == nil {
|
|
return RecoveryReview{}, ErrRecoveryActionRejected
|
|
}
|
|
return review, nil
|
|
}
|
|
|
|
func verificationPhase(phase string, action RecoveryAction) bool {
|
|
return (action == RecoveryActionVerifyCommit && phase == TransactionPhaseCommitVerified) ||
|
|
(action == RecoveryActionVerifyRollback && phase == TransactionPhaseRollbackVerified)
|
|
}
|
|
|
|
// validateRecoveryCompletion 不删除任何远端文件,残留锁或备份均要求继续保留占用。
|
|
func validateRecoveryCompletion(ctx context.Context, client Client, record TransactionRecord, committed bool) error {
|
|
for _, lock := range record.LockPaths {
|
|
if exists, err := client.Exists(ctx, lock); err != nil || exists {
|
|
return ErrRecoveryActionRejected
|
|
}
|
|
}
|
|
for _, backup := range record.Backups {
|
|
if committed {
|
|
if err := checkFileDigest(ctx, client, backup.Original, record.Evidence.Files[backup.Original]); err != nil {
|
|
return err
|
|
}
|
|
} else if backup.OriginalExists {
|
|
if err := checkFileDigest(ctx, client, backup.Original, backup.OriginalDigest); err != nil {
|
|
return err
|
|
}
|
|
} else if exists, err := client.Exists(ctx, backup.Original); err != nil || exists {
|
|
return ErrRecoveryActionRejected
|
|
}
|
|
for _, path := range []string{backup.Saved, backup.Original + ".scert-tmp-" + record.ID} {
|
|
if exists, err := client.Exists(ctx, path); err != nil || exists {
|
|
return ErrRecoveryActionRejected
|
|
}
|
|
}
|
|
}
|
|
return ctx.Err()
|
|
}
|
|
|
|
func (s *Service) verifyRecoveryCompletion(ctx context.Context, record TransactionRecord, server Server, client Client, action RecoveryAction) error {
|
|
committed := action == RecoveryActionVerifyCommit
|
|
if err := validateRecoveryCompletion(ctx, client, record, committed); err != nil {
|
|
return err
|
|
}
|
|
if err := s.VerifyRecoveryTarget(ctx, server, record.Evidence.Baseline, committed); err != nil {
|
|
return err
|
|
}
|
|
// HTTPS 检查结束后重新读取现场,避免检查期间发生的文件变化被忽略。
|
|
return validateRecoveryCompletion(ctx, client, record, committed)
|
|
}
|