mirror of
https://github.com/kevinveenbirkenbach/create-linux-swapfile.git
synced 2026-10-25 15:35:51 +00:00
fix(btrfs): allocate the swapfile with fallocate instead of dd
On a btrfs root the swapfile was written with dd from /dev/zero, so a swapfile the size of the RAM cost tens of GiB of writes and stalled every other writer on the filesystem. A caller that bounds the run, such as an async Ansible task, killed dd halfway, and the next run found the wrong size and started over. btrfs accepts a preallocated NOCOW swapfile and the swap directory already carries +C, so every filesystem now allocates with fallocate. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
43cdce5fba
commit
91e1eff8a6
4 files changed
+19
-34
No files matched your search
@@ -27,10 +27,10 @@ flowchart TD
|
|||||||
I -- yes --> J["swapoff, delete it, drop its fstab line"]
|
I -- yes --> J["swapoff, delete it, drop its fstab line"]
|
||||||
I -- no --> K
|
I -- no --> K
|
||||||
J --> K{"root filesystem is btrfs?"}
|
J --> K{"root filesystem is btrfs?"}
|
||||||
K -- yes --> L["chattr +C, turn compression off, allocate with dd"]
|
K -- yes --> L["chattr +C, turn compression off"]
|
||||||
K -- no --> M["allocate with fallocate"]
|
K -- no --> M["allocate with fallocate"]
|
||||||
L --> N["chmod 600"]
|
L --> M
|
||||||
M --> N
|
M --> N["chmod 600"]
|
||||||
N --> O["mkswap"]
|
N --> O["mkswap"]
|
||||||
O --> P["swapon"]
|
O --> P["swapon"]
|
||||||
P --> Q["append the entry to /etc/fstab"]
|
P --> Q["append the entry to /etc/fstab"]
|
||||||
@@ -43,7 +43,7 @@ The fstab entry is written **after** activation succeeds, so a failed run never
|
|||||||
## 🛠 Features
|
## 🛠 Features
|
||||||
|
|
||||||
- **Automated Swapfile Creation:** Easily create a swapfile with a specified size.
|
- **Automated Swapfile Creation:** Easily create a swapfile with a specified size.
|
||||||
- **Btrfs Aware:** Disables copy-on-write and compression on the swap directory and allocates with `dd`; other filesystems use `fallocate`.
|
- **Btrfs Aware:** Disables copy-on-write and compression on the swap directory before allocating; every filesystem allocates with `fallocate`.
|
||||||
- **FSTAB Integration:** Automatically updates `/etc/fstab` to ensure the swapfile is mounted at boot.
|
- **FSTAB Integration:** Automatically updates `/etc/fstab` to ensure the swapfile is mounted at boot.
|
||||||
- **Safety Checks:** Skips creation when a swapfile of the correct size already exists, and recreates it when the size differs.
|
- **Safety Checks:** Skips creation when a swapfile of the correct size already exists, and recreates it when the size differs.
|
||||||
- **Simple CLI Interface:** Run the command with a single argument.
|
- **Simple CLI Interface:** Run the command with a single argument.
|
||||||
@@ -66,9 +66,8 @@ The package installs **two** identical commands: `swap-forge` (primary) and `swa
|
|||||||
|
|
||||||
- **Python 3.10+** 🐍
|
- **Python 3.10+** 🐍
|
||||||
- **root privileges** — the tool writes `/etc/fstab` and activates swap
|
- **root privileges** — the tool writes `/etc/fstab` and activates swap
|
||||||
- Always required on `PATH`: `findmnt`, `swapoff`, `chmod`, `mkswap`, `swapon`
|
- Always required on `PATH`: `findmnt`, `swapoff`, `fallocate`, `chmod`, `mkswap`, `swapon`
|
||||||
- On a **btrfs** root additionally: `chattr`, `btrfs`, `dd`
|
- On a **btrfs** root additionally: `chattr`, `btrfs`
|
||||||
- On any **other** root filesystem additionally: `fallocate`
|
|
||||||
|
|
||||||
If a required command is missing, the tool exits with code `127` and a one‑line error instead of a traceback.
|
If a required command is missing, the tool exits with code `127` and a one‑line error instead of a traceback.
|
||||||
|
|
||||||
|
|||||||
+4
-17
@@ -10,9 +10,8 @@ DEFAULT_SWAP_PATH = Path("/swapfile")
|
|||||||
SIZE_PATTERN = re.compile(r"^(\d+)([gGmM]?[bB]?)?$")
|
SIZE_PATTERN = re.compile(r"^(\d+)([gGmM]?[bB]?)?$")
|
||||||
MIB_PER_GIB = 1024
|
MIB_PER_GIB = 1024
|
||||||
|
|
||||||
REQUIRED_BINARIES = ("findmnt", "swapoff", "chmod", "mkswap", "swapon")
|
REQUIRED_BINARIES = ("findmnt", "swapoff", "fallocate", "chmod", "mkswap", "swapon")
|
||||||
BTRFS_BINARIES = ("chattr", "btrfs", "dd")
|
BTRFS_BINARIES = ("chattr", "btrfs")
|
||||||
NON_BTRFS_BINARIES = ("fallocate",)
|
|
||||||
EXIT_MISSING_BINARY = 127
|
EXIT_MISSING_BINARY = 127
|
||||||
EXIT_COMMAND_FAILED = 1
|
EXIT_COMMAND_FAILED = 1
|
||||||
|
|
||||||
@@ -117,8 +116,8 @@ def create_swap(path, size_mib, fs, fstab=FSTAB_FILE):
|
|||||||
fs: Filesystem type of the root mount.
|
fs: Filesystem type of the root mount.
|
||||||
fstab: Path of the fstab file to append to.
|
fstab: Path of the fstab file to append to.
|
||||||
"""
|
"""
|
||||||
|
print(f"Creating {size_mib} MiB swapfile on {fs} at {path}")
|
||||||
if fs == "btrfs":
|
if fs == "btrfs":
|
||||||
print(f"Creating {size_mib} MiB swapfile on btrfs at {path}")
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
run(["chattr", "+C", str(path.parent)], check=False)
|
run(["chattr", "+C", str(path.parent)], check=False)
|
||||||
run(
|
run(
|
||||||
@@ -133,20 +132,8 @@ def create_swap(path, size_mib, fs, fstab=FSTAB_FILE):
|
|||||||
],
|
],
|
||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
run(
|
|
||||||
[
|
|
||||||
"dd",
|
|
||||||
"if=/dev/zero",
|
|
||||||
f"of={path}",
|
|
||||||
"bs=1M",
|
|
||||||
f"count={size_mib}",
|
|
||||||
"status=progress",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print(f"Creating {size_mib} MiB swapfile on {fs} at {path}")
|
|
||||||
run(["fallocate", "-l", f"{size_mib}M", str(path)])
|
|
||||||
|
|
||||||
|
run(["fallocate", "-l", f"{size_mib}M", str(path)])
|
||||||
run(["chmod", "600", str(path)])
|
run(["chmod", "600", str(path)])
|
||||||
run(["mkswap", str(path)])
|
run(["mkswap", str(path)])
|
||||||
run(["swapon", str(path)])
|
run(["swapon", str(path)])
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ case "${SWAPFORGE}" in
|
|||||||
esac
|
esac
|
||||||
echo "ok: resolved from the installed package, not the source tree"
|
echo "ok: resolved from the installed package, not the source tree"
|
||||||
|
|
||||||
for binary in findmnt swapoff chmod mkswap swapon fallocate chattr btrfs dd; do
|
for binary in findmnt swapoff fallocate chmod mkswap swapon chattr btrfs; do
|
||||||
command -v "${binary}" >/dev/null || fail "${binary} is missing from the image"
|
command -v "${binary}" >/dev/null || fail "${binary} is missing from the image"
|
||||||
done
|
done
|
||||||
echo "ok: every declared binary is present"
|
echo "ok: every declared binary is present"
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from swap_forge.core import (
|
|||||||
BTRFS_BINARIES,
|
BTRFS_BINARIES,
|
||||||
BTRFS_SWAP_PATH,
|
BTRFS_SWAP_PATH,
|
||||||
DEFAULT_SWAP_PATH,
|
DEFAULT_SWAP_PATH,
|
||||||
NON_BTRFS_BINARIES,
|
|
||||||
REQUIRED_BINARIES,
|
REQUIRED_BINARIES,
|
||||||
MissingBinaryError,
|
MissingBinaryError,
|
||||||
create_swap,
|
create_swap,
|
||||||
@@ -22,28 +21,24 @@ from swap_forge.core import (
|
|||||||
|
|
||||||
class TestRequiredBinaries(unittest.TestCase):
|
class TestRequiredBinaries(unittest.TestCase):
|
||||||
def test_declares_every_external_command_the_package_calls(self):
|
def test_declares_every_external_command_the_package_calls(self):
|
||||||
declared = (
|
declared = set(REQUIRED_BINARIES) | set(BTRFS_BINARIES)
|
||||||
set(REQUIRED_BINARIES) | set(BTRFS_BINARIES) | set(NON_BTRFS_BINARIES)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
declared,
|
declared,
|
||||||
{
|
{
|
||||||
"findmnt",
|
"findmnt",
|
||||||
"swapoff",
|
"swapoff",
|
||||||
|
"fallocate",
|
||||||
"chmod",
|
"chmod",
|
||||||
"mkswap",
|
"mkswap",
|
||||||
"swapon",
|
"swapon",
|
||||||
"chattr",
|
"chattr",
|
||||||
"btrfs",
|
"btrfs",
|
||||||
"dd",
|
|
||||||
"fallocate",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_filesystem_specific_binaries_are_not_declared_as_always_required(self):
|
def test_filesystem_specific_binaries_are_not_declared_as_always_required(self):
|
||||||
self.assertFalse(set(REQUIRED_BINARIES) & set(BTRFS_BINARIES))
|
self.assertFalse(set(REQUIRED_BINARIES) & set(BTRFS_BINARIES))
|
||||||
self.assertFalse(set(REQUIRED_BINARIES) & set(NON_BTRFS_BINARIES))
|
|
||||||
|
|
||||||
def test_missing_binary_is_reported_with_its_name(self):
|
def test_missing_binary_is_reported_with_its_name(self):
|
||||||
with mock.patch("swap_forge.core.subprocess.run") as subprocess_run:
|
with mock.patch("swap_forge.core.subprocess.run") as subprocess_run:
|
||||||
@@ -153,7 +148,7 @@ class TestCreateSwap(unittest.TestCase):
|
|||||||
self.assertEqual(commands, ["fallocate", "chmod", "mkswap", "swapon"])
|
self.assertEqual(commands, ["fallocate", "chmod", "mkswap", "swapon"])
|
||||||
self.assertEqual(fstab.read_text(), f"{swapfile} none swap sw 0 0\n")
|
self.assertEqual(fstab.read_text(), f"{swapfile} none swap sw 0 0\n")
|
||||||
|
|
||||||
def test_btrfs_disables_copy_on_write_and_uses_dd(self):
|
def test_btrfs_disables_copy_on_write_before_fallocate(self):
|
||||||
with tempfile.TemporaryDirectory() as base:
|
with tempfile.TemporaryDirectory() as base:
|
||||||
swapfile = Path(base) / "swap" / "swapfile"
|
swapfile = Path(base) / "swap" / "swapfile"
|
||||||
fstab = Path(base) / "fstab"
|
fstab = Path(base) / "fstab"
|
||||||
@@ -164,7 +159,11 @@ class TestCreateSwap(unittest.TestCase):
|
|||||||
|
|
||||||
commands = [call.args[0][0] for call in run.call_args_list]
|
commands = [call.args[0][0] for call in run.call_args_list]
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
commands, ["chattr", "btrfs", "dd", "chmod", "mkswap", "swapon"]
|
commands, ["chattr", "btrfs", "fallocate", "chmod", "mkswap", "swapon"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
run.call_args_list[2].args[0],
|
||||||
|
["fallocate", "-l", "1024M", str(swapfile)],
|
||||||
)
|
)
|
||||||
self.assertTrue(swapfile.parent.is_dir())
|
self.assertTrue(swapfile.parent.is_dir())
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user