This commit is contained in:
304
internal/httpapp/backup_handlers.go
Normal file
304
internal/httpapp/backup_handlers.go
Normal file
@@ -0,0 +1,304 @@
|
||||
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_path": a.cfg.DatabasePath,
|
||||
},
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user