fix: fstab write fails on bind-mounted /host-fstab (EBUSY)
rename() fails with EBUSY on Docker bind-mounted files. Add safeWriteFile() helper that tries atomic rename first, falls back to direct write. Fixes both init wizard and attach wizard fstab operations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -359,15 +359,7 @@ func appendBindFstabEntry(fstabPath, source, target string) error {
|
||||
entry := fmt.Sprintf("\n# Bind mount (auto-generated by felhom-controller)\n%s\t%s\tnone\tbind,nofail\t0 0\n", source, target)
|
||||
newContent := append(existing, []byte(entry)...)
|
||||
|
||||
tmpPath := fstabPath + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, newContent, 0644); err != nil {
|
||||
return fmt.Errorf("cannot write fstab tmp file: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, fstabPath); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("cannot rename fstab tmp file: %w", err)
|
||||
}
|
||||
return nil
|
||||
return safeWriteFile(fstabPath, newContent, 0644)
|
||||
}
|
||||
|
||||
// removeBindFstabEntry removes the bind mount fstab entry for the given target mount path.
|
||||
@@ -395,14 +387,5 @@ func removeBindFstabEntry(fstabPath, targetMountPath string) error {
|
||||
kept = append(kept, line)
|
||||
}
|
||||
|
||||
newContent := strings.Join(kept, "\n")
|
||||
tmpPath := fstabPath + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, []byte(newContent), 0644); err != nil {
|
||||
return fmt.Errorf("cannot write fstab tmp file: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, fstabPath); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("cannot rename fstab tmp file: %w", err)
|
||||
}
|
||||
return nil
|
||||
return safeWriteFile(fstabPath, []byte(strings.Join(kept, "\n")), 0644)
|
||||
}
|
||||
|
||||
@@ -105,10 +105,10 @@ func BackupFstab(fstabPath string) error {
|
||||
return os.WriteFile(backupPath, data, 0644)
|
||||
}
|
||||
|
||||
// AppendFstabEntry appends a UUID-based fstab entry atomically (write tmp + rename).
|
||||
// H8: Direct write to /etc/fstab risks corruption on crash — use atomic write pattern.
|
||||
// AppendFstabEntry appends a UUID-based fstab entry.
|
||||
// Uses atomic rename when possible, falls back to direct overwrite for bind-mounted files
|
||||
// (Docker mounts /etc/fstab as /host-fstab — rename fails with EBUSY on bind mounts).
|
||||
func AppendFstabEntry(fstabPath, uuid, mountPoint, fsType, options string) error {
|
||||
// Read existing content
|
||||
existing, err := os.ReadFile(fstabPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("cannot read fstab: %w", err)
|
||||
@@ -117,19 +117,10 @@ func AppendFstabEntry(fstabPath, uuid, mountPoint, fsType, options string) error
|
||||
entry := fmt.Sprintf("\nUUID=%s\t%s\t%s\t%s\t0 2\n", uuid, mountPoint, fsType, options)
|
||||
newContent := append(existing, []byte(entry)...)
|
||||
|
||||
// Write to .tmp then rename — atomic on same filesystem
|
||||
tmpPath := fstabPath + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, newContent, 0644); err != nil {
|
||||
return fmt.Errorf("cannot write fstab tmp file: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, fstabPath); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("cannot rename fstab tmp file: %w", err)
|
||||
}
|
||||
return nil
|
||||
return safeWriteFile(fstabPath, newContent, 0644)
|
||||
}
|
||||
|
||||
// RemoveFstabEntry removes any line containing the given UUID from fstab, atomically.
|
||||
// RemoveFstabEntry removes any line containing the given UUID from fstab.
|
||||
// H19: Called as rollback if mount fails after fstab was written.
|
||||
func RemoveFstabEntry(fstabPath, uuid string) error {
|
||||
data, err := os.ReadFile(fstabPath)
|
||||
@@ -145,13 +136,24 @@ func RemoveFstabEntry(fstabPath, uuid string) error {
|
||||
}
|
||||
newContent := strings.Join(kept, "\n")
|
||||
|
||||
tmpPath := fstabPath + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, []byte(newContent), 0644); err != nil {
|
||||
return fmt.Errorf("cannot write fstab tmp file: %w", err)
|
||||
return safeWriteFile(fstabPath, []byte(newContent), 0644)
|
||||
}
|
||||
|
||||
// safeWriteFile writes content to a file. It tries atomic rename first (write to .tmp,
|
||||
// then rename). If rename fails (e.g., bind-mounted files where rename returns EBUSY),
|
||||
// it falls back to a direct truncate-and-write to the target file.
|
||||
func safeWriteFile(path string, content []byte, perm os.FileMode) error {
|
||||
tmpPath := path + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, content, perm); err != nil {
|
||||
return fmt.Errorf("cannot write tmp file %s: %w", tmpPath, err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, fstabPath); err != nil {
|
||||
if err := os.Rename(tmpPath, path); err == nil {
|
||||
return nil // atomic rename succeeded
|
||||
}
|
||||
// Rename failed (likely bind mount) — fall back to direct write
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("cannot rename fstab tmp file: %w", err)
|
||||
if err := os.WriteFile(path, content, perm); err != nil {
|
||||
return fmt.Errorf("cannot write %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user