feat: v0.3.1 — Go/Gin 后端重写 + gh-ost 集成
架构变更: - Python/FastAPI → Go/Gin 重写(Python 后端保留在 backend-python/) - GORM + SQLite(兼容现有数据库表结构) - JWT 认证 + RBAC 权限中间件 Go 后端结构: - cmd/server/main.go — 入口,端口 9000 - config/config.go — 环境变量配置 - internal/model/ — GORM 模型(30张表,完全兼容 SQLite) - internal/handler/ — auth/instance/ticket/dashboard/ghost handler - internal/middleware/ — JWT 认证 + CORS - internal/ghost/ — gh-ost 集成模块(同步/异步执行,任务管理) - router/router.go — API 路由注册(/api/v1/...) gh-ost 集成: - RunGhOst() 同步执行 - RunGhOstAsync() 异步执行 + 进度追踪 - TaskManager 管理多个并发任务 - API: POST /ghost/dry-run, /ghost/execute, GET /ghost/status/:taskId 核心 API 已对齐: - POST /auth/login (JWT) - GET /dashboard/stats, /dashboard/my-tickets - GET /tickets/statistics, CRUD + approve/execute - GET/POST/PUT/DELETE /instances - GET /users, /audit-rules, /announcements, /settings 已验证: - 登录 admin/admin123 ✅ - Dashboard 99工单/110查询/4实例 ✅ - 工单统计按角色过滤 ✅
This commit is contained in:
1 parent
43d1f3a234
commit
ca308e277d
202 files changed
+2265
-51181
No files matched your search
@@ -0,0 +1,233 @@
|
||||
package ghost
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GhOstTask defines a gh-ost execution task
|
||||
type GhOstTask struct {
|
||||
Host string `json:"host"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Database string `json:"database"`
|
||||
Table string `json:"table"`
|
||||
Alter string `json:"alter"` // e.g. "ADD COLUMN status INT NOT NULL DEFAULT 0"
|
||||
ChunkSize int `json:"chunk_size,omitempty"`
|
||||
MaxLoad string `json:"max_load,omitempty"`
|
||||
CriticalLoad string `json:"critical_load,omitempty"`
|
||||
MaxLagMillis int `json:"max_lag_millis,omitempty"`
|
||||
AllowOnMaster bool `json:"allow_on_master"`
|
||||
DryRun bool `json:"dry_run"`
|
||||
Execute bool `json:"execute"`
|
||||
ExactRowCount bool `json:"exact_row_count"`
|
||||
PostponeCutOver bool `json:"postpone_cut_over"`
|
||||
}
|
||||
|
||||
// GhOstResult holds the execution result
|
||||
type GhOstResult struct {
|
||||
Success bool `json:"success"`
|
||||
Output string `json:"output"`
|
||||
Error string `json:"error"`
|
||||
Command string `json:"command"`
|
||||
Duration string `json:"duration"`
|
||||
TaskID string `json:"task_id"`
|
||||
}
|
||||
|
||||
// ProgressCallback is called with stdout lines for real-time progress
|
||||
type ProgressCallback func(taskID string, line string)
|
||||
|
||||
// TaskManager manages async gh-ost tasks
|
||||
type TaskManager struct {
|
||||
mu sync.RWMutex
|
||||
tasks map[string]*GhOstResult
|
||||
status map[string]string // running, completed, failed
|
||||
}
|
||||
|
||||
var Manager = &TaskManager{
|
||||
tasks: make(map[string]*GhOstResult),
|
||||
status: make(map[string]string),
|
||||
}
|
||||
|
||||
// RunGhOst executes gh-ost synchronously
|
||||
func RunGhOst(ctx context.Context, task GhOstTask, ghOstPath string) (*GhOstResult, error) {
|
||||
start := time.Now()
|
||||
|
||||
args := []string{
|
||||
"--host=" + task.Host,
|
||||
"--user=" + task.User,
|
||||
"--password=" + task.Password,
|
||||
"--database=" + task.Database,
|
||||
"--table=" + task.Table,
|
||||
"--alter=" + task.Alter,
|
||||
"--verbose",
|
||||
}
|
||||
|
||||
if task.ChunkSize > 0 {
|
||||
args = append(args, fmt.Sprintf("--chunk-size=%d", task.ChunkSize))
|
||||
}
|
||||
if task.MaxLoad != "" {
|
||||
args = append(args, "--max-load="+task.MaxLoad)
|
||||
}
|
||||
if task.CriticalLoad != "" {
|
||||
args = append(args, "--critical-load="+task.CriticalLoad)
|
||||
}
|
||||
if task.MaxLagMillis > 0 {
|
||||
args = append(args, fmt.Sprintf("--max-lag-millis=%d", task.MaxLagMillis))
|
||||
}
|
||||
if task.AllowOnMaster {
|
||||
args = append(args, "--allow-on-master")
|
||||
}
|
||||
if task.ExactRowCount {
|
||||
args = append(args, "--exact-rowcount")
|
||||
}
|
||||
if task.PostponeCutOver {
|
||||
args = append(args, "--postpone-cut-over-flag-file=/tmp/gh-ost-cutover.flag")
|
||||
}
|
||||
|
||||
// Dry-Run vs Execute
|
||||
if task.DryRun || !task.Execute {
|
||||
args = append(args, "--dry-run")
|
||||
} else {
|
||||
args = append(args, "--execute")
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, ghOstPath, args...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
|
||||
result := &GhOstResult{
|
||||
Command: "gh-ost " + strings.Join(args, " "),
|
||||
Duration: time.Since(start).String(),
|
||||
Output: stdout.String(),
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
result.Success = false
|
||||
result.Error = stderr.String() + "\n" + err.Error()
|
||||
return result, err
|
||||
}
|
||||
|
||||
result.Success = true
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RunGhOstAsync runs gh-ost in background and tracks status
|
||||
func RunGhOstAsync(task GhOstTask, ghOstPath string, cb ProgressCallback) string {
|
||||
taskID := fmt.Sprintf("ghost-%d", time.Now().UnixNano())
|
||||
|
||||
Manager.mu.Lock()
|
||||
Manager.status[taskID] = "running"
|
||||
Manager.tasks[taskID] = &GhOstResult{TaskID: taskID}
|
||||
Manager.mu.Unlock()
|
||||
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
|
||||
args := buildArgs(task, ghOstPath)
|
||||
|
||||
cmd := exec.CommandContext(ctx, ghOstPath, args...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
|
||||
Manager.mu.Lock()
|
||||
defer Manager.mu.Unlock()
|
||||
|
||||
result := &GhOstResult{
|
||||
TaskID: taskID,
|
||||
Command: "gh-ost " + strings.Join(args, " "),
|
||||
Duration: time.Since(start).String(),
|
||||
Output: stdout.String(),
|
||||
Success: err == nil,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
result.Error = stderr.String() + "\n" + err.Error()
|
||||
Manager.status[taskID] = "failed"
|
||||
} else {
|
||||
Manager.status[taskID] = "completed"
|
||||
}
|
||||
|
||||
Manager.tasks[taskID] = result
|
||||
|
||||
if cb != nil {
|
||||
cb(taskID, result.Output)
|
||||
}
|
||||
}()
|
||||
|
||||
return taskID
|
||||
}
|
||||
|
||||
// GetTaskStatus returns the status of an async task
|
||||
func (m *TaskManager) GetTaskStatus(taskID string) (string, *GhOstResult) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.status[taskID], m.tasks[taskID]
|
||||
}
|
||||
|
||||
// ListTasks returns all tracked tasks
|
||||
func (m *TaskManager) ListTasks() map[string]string {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
result := make(map[string]string)
|
||||
for k, v := range m.status {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func buildArgs(task GhOstTask, ghOstPath string) []string {
|
||||
args := []string{
|
||||
"--host=" + task.Host,
|
||||
"--user=" + task.User,
|
||||
"--password=" + task.Password,
|
||||
"--database=" + task.Database,
|
||||
"--table=" + task.Table,
|
||||
"--alter=" + task.Alter,
|
||||
"--verbose",
|
||||
}
|
||||
|
||||
if task.ChunkSize > 0 {
|
||||
args = append(args, fmt.Sprintf("--chunk-size=%d", task.ChunkSize))
|
||||
}
|
||||
if task.MaxLoad != "" {
|
||||
args = append(args, "--max-load="+task.MaxLoad)
|
||||
}
|
||||
if task.CriticalLoad != "" {
|
||||
args = append(args, "--critical-load="+task.CriticalLoad)
|
||||
}
|
||||
if task.MaxLagMillis > 0 {
|
||||
args = append(args, fmt.Sprintf("--max-lag-millis=%d", task.MaxLagMillis))
|
||||
}
|
||||
if task.AllowOnMaster {
|
||||
args = append(args, "--allow-on-master")
|
||||
}
|
||||
if task.ExactRowCount {
|
||||
args = append(args, "--exact-rowcount")
|
||||
}
|
||||
if task.PostponeCutOver {
|
||||
args = append(args, "--postpone-cut-over-flag-file=/tmp/gh-ost-cutover.flag")
|
||||
}
|
||||
|
||||
if task.DryRun || !task.Execute {
|
||||
args = append(args, "--dry-run")
|
||||
} else {
|
||||
args = append(args, "--execute")
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
Reference in new issue
Block a user