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)
|
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)...)
|
newContent := append(existing, []byte(entry)...)
|
||||||
|
|
||||||
tmpPath := fstabPath + ".tmp"
|
return safeWriteFile(fstabPath, newContent, 0644)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeBindFstabEntry removes the bind mount fstab entry for the given target mount path.
|
// 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)
|
kept = append(kept, line)
|
||||||
}
|
}
|
||||||
|
|
||||||
newContent := strings.Join(kept, "\n")
|
return safeWriteFile(fstabPath, []byte(strings.Join(kept, "\n")), 0644)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,10 +105,10 @@ func BackupFstab(fstabPath string) error {
|
|||||||
return os.WriteFile(backupPath, data, 0644)
|
return os.WriteFile(backupPath, data, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendFstabEntry appends a UUID-based fstab entry atomically (write tmp + rename).
|
// AppendFstabEntry appends a UUID-based fstab entry.
|
||||||
// H8: Direct write to /etc/fstab risks corruption on crash — use atomic write pattern.
|
// 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 {
|
func AppendFstabEntry(fstabPath, uuid, mountPoint, fsType, options string) error {
|
||||||
// Read existing content
|
|
||||||
existing, err := os.ReadFile(fstabPath)
|
existing, err := os.ReadFile(fstabPath)
|
||||||
if err != nil && !os.IsNotExist(err) {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
return fmt.Errorf("cannot read fstab: %w", 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)
|
entry := fmt.Sprintf("\nUUID=%s\t%s\t%s\t%s\t0 2\n", uuid, mountPoint, fsType, options)
|
||||||
newContent := append(existing, []byte(entry)...)
|
newContent := append(existing, []byte(entry)...)
|
||||||
|
|
||||||
// Write to .tmp then rename — atomic on same filesystem
|
return safeWriteFile(fstabPath, newContent, 0644)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
// H19: Called as rollback if mount fails after fstab was written.
|
||||||
func RemoveFstabEntry(fstabPath, uuid string) error {
|
func RemoveFstabEntry(fstabPath, uuid string) error {
|
||||||
data, err := os.ReadFile(fstabPath)
|
data, err := os.ReadFile(fstabPath)
|
||||||
@@ -145,13 +136,24 @@ func RemoveFstabEntry(fstabPath, uuid string) error {
|
|||||||
}
|
}
|
||||||
newContent := strings.Join(kept, "\n")
|
newContent := strings.Join(kept, "\n")
|
||||||
|
|
||||||
tmpPath := fstabPath + ".tmp"
|
return safeWriteFile(fstabPath, []byte(newContent), 0644)
|
||||||
if err := os.WriteFile(tmpPath, []byte(newContent), 0644); err != nil {
|
}
|
||||||
return fmt.Errorf("cannot write fstab tmp file: %w", err)
|
|
||||||
|
// 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 {
|
||||||
os.Remove(tmpPath)
|
return nil // atomic rename succeeded
|
||||||
return fmt.Errorf("cannot rename fstab tmp file: %w", err)
|
}
|
||||||
|
// Rename failed (likely bind mount) — fall back to direct write
|
||||||
|
os.Remove(tmpPath)
|
||||||
|
if err := os.WriteFile(path, content, perm); err != nil {
|
||||||
|
return fmt.Errorf("cannot write %s: %w", path, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user