305 lines
8.6 KiB
Go
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,
|
|
})
|
|
}
|