Files
atlas/internal/httpapp/backup_handlers.go
Othman H. Suseno a7ba6c83ea
Some checks failed
CI / test-build (push) Has been cancelled
switch to postgresql
2025-12-16 01:31:27 +07:00

305 lines
8.6 KiB
Go

package httpapp
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"gitea.avt.data-center.id/othman.suseno/atlas/internal/backup"
"gitea.avt.data-center.id/othman.suseno/atlas/internal/errors"
"gitea.avt.data-center.id/othman.suseno/atlas/internal/models"
)
// Backup Handlers
func (a *App) handleCreateBackup(w http.ResponseWriter, r *http.Request) {
var req struct {
Description string `json:"description,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
// Description is optional, so we'll continue even if body is empty
_ = err
}
// Collect all configuration data
backupData := backup.BackupData{
Users: a.userStore.List(),
SMBShares: a.smbStore.List(),
NFSExports: a.nfsStore.List(),
ISCSITargets: a.iscsiStore.List(),
Policies: a.snapshotPolicy.List(),
Config: map[string]interface{}{
"database_conn": a.cfg.DatabaseConn,
},
}
// Create backup
backupID, err := a.backupService.CreateBackup(backupData, req.Description)
if err != nil {
log.Printf("create backup error: %v", err)
writeError(w, errors.ErrInternal("failed to create backup").WithDetails(err.Error()))
return
}
// Get backup metadata
metadata, err := a.backupService.GetBackup(backupID)
if err != nil {
log.Printf("get backup metadata error: %v", err)
writeJSON(w, http.StatusCreated, map[string]interface{}{
"id": backupID,
"message": "backup created",
})
return
}
writeJSON(w, http.StatusCreated, metadata)
}
func (a *App) handleListBackups(w http.ResponseWriter, r *http.Request) {
backups, err := a.backupService.ListBackups()
if err != nil {
log.Printf("list backups error: %v", err)
writeError(w, errors.ErrInternal("failed to list backups").WithDetails(err.Error()))
return
}
writeJSON(w, http.StatusOK, backups)
}
func (a *App) handleGetBackup(w http.ResponseWriter, r *http.Request) {
backupID := pathParam(r, "/api/v1/backups/")
if backupID == "" {
writeError(w, errors.ErrBadRequest("backup id required"))
return
}
metadata, err := a.backupService.GetBackup(backupID)
if err != nil {
log.Printf("get backup error: %v", err)
writeError(w, errors.ErrNotFound("backup").WithDetails(backupID))
return
}
writeJSON(w, http.StatusOK, metadata)
}
func (a *App) handleRestoreBackup(w http.ResponseWriter, r *http.Request) {
// Extract backup ID from path
path := r.URL.Path
backupID := ""
// Handle both /api/v1/backups/{id} and /api/v1/backups/{id}/restore
if strings.Contains(path, "/restore") {
// Path: /api/v1/backups/{id}/restore
prefix := "/api/v1/backups/"
suffix := "/restore"
if strings.HasPrefix(path, prefix) && strings.HasSuffix(path, suffix) {
backupID = path[len(prefix) : len(path)-len(suffix)]
}
} else {
// Path: /api/v1/backups/{id}
backupID = pathParam(r, "/api/v1/backups/")
}
if backupID == "" {
writeError(w, errors.ErrBadRequest("backup id required"))
return
}
var req struct {
DryRun bool `json:"dry_run,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
// Dry run is optional, default to false
req.DryRun = false
}
// Verify backup first
if err := a.backupService.VerifyBackup(backupID); err != nil {
log.Printf("verify backup error: %v", err)
writeError(w, errors.ErrBadRequest("backup verification failed").WithDetails(err.Error()))
return
}
// Restore backup
backupData, err := a.backupService.RestoreBackup(backupID)
if err != nil {
log.Printf("restore backup error: %v", err)
writeError(w, errors.ErrInternal("failed to restore backup").WithDetails(err.Error()))
return
}
if req.DryRun {
// Return what would be restored without actually restoring
writeJSON(w, http.StatusOK, map[string]interface{}{
"message": "dry run - no changes made",
"backup_id": backupID,
"backup_data": backupData,
})
return
}
// Restore users (skip default admin user - user-1)
// Note: Passwords cannot be restored as they're hashed and not stored in user model
// Users will need to reset their passwords after restore
for _, user := range backupData.Users {
// Skip default admin user
if user.ID == "user-1" {
log.Printf("skipping default admin user")
continue
}
// Check if user already exists
if _, err := a.userStore.GetByID(user.ID); err == nil {
log.Printf("user %s already exists, skipping", user.ID)
continue
}
// Create user with temporary password (user must reset password)
// Use a secure random password that user must change
tempPassword := fmt.Sprintf("restore-%s", user.ID)
if _, err := a.userStore.Create(user.Username, user.Email, tempPassword, user.Role); err != nil {
log.Printf("restore user error: %v", err)
// Continue with other users
} else {
log.Printf("restored user %s - password reset required", user.Username)
}
}
// Restore SMB shares
for _, share := range backupData.SMBShares {
// Check if share already exists
if _, err := a.smbStore.Get(share.ID); err == nil {
log.Printf("SMB share %s already exists, skipping", share.ID)
continue
}
// Create share
if _, err := a.smbStore.Create(share.Name, share.Path, share.Dataset, share.Description, share.ReadOnly, share.GuestOK, share.ValidUsers); err != nil {
log.Printf("restore SMB share error: %v", err)
// Continue with other shares
}
}
// Restore NFS exports
for _, export := range backupData.NFSExports {
// Check if export already exists
if _, err := a.nfsStore.Get(export.ID); err == nil {
log.Printf("NFS export %s already exists, skipping", export.ID)
continue
}
// Create export
if _, err := a.nfsStore.Create(export.Path, export.Dataset, export.Clients, export.ReadOnly, export.RootSquash); err != nil {
log.Printf("restore NFS export error: %v", err)
// Continue with other exports
}
}
// Restore iSCSI targets
for _, target := range backupData.ISCSITargets {
// Check if target already exists
if _, err := a.iscsiStore.Get(target.ID); err == nil {
log.Printf("iSCSI target %s already exists, skipping", target.ID)
continue
}
// Create target
if _, err := a.iscsiStore.Create(target.IQN, target.Initiators); err != nil {
log.Printf("restore iSCSI target error: %v", err)
// Continue with other targets
}
// Restore LUNs
for _, lun := range target.LUNs {
if _, err := a.iscsiStore.AddLUN(target.ID, lun.ZVOL, lun.Size); err != nil {
log.Printf("restore iSCSI LUN error: %v", err)
// Continue with other LUNs
}
}
}
// Restore snapshot policies
for _, policy := range backupData.Policies {
// Check if policy already exists
if existing, _ := a.snapshotPolicy.Get(policy.Dataset); existing != nil {
log.Printf("snapshot policy for dataset %s already exists, skipping", policy.Dataset)
continue
}
// Set policy (uses Dataset as key)
a.snapshotPolicy.Set(&policy)
}
// Apply service configurations
shares := a.smbStore.List()
if err := a.smbService.ApplyConfiguration(shares); err != nil {
log.Printf("apply SMB configuration after restore error: %v", err)
}
exports := a.nfsStore.List()
if err := a.nfsService.ApplyConfiguration(exports); err != nil {
log.Printf("apply NFS configuration after restore error: %v", err)
}
targets := a.iscsiStore.List()
for _, target := range targets {
if err := a.iscsiService.ApplyConfiguration([]models.ISCSITarget{target}); err != nil {
log.Printf("apply iSCSI configuration after restore error: %v", err)
}
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"message": "backup restored successfully",
"backup_id": backupID,
})
}
func (a *App) handleDeleteBackup(w http.ResponseWriter, r *http.Request) {
backupID := pathParam(r, "/api/v1/backups/")
if backupID == "" {
writeError(w, errors.ErrBadRequest("backup id required"))
return
}
if err := a.backupService.DeleteBackup(backupID); err != nil {
log.Printf("delete backup error: %v", err)
writeError(w, errors.ErrInternal("failed to delete backup").WithDetails(err.Error()))
return
}
writeJSON(w, http.StatusOK, map[string]string{
"message": "backup deleted",
"backup_id": backupID,
})
}
func (a *App) handleVerifyBackup(w http.ResponseWriter, r *http.Request) {
backupID := pathParam(r, "/api/v1/backups/")
if backupID == "" {
writeError(w, errors.ErrBadRequest("backup id required"))
return
}
if err := a.backupService.VerifyBackup(backupID); err != nil {
writeError(w, errors.ErrBadRequest("backup verification failed").WithDetails(err.Error()))
return
}
metadata, err := a.backupService.GetBackup(backupID)
if err != nil {
writeError(w, errors.ErrNotFound("backup").WithDetails(backupID))
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"message": "backup is valid",
"backup_id": backupID,
"metadata": metadata,
})
}