147 lines
5.4 KiB
Python
147 lines
5.4 KiB
Python
|
|
"""release_disk() against a recorded `lsblk -P` picture of the first
|
||
|
|
hardware bench: an SSD with an old Proxmox VE `pve` volume group stacked on
|
||
|
|
sda3 (swap, root, thin pool). Commands are captured through the injected
|
||
|
|
runner — nothing touches real devices."""
|
||
|
|
|
||
|
|
import subprocess
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from diskprep import release_disk
|
||
|
|
|
||
|
|
# What the bench looked like before cleanup (lsblk -lnP -o NAME,TYPE,MOUNTPOINTS /dev/sda).
|
||
|
|
BENCH_BEFORE = """NAME="sda" TYPE="disk" MOUNTPOINTS=""
|
||
|
|
NAME="sda1" TYPE="part" MOUNTPOINTS=""
|
||
|
|
NAME="sda3" TYPE="part" MOUNTPOINTS=""
|
||
|
|
NAME="pve-swap" TYPE="lvm" MOUNTPOINTS="[SWAP]"
|
||
|
|
NAME="pve-root" TYPE="lvm" MOUNTPOINTS="/mnt/old"
|
||
|
|
NAME="pve-data_tmeta" TYPE="lvm" MOUNTPOINTS=""
|
||
|
|
NAME="pve-data-tpool" TYPE="lvm" MOUNTPOINTS=""
|
||
|
|
NAME="pve-data" TYPE="lvm" MOUNTPOINTS=""
|
||
|
|
NAME="pve-data_tdata" TYPE="lvm" MOUNTPOINTS=""
|
||
|
|
NAME="pve-data-tpool" TYPE="lvm" MOUNTPOINTS=""
|
||
|
|
NAME="pve-data" TYPE="lvm" MOUNTPOINTS=""
|
||
|
|
"""
|
||
|
|
|
||
|
|
BENCH_CLEAN = """NAME="sda" TYPE="disk" MOUNTPOINTS=""
|
||
|
|
NAME="sda1" TYPE="part" MOUNTPOINTS=""
|
||
|
|
NAME="sda3" TYPE="part" MOUNTPOINTS=""
|
||
|
|
"""
|
||
|
|
|
||
|
|
EMPTY = """NAME="sda" TYPE="disk" MOUNTPOINTS=""
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
class FakeRunner:
|
||
|
|
"""Answers lsblk from a script of snapshots (one per call, last one
|
||
|
|
sticks) and records everything else."""
|
||
|
|
|
||
|
|
def __init__(self, lsblk_outputs):
|
||
|
|
self.lsblk_outputs = list(lsblk_outputs)
|
||
|
|
self.calls = []
|
||
|
|
|
||
|
|
def __call__(self, cmd, capture_output=True, text=True):
|
||
|
|
if cmd[0] == "lsblk":
|
||
|
|
if len(self.lsblk_outputs) > 1:
|
||
|
|
out = self.lsblk_outputs.pop(0)
|
||
|
|
else:
|
||
|
|
out = self.lsblk_outputs[0]
|
||
|
|
return subprocess.CompletedProcess(cmd, 0, out, "")
|
||
|
|
self.calls.append(cmd)
|
||
|
|
return subprocess.CompletedProcess(cmd, 0, "", "")
|
||
|
|
|
||
|
|
|
||
|
|
def test_release_tears_down_lvm_stack_leaves_first():
|
||
|
|
# lsblk #1 (inventory) and #2 (first removal pass) see the full stack;
|
||
|
|
# everything after that is clean — the dm removals "worked".
|
||
|
|
runner = FakeRunner([BENCH_BEFORE, BENCH_BEFORE, BENCH_CLEAN])
|
||
|
|
lines = release_disk("/dev/sda", run=runner)
|
||
|
|
calls = runner.calls
|
||
|
|
|
||
|
|
assert ["swapoff", "/dev/pve-swap"] in calls
|
||
|
|
assert ["umount", "--all-targets", "/dev/pve-root"] in calls
|
||
|
|
removed = [c[2] for c in calls if c[:2] == ["dmsetup", "remove"]]
|
||
|
|
# Thin LV before its pool, pool before tmeta/tdata.
|
||
|
|
assert removed.index("pve-data") < removed.index("pve-data-tpool")
|
||
|
|
assert removed.index("pve-data-tpool") < removed.index("pve-data_tmeta")
|
||
|
|
assert set(removed) == {
|
||
|
|
"pve-swap",
|
||
|
|
"pve-root",
|
||
|
|
"pve-data_tmeta",
|
||
|
|
"pve-data-tpool",
|
||
|
|
"pve-data",
|
||
|
|
"pve-data_tdata",
|
||
|
|
}
|
||
|
|
assert ["wipefs", "-a", "/dev/sda3"] in calls
|
||
|
|
assert ["wipefs", "-a", "/dev/sda1"] in calls
|
||
|
|
assert ["wipefs", "-a", "/dev/sda"] in calls
|
||
|
|
assert calls[-1] == ["blockdev", "--rereadpt", "/dev/sda"]
|
||
|
|
assert not any("WARNING" in line for line in lines)
|
||
|
|
assert lines[0].startswith("Releasing /dev/sda")
|
||
|
|
|
||
|
|
|
||
|
|
def test_release_warns_when_stack_survives():
|
||
|
|
runner = FakeRunner([BENCH_BEFORE]) # never becomes clean
|
||
|
|
lines = release_disk("/dev/sda", run=runner)
|
||
|
|
assert any("WARNING: still in use" in line for line in lines)
|
||
|
|
# Still tries to give archinstall a chance.
|
||
|
|
assert ["blockdev", "--rereadpt", "/dev/sda"] in runner.calls
|
||
|
|
|
||
|
|
|
||
|
|
def test_release_empty_disk_only_wipes_and_rereads():
|
||
|
|
runner = FakeRunner([EMPTY])
|
||
|
|
lines = release_disk("sda", run=runner) # bare name is accepted too
|
||
|
|
assert runner.calls == [["wipefs", "-a", "/dev/sda"], ["blockdev", "--rereadpt", "/dev/sda"]]
|
||
|
|
assert any("nothing stacked" in line for line in lines)
|
||
|
|
|
||
|
|
|
||
|
|
def test_release_survives_missing_tools():
|
||
|
|
def runner(cmd, capture_output=True, text=True):
|
||
|
|
if cmd[0] == "lsblk":
|
||
|
|
return subprocess.CompletedProcess(cmd, 0, BENCH_BEFORE, "")
|
||
|
|
raise FileNotFoundError(cmd[0])
|
||
|
|
|
||
|
|
lines = release_disk("/dev/sda", run=runner)
|
||
|
|
assert any("command not found" in line for line in lines)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def app_module(monkeypatch, tmp_path):
|
||
|
|
import app as app_module
|
||
|
|
|
||
|
|
monkeypatch.setattr(app_module, "INSTALL_LOG", tmp_path / "install.log")
|
||
|
|
monkeypatch.setattr(app_module, "STATE_DIR", tmp_path / "state")
|
||
|
|
return app_module
|
||
|
|
|
||
|
|
|
||
|
|
def test_install_run_releases_disk_before_spawning(app_module, monkeypatch):
|
||
|
|
order = []
|
||
|
|
|
||
|
|
def fake_release(disk):
|
||
|
|
order.append(("release", disk))
|
||
|
|
return ["Releasing X"]
|
||
|
|
|
||
|
|
monkeypatch.setattr(app_module, "release_disk", fake_release)
|
||
|
|
monkeypatch.setattr(app_module, "spawn_archinstall", lambda *a: order.append(("spawn",)))
|
||
|
|
monkeypatch.setattr(app_module, "build_archinstall_config", lambda s: {})
|
||
|
|
monkeypatch.setattr(app_module, "build_archinstall_creds", lambda s: {})
|
||
|
|
monkeypatch.delenv("FURTKA_DRY_RUN", raising=False)
|
||
|
|
app_module.settings.update(boot_drive="/dev/sda", username="u", password="pw12345678")
|
||
|
|
|
||
|
|
client = app_module.app.test_client()
|
||
|
|
resp = client.post("/install/run")
|
||
|
|
|
||
|
|
assert resp.status_code == 302
|
||
|
|
assert order == [("release", "/dev/sda"), ("spawn",)]
|
||
|
|
assert Path(app_module.INSTALL_LOG).read_text().startswith("Releasing X")
|
||
|
|
|
||
|
|
|
||
|
|
def test_progress_names_disk_in_use_failure(app_module):
|
||
|
|
log = (
|
||
|
|
"Traceback (most recent call last):\n"
|
||
|
|
"_ped.IOException: ... unable to inform the kernel of the change ..."
|
||
|
|
)
|
||
|
|
progress = app_module.parse_install_progress(log)
|
||
|
|
assert progress["status"] == "error"
|
||
|
|
assert "still in use" in progress["phase"]
|