This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user