diff --git a/internal/httpapp/api_handlers.go b/internal/httpapp/api_handlers.go index df7538a..2cc0cec 100644 --- a/internal/httpapp/api_handlers.go +++ b/internal/httpapp/api_handlers.go @@ -281,13 +281,50 @@ func (a *App) handleCreateDataset(w http.ResponseWriter, r *http.Request) { req.Options = make(map[string]string) } - if err := a.zfs.CreateDataset(req.Name, req.Options); err != nil { - log.Printf("create dataset error: %v", err) - writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + err := a.zfs.CreateDataset(req.Name, req.Options) + + // CRITICAL: Always check if dataset exists, regardless of reported error + // ZFS often reports mountpoint errors but dataset is still created + // The CreateDataset function already does retries, but we double-check here + // Wait a brief moment for dataset to be fully registered + time.Sleep(300 * time.Millisecond) + + datasets, getErr := a.zfs.ListDatasets("") + var datasetExists bool + if getErr == nil { + for _, ds := range datasets { + if ds.Name == req.Name { + datasetExists = true + break + } + } + } + + if datasetExists { + // Dataset exists - this is success! + if err != nil { + log.Printf("info: dataset %s created successfully despite CreateDataset reporting error: %v", req.Name, err) + } else { + log.Printf("info: dataset %s created successfully", req.Name) + } + // Set cache-control headers + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") + writeJSON(w, http.StatusCreated, map[string]string{"message": "dataset created", "name": req.Name}) return } - writeJSON(w, http.StatusCreated, map[string]string{"message": "dataset created", "name": req.Name}) + // Dataset doesn't exist - return the error with detailed context + if err != nil { + log.Printf("error: dataset %s creation failed - CreateDataset error: %v, ListDatasets error: %v", req.Name, err, getErr) + writeError(w, errors.ErrInternal("failed to create dataset").WithDetails(err.Error())) + return + } + + // No error but dataset doesn't exist (shouldn't happen, but handle it) + log.Printf("warning: dataset %s creation reported no error but dataset was not found", req.Name) + writeError(w, errors.ErrInternal(fmt.Sprintf("Dataset '%s' creation reported success but dataset was not found", req.Name))) } func (a *App) handleGetDataset(w http.ResponseWriter, r *http.Request) { diff --git a/internal/zfs/service.go b/internal/zfs/service.go index ee84dae..6260860 100644 --- a/internal/zfs/service.go +++ b/internal/zfs/service.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log" + "os" "os/exec" "strconv" "strings" @@ -69,6 +70,10 @@ func translateZFSError(err error, operation, name string) error { return fmt.Errorf("pool '%s' tidak ditemukan. Pastikan nama pool benar dan pool sudah dibuat", name) } + if strings.Contains(errStr, "dataset already exists") { + return fmt.Errorf("dataset '%s' sudah ada. Gunakan nama yang berbeda atau hapus dataset yang sudah ada terlebih dahulu", name) + } + if strings.Contains(errStr, "dataset does not exist") || strings.Contains(errStr, "no such dataset") { return fmt.Errorf("dataset atau volume '%s' tidak ditemukan. Pastikan nama benar dan sudah dibuat", name) } @@ -423,21 +428,72 @@ func (s *Service) CreatePool(name string, vdevs []string, options map[string]str // createMountpointWithSudo creates a mountpoint directory using sudo // This allows ZFS to mount pools even if root filesystem appears read-only func (s *Service) createMountpointWithSudo(path string) error { - // Use sudo to create the directory with proper permissions - cmd := exec.Command("sudo", "-n", "mkdir", "-p", path) - var stderr bytes.Buffer - cmd.Stderr = &stderr + // Check if directory already exists + if _, err := os.Stat(path); err == nil { + // Directory already exists + return nil + } - if err := cmd.Run(); err != nil { - // If sudo mkdir fails, try without sudo (might already be root or have permissions) - directCmd := exec.Command("mkdir", "-p", path) - if directErr := directCmd.Run(); directErr != nil { - // Both failed, but don't return error - ZFS might handle it - // Log but continue, as ZFS might create it or mountpoint might already exist - return fmt.Errorf("failed to create mountpoint %s: %v: %s", path, err, stderr.String()) + // Create parent directories first + parentDir := "" + parts := strings.Split(path, "/") + if len(parts) > 1 { + // Build parent directory path (skip empty first part from leading /) + parentParts := []string{} + for i, part := range parts { + if i == 0 && part == "" { + continue // Skip leading empty part + } + if i < len(parts)-1 { + parentParts = append(parentParts, part) + } + } + if len(parentParts) > 0 { + parentDir = "/" + strings.Join(parentParts, "/") + if parentDir != "/" { + // Recursively create parent directories + if err := s.createMountpointWithSudo(parentDir); err != nil { + log.Printf("warning: failed to create parent directory %s: %v", parentDir, err) + } + } } } - return nil + + // Use sudo to create the directory with proper permissions + // Try multiple methods to ensure directory is created + methods := []struct { + name string + cmd *exec.Cmd + }{ + {"sudo mkdir", exec.Command("sudo", "-n", "mkdir", "-p", path)}, + {"direct mkdir", exec.Command("mkdir", "-p", path)}, + } + + var lastErr error + for _, method := range methods { + var stderr bytes.Buffer + method.cmd.Stderr = &stderr + + if err := method.cmd.Run(); err == nil { + // Success - verify directory was created and set permissions + if _, err := os.Stat(path); err == nil { + // Set proper permissions (755) and ownership if needed + chmodCmd := exec.Command("sudo", "-n", "chmod", "755", path) + _ = chmodCmd.Run() // Ignore errors, permissions might already be correct + return nil + } + } else { + lastErr = fmt.Errorf("%s failed: %v: %s", method.name, err, stderr.String()) + log.Printf("warning: %s failed: %v", method.name, lastErr) + } + } + + // All methods failed, but check if directory exists anyway (might have been created by ZFS or another process) + if _, err := os.Stat(path); err == nil { + return nil + } + + return fmt.Errorf("all methods failed to create mountpoint %s: %v", path, lastErr) } // DestroyPool destroys a ZFS pool @@ -708,27 +764,197 @@ func (s *Service) CreateDataset(name string, options map[string]string) error { _ = s.createMountpointWithSudo(mountpoint) } + // Handle canmount property - set to "on" by default to allow mounting + canmount := "on" + if v, ok := options["canmount"]; ok && v != "" { + canmount = v + } + delete(options, "canmount") + // Add options for k, v := range options { args = append(args, "-o", fmt.Sprintf("%s=%s", k, v)) } + // Add canmount property + args = append(args, "-o", fmt.Sprintf("canmount=%s", canmount)) + args = append(args, name) _, err := s.execCommand(s.zfsPath, args...) - return err + + // CRITICAL: Always check if dataset exists, even if creation reported an error + // ZFS often reports mountpoint errors but dataset is still created successfully + // Retry checking dataset existence up to 3 times with delays + datasetExists := false + for i := 0; i < 3; i++ { + if i > 0 { + // Wait before retry (100ms, 200ms, 300ms) + time.Sleep(time.Duration(i*100) * time.Millisecond) + } + + if existingDatasets, listErr := s.ListDatasets(""); listErr == nil { + log.Printf("checking dataset existence (attempt %d/%d): found %d datasets", i+1, 3, len(existingDatasets)) + for _, ds := range existingDatasets { + if ds.Name == name { + datasetExists = true + log.Printf("dataset %s found after %d check(s)", name, i+1) + break + } + } + } else { + log.Printf("warning: failed to list datasets during existence check (attempt %d): %v", i+1, listErr) + } + + if datasetExists { + break + } + } + + if datasetExists { + // Dataset exists! This is success, regardless of any reported errors + if err != nil { + log.Printf("info: dataset %s created successfully despite reported error: %v", name, err) + } else { + log.Printf("info: dataset %s created successfully", name) + } + + // Dataset created successfully - now set mountpoint and mount if needed + if mountpoint != "" && mountpoint != "none" { + // CRITICAL: Create parent directory first if it doesn't exist + // This is needed because ZFS can't create directories in read-only filesystems + parentDir := "" + parts := strings.Split(mountpoint, "/") + if len(parts) > 1 { + // Build parent directory path + parentDir = strings.Join(parts[:len(parts)-1], "/") + if parentDir == "" { + parentDir = "/" + } + log.Printf("ensuring parent directory exists: %s", parentDir) + _ = s.createMountpointWithSudo(parentDir) + } + + // CRITICAL: Create mountpoint directory BEFORE setting mountpoint property + // ZFS will try to create it during mount, but may fail on read-only filesystem + // So we create it explicitly with sudo first + log.Printf("creating mountpoint directory: %s", mountpoint) + if err := s.createMountpointWithSudo(mountpoint); err != nil { + log.Printf("warning: failed to create mountpoint %s: %v (will try to continue)", mountpoint, err) + } else { + log.Printf("mountpoint directory created successfully: %s", mountpoint) + } + + // Set mountpoint property on the dataset + log.Printf("setting mountpoint property: %s = %s", name, mountpoint) + setMountpointArgs := []string{"set", fmt.Sprintf("mountpoint=%s", mountpoint), name} + if _, setErr := s.execCommand(s.zfsPath, setMountpointArgs...); setErr != nil { + log.Printf("warning: failed to set mountpoint property: %v (dataset created but mountpoint not set)", setErr) + } else { + log.Printf("mountpoint property set successfully") + + // Wait a moment for mountpoint to be registered + time.Sleep(200 * time.Millisecond) + + // Ensure directory exists again (ZFS might have cleared it or parent might be read-only) + // Try creating parent and child directories + if parentDir != "" && parentDir != "/" { + _ = s.createMountpointWithSudo(parentDir) + } + _ = s.createMountpointWithSudo(mountpoint) + + // Try to mount the dataset - ZFS will create the directory if it doesn't exist + // But we need to ensure parent is writable + log.Printf("attempting to mount dataset: %s", name) + mountArgs := []string{"mount", name} + if _, mountErr := s.execCommand(s.zfsPath, mountArgs...); mountErr != nil { + log.Printf("warning: failed to mount dataset: %v (dataset created but not mounted)", mountErr) + + // If mount failed due to read-only filesystem, try using legacy mount + if strings.Contains(mountErr.Error(), "Read-only file system") || strings.Contains(mountErr.Error(), "read-only") { + log.Printf("detected read-only filesystem issue, trying alternative approach") + // Try to remount parent as rw if possible, or use a different mountpoint + // For now, just log the issue - user may need to manually mount + log.Printf("error: cannot mount dataset due to read-only filesystem at %s. You may need to manually mount it or fix filesystem permissions", mountpoint) + } else { + // Try one more time after a short delay for other errors + time.Sleep(300 * time.Millisecond) + if _, mountErr2 := s.execCommand(s.zfsPath, mountArgs...); mountErr2 != nil { + log.Printf("warning: second mount attempt also failed: %v", mountErr2) + } else { + log.Printf("info: dataset %s mounted successfully at %s (on second attempt)", name, mountpoint) + } + } + } else { + log.Printf("info: dataset %s mounted successfully at %s", name, mountpoint) + } + } + } + + // Clear error since dataset was created + return nil + } else if err != nil { + // Dataset doesn't exist and we have an error - return translated error + log.Printf("error: dataset %s creation failed and dataset does not exist", name) + return translateZFSError(err, "membuat dataset", name) + } else { + // No error reported but dataset doesn't exist - this shouldn't happen + log.Printf("warning: dataset %s creation reported no error but dataset does not exist", name) + return translateZFSError(fmt.Errorf("dataset creation reported success but dataset was not found"), "membuat dataset", name) + } } // DestroyDataset destroys a ZFS dataset func (s *Service) DestroyDataset(name string, recursive bool) error { + // Always try to unmount first, regardless of mounted status + // This prevents "dataset is busy" errors + log.Printf("attempting to unmount dataset %s before destroy", name) + unmountArgs := []string{"umount", name} + unmountOutput, unmountErr := s.execCommand(s.zfsPath, unmountArgs...) + if unmountErr != nil { + log.Printf("regular unmount failed for %s: %v (output: %s), trying force unmount", name, unmountErr, unmountOutput) + // Try force unmount if regular unmount fails + forceUnmountArgs := []string{"umount", "-f", name} + forceOutput, forceErr := s.execCommand(s.zfsPath, forceUnmountArgs...) + if forceErr != nil { + log.Printf("warning: force unmount also failed for %s: %v (output: %s)", name, forceErr, forceOutput) + } else { + log.Printf("dataset %s force unmounted successfully", name) + } + } else { + log.Printf("dataset %s unmounted successfully", name) + } + // Wait a moment for unmount to complete + time.Sleep(300 * time.Millisecond) + + // Now destroy the dataset args := []string{"destroy"} if recursive { args = append(args, "-r") } args = append(args, name) - _, err := s.execCommand(s.zfsPath, args...) + destroyOutput, err := s.execCommand(s.zfsPath, args...) if err != nil { + log.Printf("first destroy attempt failed for %s: %v (output: %s)", name, err, destroyOutput) + // If destroy fails with "dataset is busy", try unmounting again and retry + if strings.Contains(err.Error(), "dataset is busy") || strings.Contains(err.Error(), "is busy") { + log.Printf("dataset %s still busy, trying force unmount again and retry destroy", name) + // Try force unmount again + forceUnmountArgs := []string{"umount", "-f", name} + _, _ = s.execCommand(s.zfsPath, forceUnmountArgs...) + time.Sleep(500 * time.Millisecond) + // Retry destroy + destroyOutput2, err2 := s.execCommand(s.zfsPath, args...) + if err2 != nil { + log.Printf("second destroy attempt also failed for %s: %v (output: %s)", name, err2, destroyOutput2) + return translateZFSError(err2, "menghapus dataset", name) + } else { + log.Printf("dataset %s destroyed successfully on second attempt", name) + return nil + } + } return translateZFSError(err, "menghapus dataset", name) } + log.Printf("dataset %s destroyed successfully", name) return nil }