Files
ai-ticket-platform/infra/scripts/drill-restore.ps1

195 lines
6.8 KiB
PowerShell
Raw 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.
[CmdletBinding()]
param(
[string]$ComposeFile = "",
[switch]$SkipBuild
)
$ErrorActionPreference = "Stop"
$scriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path
$infraDirectory = [IO.Path]::GetFullPath((Join-Path $scriptDirectory ".."))
$projectDirectory = [IO.Path]::GetFullPath((Join-Path $infraDirectory ".."))
if (-not $ComposeFile) {
$ComposeFile = Join-Path $infraDirectory "docker-compose.prod.yml"
}
$ComposeFile = (Resolve-Path -LiteralPath $ComposeFile).Path
$projectName = "ai-ticket-restore-drill-$PID"
$databasePassword = "restore-drill-postgres-$PID"
$minioUser = "restore-drill-minio"
$minioPassword = "restore-drill-minio-$PID"
$backupRoot = Join-Path $projectDirectory "backups\$projectName"
$stopwatch = [Diagnostics.Stopwatch]::StartNew()
$savedEnvironment = @{
COMPOSE_PROJECT_NAME = $env:COMPOSE_PROJECT_NAME
POSTGRES_DB = $env:POSTGRES_DB
POSTGRES_USER = $env:POSTGRES_USER
POSTGRES_PASSWORD = $env:POSTGRES_PASSWORD
MINIO_ROOT_USER = $env:MINIO_ROOT_USER
MINIO_ROOT_PASSWORD = $env:MINIO_ROOT_PASSWORD
}
$env:COMPOSE_PROJECT_NAME = $projectName
$env:POSTGRES_DB = "ai_ticket_platform"
$env:POSTGRES_USER = "ai_ticket"
$env:POSTGRES_PASSWORD = $databasePassword
$env:MINIO_ROOT_USER = $minioUser
$env:MINIO_ROOT_PASSWORD = $minioPassword
function Invoke-Docker {
param([Parameter(Mandatory = $true)][string[]]$Arguments)
& docker @Arguments
if ($LASTEXITCODE -ne 0) {
throw "Docker 执行失败:docker $($Arguments -join ' ')"
}
}
function Invoke-DjangoCode {
param(
[Parameter(Mandatory = $true)][string[]]$RunPrefix,
[Parameter(Mandatory = $true)][string]$Code,
[Parameter(Mandatory = $true)][string]$FailureMessage
)
$bytes = [Text.Encoding]::UTF8.GetBytes($Code)
$runner = "exec(bytes([" + ($bytes -join ",") + "]))"
& docker @RunPrefix python manage.py shell -c $runner
if ($LASTEXITCODE -ne 0) {
throw $FailureMessage
}
}
$backendEnvironment = @(
"-e", "POSTGRES_HOST=postgres",
"-e", "POSTGRES_DB=ai_ticket_platform",
"-e", "POSTGRES_USER=ai_ticket",
"-e", "POSTGRES_PASSWORD=$databasePassword",
"-e", "MINIO_ENDPOINT=minio:9000",
"-e", "MINIO_ROOT_USER=$minioUser",
"-e", "MINIO_ROOT_PASSWORD=$minioPassword",
"-e", "CELERY_BROKER_URL=redis://redis:6379/0",
"-e", "CELERY_RESULT_BACKEND=redis://redis:6379/1"
)
$seedCode = @'
from django.contrib.auth import get_user_model
from apps.tickets.models import Ticket, TicketAttachment
user, _ = get_user_model().objects.get_or_create(
username="restore_drill",
defaults={"email": "restore-drill@example.invalid"},
)
ticket, _ = Ticket.objects.update_or_create(
ticket_no="RESTORE-DRILL-001",
defaults={
"title": "隔离恢复演练工单",
"description": "restore-drill-marker",
"requester": user,
"created_by": user,
},
)
TicketAttachment.objects.update_or_create(
ticket=ticket,
storage_path="restore-drill/marker.txt",
defaults={
"file_name": "marker.txt",
"mime_type": "text/plain",
"file_size": 20,
"created_by": user,
},
)
print(f"seeded_ticket={ticket.ticket_no}")
'@
$deleteCode = @'
from apps.tickets.models import Ticket
Ticket.objects.filter(ticket_no="RESTORE-DRILL-001").delete()
print("seed_deleted=true")
'@
$verifyCode = @'
from apps.tickets.models import Ticket
ticket = Ticket.objects.get(ticket_no="RESTORE-DRILL-001")
attachment = ticket.attachments.get(storage_path="restore-drill/marker.txt")
assert ticket.description == "restore-drill-marker"
assert attachment.file_name == "marker.txt"
print(f"restored_ticket={ticket.ticket_no}")
print(f"restored_attachment={attachment.storage_path}")
'@
try {
Invoke-Docker @("compose", "-f", $ComposeFile, "up", "-d", "--wait", "postgres", "minio")
if (-not $SkipBuild) {
Invoke-Docker @("compose", "-f", $ComposeFile, "build", "backend")
}
$runPrefix = @(
"compose", "-f", $ComposeFile,
"run", "--rm", "-T", "--no-deps"
) + $backendEnvironment + @("backend")
Invoke-Docker ($runPrefix + @("python", "manage.py", "migrate", "--noinput"))
Invoke-DjangoCode -RunPrefix $runPrefix -Code $seedCode -FailureMessage "演练数据创建失败"
Invoke-Docker @(
"compose", "-f", $ComposeFile, "exec", "-T", "minio",
"sh", "-c",
'mc alias set drill http://127.0.0.1:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null && mc mb --ignore-existing drill/ai-ticket-platform >/dev/null && printf restore-drill-marker | mc pipe drill/ai-ticket-platform/restore-drill/marker.txt >/dev/null'
)
& (Join-Path $scriptDirectory "backup.ps1") `
-ComposeFile $ComposeFile `
-BackupRoot $backupRoot `
-PostgresDatabase "ai_ticket_platform" `
-PostgresUser "ai_ticket" `
-SkipApplicationServices
$backupDirectory = Get-ChildItem -LiteralPath $backupRoot -Directory |
Sort-Object Name -Descending |
Select-Object -First 1
if (-not $backupDirectory) { throw "未找到演练备份目录" }
Invoke-DjangoCode -RunPrefix $runPrefix -Code $deleteCode -FailureMessage "演练数据破坏步骤失败"
Invoke-Docker @(
"compose", "-f", $ComposeFile, "exec", "-T", "minio",
"mc", "rm", "--force", "drill/ai-ticket-platform/restore-drill/marker.txt"
)
& (Join-Path $scriptDirectory "restore.ps1") `
-BackupDirectory $backupDirectory.FullName `
-ComposeFile $ComposeFile `
-PostgresDatabase "ai_ticket_platform" `
-PostgresUser "ai_ticket" `
-Confirm "restore-ai-ticket-platform" `
-SkipApplicationServices
Invoke-DjangoCode -RunPrefix $runPrefix -Code $verifyCode -FailureMessage "数据库业务冒烟失败"
$objectMarker = & docker compose -f $ComposeFile exec -T minio `
mc cat drill/ai-ticket-platform/restore-drill/marker.txt
if ($LASTEXITCODE -ne 0 -or "$objectMarker".Trim() -ne "restore-drill-marker") {
throw "对象存储业务冒烟失败"
}
Invoke-Docker ($runPrefix + @("python", "manage.py", "check"))
$stopwatch.Stop()
[pscustomobject]@{
status = "passed"
compose_project = $projectName
backup_id = $backupDirectory.Name
duration_seconds = [Math]::Round($stopwatch.Elapsed.TotalSeconds, 2)
database_smoke = "ticket_and_attachment_restored"
object_storage_smoke = "marker_restored"
django_check = "passed"
backup_directory = $backupDirectory.FullName
} | ConvertTo-Json
}
finally {
& docker compose -f $ComposeFile down -v --remove-orphans --rmi local
foreach ($entry in $savedEnvironment.GetEnumerator()) {
if ($null -eq $entry.Value) {
Remove-Item "Env:$($entry.Key)" -ErrorAction SilentlyContinue
}
else {
Set-Item "Env:$($entry.Key)" $entry.Value
}
}
}