diff --git a/build.zig b/build.zig index 4e05d0c..8d76688 100644 --- a/build.zig +++ b/build.zig @@ -317,6 +317,11 @@ pub fn build(b: *std.Build) void { // out of the same read-only initrd, before it spawns anything — the registrar // has to know its policy before the first provider asks. bundled_list.append(b.allocator, .{ .path = "system/configuration/protocol.csv", .binary = b.path("system/configuration/protocol.csv") }) catch @panic("OOM"); + // The storage mount map (docs/file-system-development/storage-architecture.md): + // filesystems.csv (content signature -> service binary) and volumes.csv (the + // optional id -> mount-prefix override), both read by the volume manager. + bundled_list.append(b.allocator, .{ .path = "system/configuration/filesystems.csv", .binary = b.path("system/configuration/filesystems.csv") }) catch @panic("OOM"); + bundled_list.append(b.allocator, .{ .path = "system/configuration/volumes.csv", .binary = b.path("system/configuration/volumes.csv") }) catch @panic("OOM"); // A no-option build assumes neither -Dtest-case nor -Ddiagnose: it ships the // production set only. The userspace test fixtures under /test join in only // for a test build — which the QEMU harness signals by passing diff --git a/system/configuration/filesystems.csv b/system/configuration/filesystems.csv new file mode 100644 index 0000000..81dea8b --- /dev/null +++ b/system/configuration/filesystems.csv @@ -0,0 +1,7 @@ +# The filesystem map: a probed volume's content signature -> the service binary +# that serves it (docs/file-system-development/storage-architecture.md). The +# volume manager reads this (the policy); a signature no row matches goes +# unserved, never guessed. Adding a filesystem adds a row. +# +# signature, binary +fat, /system/services/fat diff --git a/system/configuration/volumes.csv b/system/configuration/volumes.csv new file mode 100644 index 0000000..55e29ee --- /dev/null +++ b/system/configuration/volumes.csv @@ -0,0 +1,8 @@ +# The mount map (danos's fstab): a volume's content id -> a chosen mount prefix. +# This is an OPTIONAL override, read by the volume manager. A volume with no row +# mounts at its default /volumes/, where is the manager's rendered +# content identity (e.g. fat-12345678, gpt-, mbr--) — stable, +# unique, and never a port or a label. The label is display metadata, not here: +# query it via the volume manager's `volumes` verb. +# +# id, mount_prefix diff --git a/system/services/volume-manager/build.zig b/system/services/volume-manager/build.zig index a9145e8..d2944b0 100644 --- a/system/services/volume-manager/build.zig +++ b/system/services/volume-manager/build.zig @@ -10,9 +10,10 @@ pub fn build(b: *std.Build) void { .name = "volume-manager", .root_source_file = b.path("volume-manager.zig"), .imports = &.{ - "block", "channel", "device-manager-protocol", "driver", - "envelope", "ipc", "logging", "memory", - "process", "service", "time", "volume-manager-protocol", + "block", "channel", "csv", "device-manager-protocol", + "driver", "envelope", "file-system", "ipc", + "logging", "memory", "process", "service", + "time", "volume-manager-protocol", }, }); b.installArtifact(exe); diff --git a/system/services/volume-manager/volume-manager.zig b/system/services/volume-manager/volume-manager.zig index f9e6ea6..f80d6d9 100644 --- a/system/services/volume-manager/volume-manager.zig +++ b/system/services/volume-manager/volume-manager.zig @@ -26,7 +26,10 @@ const process = @import("process"); const service = @import("service"); const time = @import("time"); const envelope = @import("envelope"); +const fs = @import("file-system"); const partition = @import("partition.zig"); +const filesystem_map = @import("filesystem-map.zig"); +const volume_map = @import("volume-map.zig"); const Serve = volume_manager_protocol.Protocol.Provider(void); const Invocation = envelope.Invocation; @@ -42,15 +45,53 @@ const Volume = struct { block_count: u64, identity: partition.Identity, id: u64, + binary: []const u8, // the service binary, from filesystems.csv by signature + mount_prefix: []const u8, // the volume-root mount path (its id-path, or a volumes.csv override) filesystem_pid: u32 = 0, }; -/// The filesystem binary a probed volume is served by. The signature->binary -/// map (filesystems.csv) lands with the identity ladder; for now every FAT-shaped -/// volume gets the FAT service. -const filesystem_binary = "/system/services/fat"; const volume_id: u64 = 1; +// The mount map, read from configuration at boot (the policy home, storage- +// architecture.md): filesystems.csv (content signature -> service binary) and +// volumes.csv (an optional id -> mount-prefix override). The sources are held +// for the process life so the parsed rules' slices into them stay valid. +/// bound: bytes of filesystems.csv / volumes.csv the manager reads +/// decided-by: ours +/// protects: the config source buffers below +/// at-limit: truncate - a longer file is cut; a row split by the cut is malformed +/// observed-by: the per-file "malformed/truncated" log line +const config_source_bytes = 2048; +var filesystems_source: [config_source_bytes]u8 = undefined; +var volumes_source: [config_source_bytes]u8 = undefined; +/// bound: filesystem-map rules held (one per content signature) +/// decided-by: ours +/// protects: the filesystem_rules table +/// at-limit: truncate - extra rows are dropped and the "truncated" note logged +/// observed-by: the "truncated" log line +const maximum_filesystem_rules = 8; +/// bound: volumes.csv override rows held (one per pinned volume id) +/// decided-by: ours +/// protects: the volume_rules table +/// at-limit: truncate - extra rows are dropped and the "truncated" note logged +/// observed-by: the "truncated" log line +const maximum_volume_rules = 64; +var filesystem_rules: [maximum_filesystem_rules]filesystem_map.Rule = undefined; +var filesystem_rule_count: usize = 0; +var volume_rules: [maximum_volume_rules]volume_map.Override = undefined; +var volume_rule_count: usize = 0; +/// The composed default mount path (/volumes/) for the current volume; a +/// volumes.csv override is used in place and needs no buffer (it is already a +/// slice into volumes_source). One buffer suffices while the manager serves one +/// volume (multi-volume gives each its own in S3). +/// bound: bytes of a composed /volumes/ mount path +/// decided-by: ours +/// protects: the mount_prefix_buf below +/// at-limit: truncate - bufPrint fails; the volume mounts at a fallback path (logged) +/// observed-by: the fallback path in the log +const mount_path_maximum = 64; +var mount_prefix_buf: [mount_path_maximum]u8 = undefined; + var service_endpoint: ipc.Handle = 0; var manager_handle: ?ipc.Handle = null; var bounce: memory.DmaRegion = undefined; @@ -156,7 +197,7 @@ fn isDevicePresent(device_id: u64) bool { /// confinement controller (it defines the first range on the device). fn spawnFilesystem(v: *Volume) void { if (fs_failed) return; - const pid = process.spawnSupervised(filesystem_binary, &.{"1"}, service_endpoint) orelse { + const pid = process.spawnSupervised(v.binary, &.{ "1", v.mount_prefix }, service_endpoint) orelse { _ = logging.write("volume-manager: could not spawn the filesystem; retrying\n"); armRestart(); return; @@ -169,7 +210,7 @@ fn spawnFilesystem(v: *Volume) void { } v.filesystem_pid = pid; fs_spawn_ns = time.clock(); - std.log.info("volume 0x{x} -> {s} (pid {d}), lba {d}, {d} blocks", .{ v.identity.key, filesystem_binary, pid, v.base_lba, v.block_count }); + std.log.info("volume 0x{x} -> {s} (pid {d}), lba {d}, {d} blocks", .{ v.identity.key, v.binary, pid, v.base_lba, v.block_count }); } /// Schedule a fat restart after backoff; the poll loop performs it once due. @@ -224,11 +265,29 @@ fn bringUpVolume() void { _ = ipc.close(device.endpoint); return; }; + // Pick the service binary from the volume's content signature. A signature + // no filesystems.csv row serves goes unserved (logged), like an unbound + // device — the manager does not guess. + const binary = filesystem_map.match(filesystem_rules[0..filesystem_rule_count], found.signature) orelse { + if (!logged_no_volume) { + _ = logging.write("volume-manager: no filesystem serves this volume's content; unserved\n"); + logged_no_volume = true; + } + _ = ipc.close(device.endpoint); + return; + }; + // The mount path is the volume's identity id (/volumes/), or a + // volumes.csv override pinning it to a chosen path. The id is content-derived, + // so the path is stable and never a port or a label. + var id_buf: [volume_map.id_maximum]u8 = undefined; + const id = volume_map.idString(found.identity, &id_buf); + const mount_prefix = volume_map.overrideFor(volume_rules[0..volume_rule_count], id) orelse + (std.fmt.bufPrint(&mount_prefix_buf, "/volumes/{s}", .{id}) catch "/volumes/unknown"); logged_no_volume = false; fs_restarts = 0; fs_failed = false; restart_pending = false; - volume = .{ .storage = device, .storage_device_id = opened.device_id, .base_lba = found.base_lba, .block_count = found.block_count, .identity = found.identity, .id = volume_id }; + volume = .{ .storage = device, .storage_device_id = opened.device_id, .base_lba = found.base_lba, .block_count = found.block_count, .identity = found.identity, .id = volume_id, .binary = binary, .mount_prefix = mount_prefix }; spawnFilesystem(&volume.?); } @@ -296,9 +355,42 @@ fn onMessage(message: []const u8, out: []u8, sender: u32, arrived: *ipc.Arrival) return Serve.dispatch({}, handlers, message, sender, arrived.peek(), out); } +/// Read a config file into `buf`, returning the byte count (0 if missing). +fn readConfig(path: []const u8, buf: []u8) usize { + var file = fs.open(path, .{}) orelse { + std.log.info("volume-manager: {s} missing", .{path}); + return 0; + }; + defer file.close(); + var used: usize = 0; + while (used < buf.len) { + const n = file.read(buf[used..]) orelse break; + if (n == 0) break; + used += n; + } + return used; +} + +/// Load the mount map from configuration once at boot (mirrors the device +/// manager's registry load). A missing or empty filesystems.csv means no volume +/// is served; volumes.csv is optional — no rows means every volume takes its +/// default /volumes/ path. +fn loadTables() void { + const fs_used = readConfig("/system/configuration/filesystems.csv", &filesystems_source); + const fr = filesystem_map.parse(filesystems_source[0..fs_used], &filesystem_rules); + filesystem_rule_count = fr.count; + if (fr.malformed != 0 or fr.truncated) std.log.info("filesystems.csv: {d} malformed, truncated={}", .{ fr.malformed, fr.truncated }); + + const vol_used = readConfig("/system/configuration/volumes.csv", &volumes_source); + const vr = volume_map.parse(volumes_source[0..vol_used], &volume_rules); + volume_rule_count = vr.count; + if (vr.malformed != 0 or vr.truncated) std.log.info("volumes.csv: {d} malformed, truncated={}", .{ vr.malformed, vr.truncated }); +} + fn initialise(endpoint: ipc.Handle) bool { service_endpoint = endpoint; _ = logging.write("volume-manager: starting, waiting for a storage device\n"); + loadTables(); _ = process.subscribeExits(endpoint); pollTick(); _ = time.timerOnce(endpoint, poll_interval_ms); // the poll runs for the life of the boot