test: partitioned-image tool + shared-channel multi-volume proof (S3)
The two-volumes case proves multiple DEVICES; this proves multiple volumes on ONE device. make-partitioned-image.py lays several FAT32 partitions (each from make-fat-image) behind a classic MBR; the new partitioned-volume case attaches one such disk (two partitions, da7a0001 at lba 2048, da7a0002 at lba 83968) as a single usb-storage device. partition.allVolumes walks the table and the manager spawns a confined fat per partition on the SAME block channel, each clamped to its own LBA range by usb-storage's per-badge range table — so partition B's fat cannot read partition A's blocks. The case asserts two mount lines at two distinct non-zero base_lbas on one device; against the pre-uncap allVolumes (S3 step 2, capped to one partition) only da7a0001 mounts. No image is committed — the disk is generated per run. Full suite 130/130 (128 + two-volumes + partitioned-volume).
This commit is contained in:
+31
-4
@@ -837,6 +837,26 @@ CASES = [
|
|||||||
r"(?=.*carries the system tree)"
|
r"(?=.*carries the system tree)"
|
||||||
r"(?=.*data volume; mounted at /volumes/fat-da7a0001)",
|
r"(?=.*data volume; mounted at /volumes/fat-da7a0001)",
|
||||||
"fail": r"data volume; mounted at /volumes/fat-12345678|DANOS-TEST-RESULT: FAIL"},
|
"fail": r"data volume; mounted at /volumes/fat-12345678|DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# S3 shared-channel multi-volume: ONE usb-storage device carrying an MBR with
|
||||||
|
# TWO FAT partitions (da7a0001 at lba 2048, da7a0002 at lba 83968). allVolumes
|
||||||
|
# walks the table and the manager spawns a confined fat per partition on the
|
||||||
|
# SAME block channel, each clamped to its own LBA range (usb-storage's
|
||||||
|
# per-badge range table) — the path a pair of single-volume sticks (the
|
||||||
|
# two-volumes case) does NOT exercise. The two mount lines sit at two DISTINCT
|
||||||
|
# non-zero base_lbas on one device. Against the pre-uncap allVolumes (S3 step
|
||||||
|
# 2, capped to one partition) only da7a0001 mounts, so the da7a0002 lookaheads
|
||||||
|
# fail.
|
||||||
|
{"name": "partitioned-volume",
|
||||||
|
"build_case": "fat-mount",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 150,
|
||||||
|
"data_volume": {"partitions": [{"serial": "DA7A0001", "size_mib": 40},
|
||||||
|
{"serial": "DA7A0002", "size_mib": 40}]},
|
||||||
|
"expect": r"(?s)(?=.*volume 0x0*da7a0001 -> \S+ \(pid \d+\), lba 2048, )"
|
||||||
|
r"(?=.*volume 0x0*da7a0002 -> \S+ \(pid \d+\), lba 83968, )"
|
||||||
|
r"(?=.*fat: mounted /volumes/fat-da7a0001)"
|
||||||
|
r"(?=.*fat: mounted /volumes/fat-da7a0002)",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
# Phase 2b: mkdir/unlink through the mount. Reuses the fat-mount build — the
|
# Phase 2b: mkdir/unlink through the mount. Reuses the fat-mount build — the
|
||||||
# fat-test client, after listing, makes a directory, writes+reads a file inside
|
# fat-test client, after listing, makes a directory, writes+reads a file inside
|
||||||
# it, then removes the file, exercising the whole VFS -> fat mutation path.
|
# it, then removes the file, exercising the whole VFS -> fat mutation path.
|
||||||
@@ -1451,11 +1471,18 @@ def run_case(arch, case):
|
|||||||
if case.get("data_volume"):
|
if case.get("data_volume"):
|
||||||
dv = case["data_volume"]
|
dv = case["data_volume"]
|
||||||
data_img = os.path.join(WORK, "data-volume.img")
|
data_img = os.path.join(WORK, "data-volume.img")
|
||||||
subprocess.run(
|
if dv.get("partitions"):
|
||||||
[sys.executable, os.path.join(REPO, "tools", "make-fat-image.py"),
|
# One device, an MBR with several FAT partitions: several volumes share
|
||||||
|
# ONE block channel, each confined to its own LBA range.
|
||||||
|
gen = [sys.executable, os.path.join(REPO, "tools", "make-partitioned-image.py"), data_img]
|
||||||
|
for part in dv["partitions"]:
|
||||||
|
gen += [part["serial"], str(part.get("size_mib", 40))]
|
||||||
|
else:
|
||||||
|
# One device, one bare FAT volume.
|
||||||
|
gen = [sys.executable, os.path.join(REPO, "tools", "make-fat-image.py"),
|
||||||
"--serial", dv["serial"], "--label", dv.get("label", "DATAVOL"),
|
"--serial", dv["serial"], "--label", dv.get("label", "DATAVOL"),
|
||||||
data_img, str(dv.get("size_mib", 64))],
|
data_img, str(dv.get("size_mib", 64))]
|
||||||
check=True, stdout=subprocess.DEVNULL)
|
subprocess.run(gen, check=True, stdout=subprocess.DEVNULL)
|
||||||
cmd += [
|
cmd += [
|
||||||
"-drive", f"if=none,id=datausb,format=raw,file={data_img}",
|
"-drive", f"if=none,id=datausb,format=raw,file={data_img}",
|
||||||
"-device", "usb-storage,bus=xhci.0,port=4,drive=datausb,removable=on,id=datastorage",
|
"-device", "usb-storage,bus=xhci.0,port=4,drive=datausb,removable=on,id=datastorage",
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Assemble an MBR-partitioned disk image from N FAT32 partitions — the danos
|
||||||
|
multi-volume test disk.
|
||||||
|
|
||||||
|
Pure Python 3 stdlib (no mtools / parted). Each partition is a real FAT32
|
||||||
|
filesystem produced by make-fat-image.py, laid out behind a classic MBR so the
|
||||||
|
danos partition prober (partition.allVolumes) walks the table and the volume
|
||||||
|
manager spawns one confined filesystem per partition — several volumes sharing
|
||||||
|
ONE block channel, each clamped to its own LBA range. That shared-channel,
|
||||||
|
per-partition path is what a single stick with two partitions exercises and a
|
||||||
|
pair of single-volume sticks does not.
|
||||||
|
|
||||||
|
make-partitioned-image.py <out.img> [<serial-hex> <size-MiB>]...
|
||||||
|
|
||||||
|
Each partition is an empty FAT32 with the given volume serial (its /volumes/
|
||||||
|
fat-<serial> content id). Partitions are 1-MiB aligned; the MBR marks each
|
||||||
|
type 0x0C (FAT32 LBA). At most four (an MBR holds four primaries).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
SECTOR = 512
|
||||||
|
ALIGN = 2048 # sectors (1 MiB) — standard partition alignment, and the gap the MBR sits in
|
||||||
|
MBR_TYPE_FAT32_LBA = 0x0C
|
||||||
|
MAX_PRIMARY_PARTITIONS = 4
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
def align_up(sectors, to=ALIGN):
|
||||||
|
return (sectors + to - 1) // to * to
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
if len(argv) < 4 or (len(argv) - 2) % 2 != 0:
|
||||||
|
sys.exit("usage: make-partitioned-image.py <out.img> [<serial-hex> <size-MiB>]...")
|
||||||
|
out_path = argv[1]
|
||||||
|
specs = [(argv[i], int(argv[i + 1])) for i in range(2, len(argv), 2)]
|
||||||
|
if len(specs) > MAX_PRIMARY_PARTITIONS:
|
||||||
|
sys.exit(f"error: an MBR holds at most {MAX_PRIMARY_PARTITIONS} primary partitions")
|
||||||
|
|
||||||
|
# Generate each partition's FAT32 image, then place it at its aligned start.
|
||||||
|
partitions = [] # (start_sector, sector_count, bytes)
|
||||||
|
cursor = ALIGN # leave the first 1 MiB for the MBR + alignment gap
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
for idx, (serial, size_mib) in enumerate(specs):
|
||||||
|
part_path = os.path.join(tmp, f"p{idx}.img")
|
||||||
|
subprocess.run(
|
||||||
|
[sys.executable, os.path.join(HERE, "make-fat-image.py"),
|
||||||
|
"--serial", serial, "--label", f"DATA{idx}",
|
||||||
|
part_path, str(size_mib)],
|
||||||
|
check=True, stdout=subprocess.DEVNULL)
|
||||||
|
with open(part_path, "rb") as handle:
|
||||||
|
data = handle.read()
|
||||||
|
count = len(data) // SECTOR
|
||||||
|
partitions.append((cursor, count, data))
|
||||||
|
cursor = align_up(cursor + count)
|
||||||
|
|
||||||
|
total_sectors = cursor
|
||||||
|
disk = bytearray(total_sectors * SECTOR)
|
||||||
|
# The MBR: a disk signature, one partition entry per FAT partition, 0x55AA.
|
||||||
|
# No boot code (this disk is data, never booted); danos's mount() sees the
|
||||||
|
# signature but no BPB at LBA 0 and takes the MBR-walk path.
|
||||||
|
struct.pack_into("<I", disk, 440, 0x0D05DA05) # arbitrary but fixed disk signature
|
||||||
|
for idx, (start, count, data) in enumerate(partitions):
|
||||||
|
entry = 446 + idx * 16
|
||||||
|
disk[entry + 0] = 0x00 # not bootable
|
||||||
|
disk[entry + 1:entry + 4] = b"\xFE\xFF\xFF" # CHS start (LBA-aware tools ignore)
|
||||||
|
disk[entry + 4] = MBR_TYPE_FAT32_LBA
|
||||||
|
disk[entry + 5:entry + 8] = b"\xFE\xFF\xFF" # CHS end
|
||||||
|
struct.pack_into("<I", disk, entry + 8, start) # start LBA
|
||||||
|
struct.pack_into("<I", disk, entry + 12, count) # sector count
|
||||||
|
disk[start * SECTOR:start * SECTOR + len(data)] = data
|
||||||
|
disk[510] = 0x55
|
||||||
|
disk[511] = 0xAA
|
||||||
|
|
||||||
|
with open(out_path, "wb") as handle:
|
||||||
|
handle.write(disk)
|
||||||
|
print(f"make-partitioned-image: wrote {out_path} "
|
||||||
|
f"({total_sectors * SECTOR // (1024 * 1024)} MiB, {len(partitions)} partitions)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv))
|
||||||
Reference in New Issue
Block a user