package services import ( "fmt" "os" "os/exec" "strings" "sync" "gitea.avt.data-center.id/othman.suseno/atlas/internal/models" ) // NFSService manages NFS service integration type NFSService struct { mu sync.RWMutex exportsPath string } // NewNFSService creates a new NFS service manager func NewNFSService() *NFSService { return &NFSService{ exportsPath: "/etc/exports", } } // ApplyConfiguration generates and applies NFS exports configuration func (s *NFSService) ApplyConfiguration(exports []models.NFSExport) error { s.mu.Lock() defer s.mu.Unlock() config, err := s.generateExports(exports) if err != nil { return fmt.Errorf("generate exports: %w", err) } // Write configuration to a temporary file first tmpPath := s.exportsPath + ".atlas.tmp" if err := os.WriteFile(tmpPath, []byte(config), 0644); err != nil { return fmt.Errorf("write exports: %w", err) } // Backup existing exports backupPath := s.exportsPath + ".backup" if _, err := os.Stat(s.exportsPath); err == nil { if err := exec.Command("cp", s.exportsPath, backupPath).Run(); err != nil { // Non-fatal, log but continue } } // Atomically replace exports file if err := os.Rename(tmpPath, s.exportsPath); err != nil { return fmt.Errorf("replace exports: %w", err) } // Reload NFS exports with error recovery reloadErr := s.reloadExports() if reloadErr != nil { // Try to restore backup on failure if _, err2 := os.Stat(backupPath); err2 == nil { if restoreErr := os.Rename(backupPath, s.exportsPath); restoreErr != nil { return fmt.Errorf("reload failed and backup restore failed: reload=%v, restore=%v", reloadErr, restoreErr) } } return fmt.Errorf("reload exports: %w", reloadErr) } return nil } // generateExports generates /etc/exports format from NFS exports func (s *NFSService) generateExports(exports []models.NFSExport) (string, error) { var b strings.Builder for _, export := range exports { if !export.Enabled { continue } // Build export options var options []string if export.ReadOnly { options = append(options, "ro") } else { options = append(options, "rw") } if export.RootSquash { options = append(options, "root_squash") } else { options = append(options, "no_root_squash") } options = append(options, "sync", "subtree_check") // Format: path client1(options) client2(options) optStr := "(" + strings.Join(options, ",") + ")" if len(export.Clients) == 0 { // Default to all clients if none specified b.WriteString(fmt.Sprintf("%s *%s\n", export.Path, optStr)) } else { for _, client := range export.Clients { b.WriteString(fmt.Sprintf("%s %s%s\n", export.Path, client, optStr)) } } } return b.String(), nil } // reloadExports reloads NFS exports func (s *NFSService) reloadExports() error { // Use exportfs -ra to reload all exports cmd := exec.Command("exportfs", "-ra") if err := cmd.Run(); err != nil { return fmt.Errorf("exportfs failed: %w", err) } return nil } // ValidateConfiguration validates NFS exports syntax func (s *NFSService) ValidateConfiguration(exports string) error { // Use exportfs -v to validate (dry-run) cmd := exec.Command("exportfs", "-v") cmd.Stdin = strings.NewReader(exports) // Note: exportfs doesn't have a direct validation mode // We'll rely on the reload to catch errors return nil } // GetStatus returns the status of NFS service func (s *NFSService) GetStatus() (bool, error) { // Check if nfs-server is running cmd := exec.Command("systemctl", "is-active", "nfs-server") if err := cmd.Run(); err == nil { return true, nil } // Fallback: check process cmd = exec.Command("pgrep", "-x", "nfsd") if err := cmd.Run(); err == nil { return true, nil } return false, nil }