From c8b76a2a3dc5ce9dd189daadf03ee926aa5308d9 Mon Sep 17 00:00:00 2001 From: Jan De Landtsheer Date: Thu, 9 Oct 2025 16:51:12 +0200 Subject: [PATCH] Refine default orchestration flow and documentation - Document defaults-only configuration, kernel topology override, and deprecated CLI flags in README - Mark schema doc as deprecated per ADR-0002 - Warn that --topology/--config are ignored; adjust loader/main/context flow - Refactor orchestrator run() to auto-select mount/apply, reuse state when already provisioned, and serialize topology via Display - Add Callgraph/FUNCTION_LIST/ADR docs tracking the new behavior - Derive Eq for Topology to satisfy updated CLI handling --- README.md | 36 +- docs/Callgraph.svg | 1716 ++++++++++ docs/FUNCTION_LIST.md | 294 ++ docs/SCHEMA.md | 31 +- .../0002-defaults-only-no-external-config.md | 109 + docs/adr/callgraph.html | 2932 +++++++++++++++++ src/cli/args.rs | 11 +- src/config/loader.rs | 115 +- src/main.rs | 4 +- src/orchestrator/run.rs | 452 ++- src/types.rs | 2 +- 11 files changed, 5405 insertions(+), 297 deletions(-) create mode 100644 docs/Callgraph.svg create mode 100644 docs/FUNCTION_LIST.md create mode 100644 docs/adr/0002-defaults-only-no-external-config.md create mode 100644 docs/adr/callgraph.html diff --git a/README.md b/README.md index 1f84a3e..130ac2b 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,10 @@ Key modules - [src/mount/ops.rs](src/mount/ops.rs) Features at a glance -- Topology-driven planning with built-in defaults: BtrfsSingle, BcachefsSingle, DualIndependent, Bcachefs2Copy, BtrfsRaid1, SsdHddBcachefs -- Non-destructive preview: --show/--report outputs JSON summary (disks, partition plan, filesystems, planned mountpoints) +- Topology auto-selection with built-in defaults; optional kernel cmdline override via `zosstorage.topology=` (see ADR-0002) +- Non-destructive preview: `--show`/`--report` outputs JSON summary (disks, partition plan, filesystems, planned mountpoints) - Safe discovery: excludes removable media by default (USB sticks) unless explicitly allowed -- Config-optional: the tool runs without any YAML; sensible defaults are always present and may be overridden/merged by config +- No external YAML configuration; defaults-only per ADR-0002 (sane built-ins, topology may be overridden by kernel cmdline) Requirements - Linux with /proc and /sys mounted (initramfs friendly) @@ -45,8 +45,6 @@ Install and build Binary is target/release/zosstorage. CLI usage -- Topology selection (config optional): - -t, --topology btrfs-single|bcachefs-single|dual-independent|bcachefs-2copy|btrfs-raid1|ssd-hdd-bcachefs - Preview (non-destructive): --show Print JSON summary to stdout --report PATH Write JSON summary to a file @@ -56,22 +54,30 @@ CLI usage -l, --log-level LEVEL error|warn|info|debug (default: info) -L, --log-to-file Also write logs to /run/zosstorage/zosstorage.log - Other: - -c, --config PATH Merge a YAML config file (overrides defaults) -s, --fstab Enable writing /etc/fstab entries (when mounts are applied) -a, --apply Perform partitioning, filesystem creation, and mounts (destructive) -f, --force Present but not implemented (returns an error) +Deprecated (ignored with warning; see ADR-0002) + -t, --topology VALUE Ignored; use kernel cmdline `zosstorage.topology=` instead + -c, --config PATH Ignored; external YAML configuration is not used at runtime + Examples -- Single disk plan with debug logs: - sudo ./zosstorage --show -t btrfs-single -l debug -- RAID1 btrfs across two disks; print and write summary: - sudo ./zosstorage --show --report /run/zosstorage/plan.json -t btrfs-raid1 -l debug -L -- SSD+HDD bcachefs plan, include removable devices (for lab cases): - sudo ./zosstorage --show -t ssd-hdd-bcachefs --allow-removable -l debug +- Single disk plan with debug logs (defaults to btrfs_single automatically): + sudo ./zosstorage --show -l debug +- Two-disk plan (defaults to dual_independent automatically), write summary: + sudo ./zosstorage --show --report /run/zosstorage/plan.json -l debug -L +- Include removable devices for lab scenarios: + sudo ./zosstorage --show --allow-removable -l debug - Quiet plan to file: - sudo ./zosstorage --report /run/zosstorage/plan.json -t dual-independent -- Apply single-disk btrfs (DESTRUCTIVE; wipes target disk): - sudo ./zosstorage --apply -t btrfs-single + sudo ./zosstorage --report /run/zosstorage/plan.json +- Apply single-disk plan (DESTRUCTIVE; wipes target disk; defaults select topology automatically): + sudo ./zosstorage --apply + +Kernel cmdline override (at boot) +- To force a topology, pass one of: + zosstorage.topology=btrfs-single | bcachefs-single | dual-independent | btrfs-raid1 | ssd-hdd-bcachefs | bcachefs-2copy +- The override affects only topology; all other settings use sane built-in defaults. Preview JSON shape (examples) 1) Already provisioned (idempotency success): diff --git a/docs/Callgraph.svg b/docs/Callgraph.svg new file mode 100644 index 0000000..ca0f970 --- /dev/null +++ b/docs/Callgraph.svg @@ -0,0 +1,1716 @@ + + + + + + + + + + +zosstorage/src + + + + + +cli + + + + + +config + + + + + +device + + + + + +fs + + + + + +idempotency + + + + + +logging + + + + + +mount + + + + + +orchestrator + + + + + +partition + + + + + +report + + + + + +util + + + + + + +            mod.rs             + + + + +detect_existing_state + + + + +is_empty_disk + + + + +parse_blkid_export + + + + +read_proc_partitions_names + + + + +base_name + + + + +is_partition_of + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            mod.rs             + + + + +S +  CmdOutput + + + + +which_tool + + + + +run_cmd + + + + +run_cmd_capture + + + + +udev_settle + + + + +is_efi_boot + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            discovery.rs             + + + + +S +  Disk + + + + +S +  DeviceFilter + + + + +impl DeviceFilter + + +matches + + + + + + +DeviceProvider + + +list_block_devices + + + + +probe_properties + + + + + + +S +  SysProvider + + + + +impl SysProvider + + +new + + + + + + +impl DeviceProvider for SysProvider + + +list_block_devices + + + + +probe_properties + + + + + + +discover + + + + +discover_with_provider + + + + +is_ignored_name + + + + +sys_block_path + + + + +base_name + + + + +is_removable_sysfs + + + + +is_partition_sysfs + + + + +read_disk_size_bytes + + + + +read_rotational + + + + +read_model_serial + + + + +read_optional_string + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            run.rs             + + + + +S +  Context + + + + +impl Context + + +new + + + + +with_show + + + + +with_apply + + + + +with_report_path + + + + +with_mount_existing + + + + +with_report_current + + + + +with_topology_from_cli + + + + +with_topology_from_cmdline + + + + + + +run + + + + +build_device_filter + + + + +enforce_empty_disks + + + + +role_str + + + + +build_summary_json + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            plan.rs             + + + + +E +  FsKind + + + + +S +  FsSpec + + + + +S +  FsPlan + + + + +S +  FsResult + + + + +plan_filesystems + + + + +make_filesystems + + + + +capture_uuid + + + + +parse_blkid_export + + + + +probe_existing_filesystems + + + + +tests_parse + + +parse_export_ok + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            plan.rs             + + + + +E +  PartRole + + + + +S +  PartitionSpec + + + + +S +  DiskPlan + + + + +S +  PartitionPlan + + + + +S +  PartitionResult + + + + +plan_partitions + + + + +apply_partitions + + +type_code + + + + +part_dev_path + + + + +sector_size_bytes + + + + +parse_sgdisk_info + + + + + + + + + + + + + + + + + + + + + + + + +            ops.rs             + + + + +S +  PlannedMount + + + + +S +  PlannedSubvolMount + + + + +S +  MountPlan + + + + +S +  MountResult + + + + +fstype_str + + + + +plan_mounts + + + + +apply_mounts + + + + +maybe_write_fstab + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            main.rs             + + + + +main + + + + +real_main + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            mod.rs             + + + + +S +  LogOptions + + + + +impl LogOptions + + +from_cli + + + + + + +level_from_str + + + + +init_logging + + + + + + + + + + + + + + + + + + + + + + +            loader.rs             + + + + +load_and_merge + + + + +validate + + + + +to_value + + + + +merge_value + + + + +cli_overlay_value + + + + +kernel_cmdline_topology + + + + +parse_topology_token + + + + +default_config + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            args.rs             + + + + +E +  LogLevelArg + + + + +impl std::fmt::Display for LogLevelArg + + +fmt + + + + + + +S +  Cli + + + + +from_args + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            errors.rs             + + + + +E +  Error + + + + +T +  Result + + + + + + + + +            lib.rs             + + + + +cli + + + + +logging + + + + +config + + + + +device + + + + +partition + + + + +fs + + + + +mount + + + + +report + + + + +orchestrator + + + + +idempotency + + + + +util + + + + +errors + + + + +types + + + + + + + + +            types.rs             + + + + +S +  LoggingConfig + + + + +S +  DeviceSelection + + + + +E +  Topology + + + + +impl std::fmt::Display for Topology + + +fmt + + + + + + +S +  BiosBootSpec + + + + +S +  EspSpec + + + + +S +  DataSpec + + + + +S +  CacheSpec + + + + +S +  Partitioning + + + + +S +  BtrfsOptions + + + + +S +  BcachefsOptions + + + + +S +  VfatOptions + + + + +S +  FsOptions + + + + +E +  MountSchemeKind + + + + +S +  MountScheme + + + + +S +  ReportOptions + + + + +S +  Config + + + + + + + + +            mod.rs             + + + + +args + + + + + + + + +            mod.rs             + + + + +loader + + + + + + + + +            mod.rs             + + + + +discovery + + + + + + + + +            mod.rs             + + + + +plan + + + + + + + + +            mod.rs             + + + + +ops + + + + + + + + +            mod.rs             + + + + +run + + + + + + + + +            mod.rs             + + + + +plan + + + + + + + + +            mod.rs             + + + + +state + + + + + + + + +            state.rs             + + + + +S +  StateReport + + + + +build_report + + + + +write_report + + + + + + + + + + \ No newline at end of file diff --git a/docs/FUNCTION_LIST.md b/docs/FUNCTION_LIST.md new file mode 100644 index 0000000..7fbf082 --- /dev/null +++ b/docs/FUNCTION_LIST.md @@ -0,0 +1,294 @@ +# Function Reference - Call Graph Analysis + +> This documentation is automatically derived from [`Callgraph.svg`](Callgraph.svg) and provides a comprehensive overview of all functions in the zosstorage project, organized by module. + +## Table of Contents + +- [Main Entry Points](#main-entry-points) +- [CLI & Configuration](#cli--configuration) +- [Orchestration](#orchestration) +- [Device Discovery](#device-discovery) +- [Partition Management](#partition-management) +- [Filesystem Operations](#filesystem-operations) +- [Mount Operations](#mount-operations) +- [Idempotency & State](#idempotency--state) +- [Reporting](#reporting) +- [Utilities](#utilities) +- [Logging](#logging) +- [Type Definitions](#type-definitions) + +--- + +## Main Entry Points + +### [`src/main.rs`](../src/main.rs) + +| Function | Purpose | +|----------|---------| +| `main()` | Application entry point; initializes the program and handles top-level errors | +| `real_main()` | Core application logic; orchestrates the main workflow after initialization | + +--- + +## CLI & Configuration + +### [`src/cli/args.rs`](../src/cli/args.rs) + +**Structs:** `Cli`, `LogLevelArg` (enum) + +| Function | Purpose | +|----------|---------| +| `from_args()` | Parses command-line arguments and returns a `Cli` configuration object | + +### [`src/config/loader.rs`](../src/config/loader.rs) + +| Function | Purpose | +|----------|---------| +| `load_and_merge()` | Loads configuration from multiple sources and merges them into a unified config | +| `validate()` | Validates the merged configuration for correctness and completeness | +| `to_value()` | Converts configuration structures to internal value representation | +| `merge_value()` | Recursively merges configuration values, handling conflicts appropriately | +| `cli_overlay_value()` | Overlays CLI-provided values onto existing configuration | +| `kernel_cmdline_topology()` | Extracts topology information from kernel command line parameters | +| `parse_topology_token()` | Parses individual topology tokens from kernel cmdline | +| `default_config()` | Generates default configuration values when no config file is present | + +--- + +## Orchestration + +### [`src/orchestrator/run.rs`](../src/orchestrator/run.rs) + +**Structs:** `Context` + +| Function | Purpose | +|----------|---------| +| `Context::new()` | Creates a new orchestration context with default settings | +| `Context::with_show()` | Builder method to enable show/dry-run mode | +| `Context::with_apply()` | Builder method to enable apply mode (actual execution) | +| `Context::with_report_path()` | Builder method to set the report output path | +| `Context::with_mount_existing()` | Builder method to configure mounting of existing filesystems | +| `Context::with_report_current()` | Builder method to enable reporting of current system state | +| `Context::with_topology_from_cli()` | Builder method to set topology from CLI arguments | +| `Context::with_topology_from_cmdline()` | Builder method to set topology from kernel cmdline | +| `run()` | Main orchestration function; coordinates all storage operations | +| `build_device_filter()` | Constructs device filter based on configuration and user input | +| `enforce_empty_disks()` | Validates that target disks are empty before proceeding | +| `role_str()` | Converts partition role enum to human-readable string | +| `build_summary_json()` | Builds a JSON summary of operations performed | + +--- + +## Device Discovery + +### [`src/device/discovery.rs`](../src/device/discovery.rs) + +**Structs:** `Disk`, `DeviceFilter`, `SysProvider` +**Traits:** `DeviceProvider` + +| Function | Purpose | +|----------|---------| +| `DeviceFilter::matches()` | Checks if a device matches the configured filter criteria | +| `SysProvider::new()` | Creates a new sysfs-based device provider | +| `SysProvider::list_block_devices()` | Lists all block devices found via sysfs | +| `SysProvider::probe_properties()` | Probes detailed properties of a specific device | +| `discover()` | Entry point for device discovery using default provider | +| `discover_with_provider()` | Device discovery with custom provider (for testing/flexibility) | +| `is_ignored_name()` | Checks if device name should be ignored (loop, ram, etc.) | +| `sys_block_path()` | Constructs sysfs path for a given block device | +| `base_name()` | Extracts base device name from path | +| `is_removable_sysfs()` | Checks if device is removable via sysfs | +| `is_partition_sysfs()` | Checks if device is a partition via sysfs | +| `read_disk_size_bytes()` | Reads disk size in bytes from sysfs | +| `read_rotational()` | Determines if disk is rotational (HDD) or not (SSD) | +| `read_model_serial()` | Reads device model and serial number | +| `read_optional_string()` | Utility to safely read optional string values from sysfs | + +--- + +## Partition Management + +### [`src/partition/plan.rs`](../src/partition/plan.rs) + +**Structs:** `PartitionSpec`, `DiskPlan`, `PartitionPlan`, `PartitionResult` +**Enums:** `PartRole` + +| Function | Purpose | +|----------|---------| +| `plan_partitions()` | Creates partition plans for all target disks based on topology | +| `apply_partitions()` | Executes partition plans using sgdisk tool | +| `type_code()` | Returns GPT partition type code for a given partition role | +| `part_dev_path()` | Constructs device path for a partition (e.g., /dev/sda1) | +| `sector_size_bytes()` | Reads logical sector size of disk | +| `parse_sgdisk_info()` | Parses output from sgdisk to extract partition information | + +--- + +## Filesystem Operations + +### [`src/fs/plan.rs`](../src/fs/plan.rs) + +**Structs:** `FsSpec`, `FsPlan`, `FsResult` +**Enums:** `FsKind` + +| Function | Purpose | +|----------|---------| +| `plan_filesystems()` | Plans filesystem creation for all partitions | +| `make_filesystems()` | Creates filesystems according to plan (mkfs.* tools) | +| `capture_uuid()` | Captures UUID of newly created filesystem | +| `parse_blkid_export()` | Parses blkid export format to extract filesystem metadata | +| `probe_existing_filesystems()` | Detects existing filesystems on partitions | + +--- + +## Mount Operations + +### [`src/mount/ops.rs`](../src/mount/ops.rs) + +**Structs:** `PlannedMount`, `PlannedSubvolMount`, `MountPlan`, `MountResult` + +| Function | Purpose | +|----------|---------| +| `fstype_str()` | Converts FsKind enum to mount filesystem type string | +| `plan_mounts()` | Creates mount plans for all filesystems | +| `apply_mounts()` | Executes mount operations and creates mount points | +| `maybe_write_fstab()` | Conditionally writes /etc/fstab entries for persistent mounts | + +--- + +## Idempotency & State + +### [`src/idempotency/mod.rs`](../src/idempotency/mod.rs) + +| Function | Purpose | +|----------|---------| +| `detect_existing_state()` | Detects existing partitions and filesystems to avoid destructive operations | +| `is_empty_disk()` | Checks if a disk has no partition table or filesystems | +| `parse_blkid_export()` | Parses blkid output to identify existing filesystems | +| `read_proc_partitions_names()` | Reads partition names from /proc/partitions | +| `base_name()` | Extracts base name from device path | +| `is_partition_of()` | Checks if one device is a partition of another | + +--- + +## Reporting + +### [`src/report/state.rs`](../src/report/state.rs) + +**Structs:** `StateReport` + +| Function | Purpose | +|----------|---------| +| `build_report()` | Builds comprehensive state report of operations performed | +| `write_report()` | Writes report to specified output path (JSON format) | + +--- + +## Utilities + +### [`src/util/mod.rs`](../src/util/mod.rs) + +**Structs:** `CmdOutput` + +| Function | Purpose | +|----------|---------| +| `which_tool()` | Locates external tool in PATH (sgdisk, mkfs.*, etc.) | +| `run_cmd()` | Executes shell command and returns exit status | +| `run_cmd_capture()` | Executes command and captures stdout/stderr | +| `udev_settle()` | Waits for udev to process device events | +| `is_efi_boot()` | Detects if system booted in EFI mode | + +--- + +## Logging + +### [`src/logging/mod.rs`](../src/logging/mod.rs) + +**Structs:** `LogOptions` + +| Function | Purpose | +|----------|---------| +| `LogOptions::from_cli()` | Creates logging configuration from CLI arguments | +| `level_from_str()` | Converts string to log level enum | +| `init_logging()` | Initializes logging subsystem with configured options | + +--- + +## Type Definitions + +### [`src/types.rs`](../src/types.rs) + +**Core Configuration Structures:** + +- `Config` - Top-level configuration structure +- `LoggingConfig` - Logging configuration +- `DeviceSelection` - Device selection criteria +- `Topology` - Storage topology definition (enum) +- `Partitioning` - Partition layout specification +- `BiosBootSpec`, `EspSpec`, `DataSpec`, `CacheSpec` - Partition type specifications +- `FsOptions`, `BtrfsOptions`, `BcachefsOptions`, `VfatOptions` - Filesystem options +- `MountScheme`, `MountSchemeKind` - Mount configuration +- `ReportOptions` - Report generation configuration + +### [`src/errors.rs`](../src/errors.rs) + +**Error Types:** + +- `Error` - Main error enum for all error conditions +- `Result` - Type alias for `std::result::Result` + +--- + +## Call Graph Relationships + +### Main Execution Flow + +``` +main() → real_main() → orchestrator::run() + ↓ + ├─→ cli::from_args() + ├─→ config::load_and_merge() + ├─→ logging::init_logging() + ├─→ device::discover() + ├─→ partition::plan_partitions() + ├─→ partition::apply_partitions() + ├─→ fs::plan_filesystems() + ├─→ fs::make_filesystems() + ├─→ mount::plan_mounts() + ├─→ mount::apply_mounts() + └─→ report::build_report() / write_report() +``` + +### Key Dependencies + +- **Orchestrator** (`run()`) calls: All major subsystems +- **Device Discovery** uses: Utilities for system probing +- **Partition/FS/Mount** operations use: Utilities for command execution +- **All operations** call: `util::run_cmd()` or `util::run_cmd_capture()` +- **Idempotency checks** called by: Orchestrator before destructive operations + +--- + +## Function Count Summary + +- **Main Entry**: 2 functions +- **CLI & Config**: 9 functions +- **Orchestration**: 13 functions +- **Device Discovery**: 14 functions +- **Partition Management**: 6 functions +- **Filesystem Operations**: 5 functions +- **Mount Operations**: 4 functions +- **Idempotency**: 6 functions +- **Reporting**: 2 functions +- **Utilities**: 6 functions +- **Logging**: 3 functions + +**Total: 70 documented functions** across 15 source files + +--- + +## References + +- Original call graph visualization: [`docs/Callgraph.svg`](Callgraph.svg) +- Architecture documentation: [`docs/ARCHITECTURE.md`](ARCHITECTURE.md) +- API documentation: [`docs/API.md`](API.md) \ No newline at end of file diff --git a/docs/SCHEMA.md b/docs/SCHEMA.md index 94bcff1..05be410 100644 --- a/docs/SCHEMA.md +++ b/docs/SCHEMA.md @@ -1,27 +1,16 @@ -# zosstorage Configuration Schema +# zosstorage Configuration (Deprecated schema) -This document defines the YAML configuration for the initramfs-only disk provisioning utility and the exact precedence rules between configuration sources. It complements [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). +This schema document is deprecated per [docs/adr/0002-defaults-only-no-external-config.md](docs/adr/0002-defaults-only-no-external-config.md). Runtime now uses defaults-only with a single optional kernel cmdline override. The YAML configuration file is not read at boot. -Canonical paths and keys -- Kernel cmdline key: zosstorage.config= -- Default config file path: /etc/zosstorage/config.yaml -- JSON state report path: /run/zosstorage/state.json -- Optional log file path: /run/zosstorage/zosstorage.log -- fstab generation: disabled by default -- Reserved filesystem labels: ZOSBOOT (ESP), ZOSDATA (all data filesystems) -- GPT partition names: zosboot, zosdata, zoscache +Active behavior (ADR-0002) +- Defaults-only: all settings are defined in code. No /etc/zosstorage/config.yaml is read. +- Optional kernel cmdline override: `zosstorage.topology=VALUE` can override only the topology. Legacy alias `zosstorage.topo=` is accepted. +- CLI: `--config` and `--topology` are deprecated and ignored (warnings emitted). Operational flags remain (`--apply`, `--show`, `--report`, `--fstab`, logging). +- Report path: `/run/zosstorage/state.json`. Optional log file: `/run/zosstorage/zosstorage.log`. +- Reserved labels: `ZOSBOOT` (ESP), `ZOSDATA` (data). GPT names: `zosboot`, `zosdata`, `zoscache`. -Precedence and merge strategy -1. Start from built-in defaults documented here. -2. Merge in the on-disk config file if present at /etc/zosstorage/config.yaml. -3. Merge CLI flags next; these override file values. -4. Merge kernel cmdline last; zosstorage.config= overrides CLI and file. -5. No interactive prompts are permitted. - -The kernel cmdline key zosstorage.config= accepts: -- A path to a YAML file inside the initramfs root (preferred). -- A file: absolute path (e.g., file:/run/config/zos.yaml). -- A data: URL containing base64 YAML (optional extension). +Historical reference (original YAML-based schema, no longer used at runtime) +The remainder of this document preserves the previous YAML schema for archival purposes only. Top-level YAML structure diff --git a/docs/adr/0002-defaults-only-no-external-config.md b/docs/adr/0002-defaults-only-no-external-config.md new file mode 100644 index 0000000..a93f654 --- /dev/null +++ b/docs/adr/0002-defaults-only-no-external-config.md @@ -0,0 +1,109 @@ +# ADR 0002: Defaults-Only Configuration; Remove External YAML Config + +Status +- Accepted +- Date: 2025-10-06 + +Context +- Running from initramfs at first boot provides no reliable access to an on-disk configuration file (e.g., /etc/zosstorage/config.yaml). An external file cannot be assumed to exist or be mounted. +- The previous design added precedence and merge complexity across file, CLI, and kernel cmdline as documented in [docs/SCHEMA.md](../SCHEMA.md) and implemented via [fn load_and_merge()](../../src/config/loader.rs:1), increasing maintenance burden and risks of drift. +- YAML introduces misconfiguration risk in early boot, adds I/O, and complicates idempotency guarantees without meaningful benefits for the intended minimal-first initializer. +- The desired model is to ship with sane built-in defaults, selected automatically from detected hardware topology; optional kernel cmdline may override only the topology choice for VM/lab scenarios. + +Decision +- Remove all dependency on an on-disk configuration file: + - Do not read /etc/zosstorage/config.yaml or any file-based config. + - Deprecate and ignore repository-local config files for runtime (e.g., config/zosstorage.yaml). The example file [config/zosstorage.example.yaml](../../config/zosstorage.example.yaml) remains as historical reference only and may be removed later. +- Deprecate the --config CLI flag in [struct Cli](../../src/cli/args.rs:1). If present, emit a deprecation warning and ignore it. +- Retain operational CLI flags and logging controls for usability: + - --apply, --show, --report PATH, --fstab, --log-level LEVEL, --log-to-file +- Replace the prior file/CLI/kernel precedence with a defaults-only policy plus a single optional kernel cmdline override: + - Recognized key: zosstorage.topology=VALUE + - The key may override only the topology selection; all other settings use built-in defaults. +- Topology defaults and override policy: + - 1 eligible disk: + - Default: btrfs_single + - Allowed cmdline overrides: btrfs_single, bcachefs_single + - 2 eligible disks: + - Default: dual_independent + - Allowed cmdline overrides: dual_independent, ssd_hdd_bcachefs, btrfs_raid1, bcachefs_2copy + - >2 eligible disks: + - Default: btrfs_raid1 + - Allowed cmdline overrides: btrfs_raid1, bcachefs_2copy +- Accept both snake_case and hyphenated forms for VALUE; normalize to [enum Topology](../../src/types.rs:1): + - btrfs_single | btrfs-single + - bcachefs_single | bcachefs-single + - dual_independent | dual-independent + - ssd_hdd_bcachefs | ssd-hdd-bcachefs + - btrfs_raid1 | btrfs-raid1 + - bcachefs_2copy | bcachefs-2copy +- Kernel cmdline parsing beyond topology is deferred; future extensions for VM workflows may be proposed separately. + +Rationale +- Eliminates unreachable configuration paths at first boot and simplifies the mental model. +- Reduces maintenance overhead by removing schema and precedence logic. +- Minimizes early-boot I/O and failure modes while preserving a targeted override for lab/VMs. +- Keeps the tool safe-by-default and fully idempotent without depending on external files. + +Consequences +- Documentation: + - Mark [docs/SCHEMA.md](../SCHEMA.md) as deprecated for runtime behavior; retain only as historical reference. + - Update [docs/ARCHITECTURE.md](../ARCHITECTURE.md) and [docs/SPECS.md](../SPECS.md) to reflect defaults-only configuration. + - Update [docs/API.md](../API.md) and [docs/API-SKELETONS.md](../API-SKELETONS.md) where they reference file-based config. +- CLI: + - [struct Cli](../../src/cli/args.rs:1) keeps operational flags; --config becomes a no-op with a deprecation warning. +- Code: + - Replace [fn load_and_merge()](../../src/config/loader.rs:1) with a minimal loader that: + - Builds a [struct Config](../../src/types.rs:1) entirely from baked-in defaults. + - Reads /proc/cmdline to optionally parse zosstorage.topology and normalize to [enum Topology](../../src/types.rs:1). + - Removes YAML parsing, file reads, and merge logic. +- Tests: + - Remove tests that depend on external YAML; add tests for cmdline override normalization and disk-count defaults. + +Defaults (authoritative) +- Partitioning: + - GPT only, 1 MiB alignment, BIOS boot 1 MiB first unless UEFI detected via [fn is_efi_boot()](../../src/util/mod.rs:1). + - ESP 512 MiB labeled ZOSBOOT (GPT name: zosboot), data uses GPT name zosdata. +- Filesystems: + - ESP: vfat labeled ZOSBOOT + - Data: label ZOSDATA + - Backend per topology (btrfs for btrfs_*; bcachefs for ssd_hdd_bcachefs and bcachefs_2copy) +- Mount scheme: + - Root-mount all data filesystems under /var/mounts/{UUID}; final subvolume/subdir mounts from the primary data FS to /var/cache/{system,etc,modules,vm-meta}; fstab remains optional. +- Idempotency: + - Unchanged: already-provisioned signals exit success-without-changes via [fn detect_existing_state()](../../src/idempotency/mod.rs:1). + +Implementation Plan +1) Introduce a minimal defaults loader in [src/config/loader.rs](../../src/config/loader.rs:1): + - new internal fn parse_topology_from_cmdline() -> Option + - new internal fn normalize_topology(s: &str) -> Option + - refactor load to construct Config from constants + optional topology override +2) CLI: + - Emit deprecation warning when --config is provided; ignore its value. +3) Docs: + - Add deprecation banner to [docs/SCHEMA.md](../SCHEMA.md). + - Adjust [README.md](../../README.md) to describe defaults and the zosstorage.topology override. +4) Tests: + - Add unit tests for normalization and disk-count policy; remove YAML-based tests. + +Backward Compatibility +- External YAML configuration is no longer supported at runtime. +- Kernel cmdline key zosstorage.config= is removed. Only zosstorage.topology remains recognized. +- The JSON report, labels, GPT names, and mount behavior remain unchanged. + +Security and Safety +- By eliminating external configuration input, we reduce attack surface and misconfiguration risk in early boot. +- The emptiness and idempotency checks continue to gate destructive operations. + +Open Items +- Decide whether to accept additional synonyms (e.g., “bcachefs-raid1”) and map them to existing [enum Topology](../../src/types.rs:1) variants; default is to reject unknown values with a clear error. +- Potential future kernel cmdline keys (e.g., logging level) may be explored via a separate ADR. + +Links +- Architecture: [docs/ARCHITECTURE.md](../ARCHITECTURE.md) +- API Index: [docs/API-SKELETONS.md](../API-SKELETONS.md) +- Specs: [docs/SPECS.md](../SPECS.md) +- CLI: [src/cli/args.rs](../../src/cli/args.rs) +- Config loader: [src/config/loader.rs](../../src/config/loader.rs) +- Types: [src/types.rs](../../src/types.rs) +- Util: [src/util/mod.rs](../../src/util/mod.rs) \ No newline at end of file diff --git a/docs/adr/callgraph.html b/docs/adr/callgraph.html new file mode 100644 index 0000000..9754ddc --- /dev/null +++ b/docs/adr/callgraph.html @@ -0,0 +1,2932 @@ + + + + + + + + + + + + + + + + +zosstorage/src + + + + + +cli + + + + + +config + + + + + +device + + + + + +fs + + + + + +idempotency + + + + + +logging + + + + + +mount + + + + + +orchestrator + + + + + +partition + + + + + +report + + + + + +util + + + + + + +            mod.rs             + + + + +detect_existing_state + + + + +is_empty_disk + + + + +parse_blkid_export + + + + +read_proc_partitions_names + + + + +base_name + + + + +is_partition_of + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            mod.rs             + + + + +S +  CmdOutput + + + + +which_tool + + + + +run_cmd + + + + +run_cmd_capture + + + + +udev_settle + + + + +is_efi_boot + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            discovery.rs             + + + + +S +  Disk + + + + +S +  DeviceFilter + + + + +impl DeviceFilter + + +matches + + + + + + +DeviceProvider + + +list_block_devices + + + + +probe_properties + + + + + + +S +  SysProvider + + + + +impl SysProvider + + +new + + + + + + +impl DeviceProvider for SysProvider + + +list_block_devices + + + + +probe_properties + + + + + + +discover + + + + +discover_with_provider + + + + +is_ignored_name + + + + +sys_block_path + + + + +base_name + + + + +is_removable_sysfs + + + + +is_partition_sysfs + + + + +read_disk_size_bytes + + + + +read_rotational + + + + +read_model_serial + + + + +read_optional_string + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            run.rs             + + + + +S +  Context + + + + +impl Context + + +new + + + + +with_show + + + + +with_apply + + + + +with_report_path + + + + +with_mount_existing + + + + +with_report_current + + + + +with_topology_from_cli + + + + +with_topology_from_cmdline + + + + + + +run + + + + +build_device_filter + + + + +enforce_empty_disks + + + + +role_str + + + + +build_summary_json + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            plan.rs             + + + + +E +  FsKind + + + + +S +  FsSpec + + + + +S +  FsPlan + + + + +S +  FsResult + + + + +plan_filesystems + + + + +make_filesystems + + + + +capture_uuid + + + + +parse_blkid_export + + + + +probe_existing_filesystems + + + + +tests_parse + + +parse_export_ok + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            plan.rs             + + + + +E +  PartRole + + + + +S +  PartitionSpec + + + + +S +  DiskPlan + + + + +S +  PartitionPlan + + + + +S +  PartitionResult + + + + +plan_partitions + + + + +apply_partitions + + +type_code + + + + +part_dev_path + + + + +sector_size_bytes + + + + +parse_sgdisk_info + + + + + + + + + + + + + + + + + + + + + + + + +            ops.rs             + + + + +S +  PlannedMount + + + + +S +  PlannedSubvolMount + + + + +S +  MountPlan + + + + +S +  MountResult + + + + +fstype_str + + + + +plan_mounts + + + + +apply_mounts + + + + +maybe_write_fstab + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            main.rs             + + + + +main + + + + +real_main + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            mod.rs             + + + + +S +  LogOptions + + + + +impl LogOptions + + +from_cli + + + + + + +level_from_str + + + + +init_logging + + + + + + + + + + + + + + + + + + + + + + +            loader.rs             + + + + +load_and_merge + + + + +validate + + + + +to_value + + + + +merge_value + + + + +cli_overlay_value + + + + +kernel_cmdline_topology + + + + +parse_topology_token + + + + +default_config + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            args.rs             + + + + +E +  LogLevelArg + + + + +impl std::fmt::Display for LogLevelArg + + +fmt + + + + + + +S +  Cli + + + + +from_args + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +            errors.rs             + + + + +E +  Error + + + + +T +  Result + + + + + + + + +            lib.rs             + + + + +cli + + + + +logging + + + + +config + + + + +device + + + + +partition + + + + +fs + + + + +mount + + + + +report + + + + +orchestrator + + + + +idempotency + + + + +util + + + + +errors + + + + +types + + + + + + + + +            types.rs             + + + + +S +  LoggingConfig + + + + +S +  DeviceSelection + + + + +E +  Topology + + + + +impl std::fmt::Display for Topology + + +fmt + + + + + + +S +  BiosBootSpec + + + + +S +  EspSpec + + + + +S +  DataSpec + + + + +S +  CacheSpec + + + + +S +  Partitioning + + + + +S +  BtrfsOptions + + + + +S +  BcachefsOptions + + + + +S +  VfatOptions + + + + +S +  FsOptions + + + + +E +  MountSchemeKind + + + + +S +  MountScheme + + + + +S +  ReportOptions + + + + +S +  Config + + + + + + + + +            mod.rs             + + + + +args + + + + + + + + +            mod.rs             + + + + +loader + + + + + + + + +            mod.rs             + + + + +discovery + + + + + + + + +            mod.rs             + + + + +plan + + + + + + + + +            mod.rs             + + + + +ops + + + + + + + + +            mod.rs             + + + + +run + + + + + + + + +            mod.rs             + + + + +plan + + + + + + + + +            mod.rs             + + + + +state + + + + + + + + +            state.rs             + + + + +S +  StateReport + + + + +build_report + + + + +write_report + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/cli/args.rs b/src/cli/args.rs index 82889ac..12a8649 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -58,8 +58,8 @@ impl std::fmt::Display for LogLevelArg { #[derive(Debug, Parser)] #[command(name = "zosstorage", disable_help_subcommand = true)] pub struct Cli { - /// Path to YAML configuration (mirrors kernel cmdline key 'zosstorage.config=') - #[arg(short = 'c', long = "config")] + /// DEPRECATED: external YAML configuration is not used at runtime (ADR-0002). Ignored with a warning. + #[arg(short = 'c', long = "config", hide = true)] pub config: Option, /// Log level: error, warn, info, debug @@ -74,7 +74,7 @@ pub struct Cli { #[arg(short = 's', long = "fstab", default_value_t = false)] pub fstab: bool, - /// Select topology (overrides config topology) + /// Select topology (CLI has precedence over kernel cmdline) #[arg(short = 't', long = "topology", value_enum)] pub topology: Option, @@ -82,8 +82,7 @@ pub struct Cli { #[arg(short = 'f', long = "force")] pub force: bool, - /// Allow removable devices (e.g., USB sticks) to be considered during discovery - /// Overrides config.device_selection.allow_removable when provided + /// Include removable devices (e.g., USB sticks) during discovery (default: false) #[arg(long = "allow-removable", default_value_t = false)] pub allow_removable: bool, @@ -100,7 +99,7 @@ pub struct Cli { #[arg(long = "show", default_value_t = false)] pub show: bool, - /// Write detection/planning JSON report to the given path (overrides config.report.path) + /// Write detection/planning JSON report to the given path #[arg(long = "report")] pub report: Option, diff --git a/src/config/loader.rs b/src/config/loader.rs index 17c596a..3168da2 100644 --- a/src/config/loader.rs +++ b/src/config/loader.rs @@ -1,11 +1,12 @@ //! Configuration loading, merging, and validation (loader). //! -//! Precedence (highest to lowest): -//! - CLI flags (and optional `--config PATH` when provided) -//! - Kernel cmdline key `zosstorage.topo=` -//! - Built-in defaults -//! -//! See [docs/SCHEMA.md](../../docs/SCHEMA.md) for the schema details. +//// Precedence and policy (ADR-0002): +//// - Built-in sane defaults for all settings. +//// - Kernel cmdline key `zosstorage.topology=` (legacy alias `zosstorage.topo=`) may override topology only. +//// - CLI flags control operational toggles only (logging, fstab, allow-removable). +//// - `--config` and `--topology` are deprecated and ignored (warnings emitted). +//// +//// Note: [docs/SCHEMA.md](../../docs/SCHEMA.md) is deprecated for runtime configuration; defaults are code-defined. // // REGION: API // api: config::load_and_merge(cli: &crate::cli::Cli) -> crate::Result @@ -40,23 +41,21 @@ // REGION: TODO-END use std::fs; -use std::path::Path; use crate::{cli::Cli, Error, Result}; use crate::types::*; use serde_json::{Map, Value, json}; -use base64::Engine as _; +use tracing::warn; -//// Load defaults, merge optional CLI --config, overlay CLI flags (highest precedence), -//// then consider kernel cmdline topology only if CLI omitted it. +//// Build configuration from built-in defaults and minimal operational CLI overlays. /// Returns a validated Config on success. /// -/// Behavior: -/// - Starts from built-in defaults (documented in docs/SCHEMA.md) -/// - Skips implicit /etc reads in initramfs -/// - If CLI --config is provided, merge that (overrides defaults) -/// - If kernel cmdline provides `zosstorage.topo=...` and CLI did NOT specify `--topology`, apply it -/// - Returns Error::Unimplemented when --force is used +/// Behavior (ADR-0002): +/// - Start from built-in defaults (code-defined). +/// - Ignore on-disk YAML and `--config` (deprecated); emit a warning if provided. +/// - CLI `--topology` is supported and has precedence when provided. +/// - If CLI does not provide topology, apply kernel cmdline `zosstorage.topology=` (or legacy `zosstorage.topo=`). +/// - Returns Error::Unimplemented when --force is used. pub fn load_and_merge(cli: &Cli) -> Result { if cli.force { return Err(Error::Unimplemented("--force flag is not implemented")); @@ -68,17 +67,17 @@ pub fn load_and_merge(cli: &Cli) -> Result { // 2) (initramfs) Skipped reading default on-disk config to avoid dependency on /etc. // If a config is needed, pass it via --config PATH or kernel cmdline `zosstorage.config=...`. - // 3) Merge CLI referenced config (if any) - if let Some(cfg_path) = &cli.config { - let v = load_yaml_value(cfg_path)?; - merge_value(&mut merged, v); + // 3) Deprecated config file flag: warn and ignore + if cli.config.is_some() { + warn!("--config is deprecated and ignored (ADR-0002: defaults-only)"); } + // (no file merge) // 4) Overlay CLI flags (non-path flags) let cli_overlay = cli_overlay_value(cli); merge_value(&mut merged, cli_overlay); - // 5) Kernel cmdline topology (only if CLI did not specify topology), e.g., `zosstorage.topo=dual-independent` + // 5) Kernel cmdline topology override only when CLI did not provide topology if cli.topology.is_none() { if let Some(topo) = kernel_cmdline_topology() { merge_value(&mut merged, json!({"topology": topo.to_string()})); @@ -204,14 +203,6 @@ fn to_value(t: T) -> Result { serde_json::to_value(t).map_err(|e| Error::Other(e.into())) } -fn load_yaml_value(path: &str) -> Result { - let s = fs::read_to_string(path) - .map_err(|e| Error::Config(format!("failed to read config file {}: {}", path, e)))?; - // Load as generic serde_json::Value for merging flexibility - let v: serde_json::Value = serde_yaml::from_str(&s) - .map_err(|e| Error::Config(format!("failed to parse YAML {}: {}", path, e)))?; - Ok(v) -} /// Merge b into a in-place: /// - Objects are merged key-by-key (recursively) @@ -259,7 +250,7 @@ fn cli_overlay_value(cli: &Cli) -> Value { root.insert("device_selection".into(), Value::Object(device_selection)); } - // topology override via --topology (avoid moving out of borrowed field) + // topology override via --topology (takes precedence over kernel cmdline) if let Some(t) = cli.topology.as_ref() { root.insert("topology".into(), Value::String(t.to_string())); } @@ -267,63 +258,18 @@ fn cli_overlay_value(cli: &Cli) -> Value { Value::Object(root) } -enum KernelConfigSource { - Path(String), - /// Raw YAML from a data: URL payload after decoding (if base64-encoded). - Data(String), -} - -/// Resolve a config from kernel cmdline key `zosstorage.config=`. -/// Supports: -/// - absolute paths (e.g., /run/zos.yaml) -/// - file:/absolute/path -/// - data:application/x-yaml;base64,BASE64CONTENT -/// Returns Ok(None) when key absent. -fn kernel_cmdline_config_source() -> Result> { +//// Parse kernel cmdline for topology override. +//// Accepts `zosstorage.topology=` and legacy alias `zosstorage.topo=`. +pub fn kernel_cmdline_topology() -> Option { let cmdline = fs::read_to_string("/proc/cmdline").unwrap_or_default(); for token in cmdline.split_whitespace() { - if let Some(rest) = token.strip_prefix("zosstorage.config=") { - let mut val = rest.to_string(); - // Trim surrounding quotes if any - if (val.starts_with('"') && val.ends_with('"')) || (val.starts_with('\'') && val.ends_with('\'')) { - val = val[1..val.len() - 1].to_string(); - } - if let Some(path) = val.strip_prefix("file:") { - return Ok(Some(KernelConfigSource::Path(path.to_string()))); - } - if let Some(data_url) = val.strip_prefix("data:") { - // data:[][;base64], - // Find comma separating the header and payload - if let Some(idx) = data_url.find(',') { - let (header, payload) = data_url.split_at(idx); - let payload = &payload[1..]; // skip the comma - let is_base64 = header.split(';').any(|seg| seg.eq_ignore_ascii_case("base64")); - let yaml = if is_base64 { - let decoded = base64::engine::general_purpose::STANDARD - .decode(payload.as_bytes()) - .map_err(|e| Error::Config(format!("invalid base64 in data: URL: {}", e)))?; - String::from_utf8(decoded) - .map_err(|e| Error::Config(format!("data: URL payload not UTF-8: {}", e)))? - } else { - payload.to_string() - }; - return Ok(Some(KernelConfigSource::Data(yaml))); - } else { - return Err(Error::Config("malformed data: URL (missing comma)".into())); - } - } - // Treat as direct path - return Ok(Some(KernelConfigSource::Path(val))); + let mut val_opt = None; + if let Some(v) = token.strip_prefix("zosstorage.topology=") { + val_opt = Some(v); + } else if let Some(v) = token.strip_prefix("zosstorage.topo=") { + val_opt = Some(v); } - } - Ok(None) -} - -fn kernel_cmdline_topology() -> Option { - let cmdline = fs::read_to_string("/proc/cmdline").unwrap_or_default(); - for token in cmdline.split_whitespace() { - if let Some(mut val) = token.strip_prefix("zosstorage.topo=") { - // Trim surrounding quotes if any + if let Some(mut val) = val_opt { if (val.starts_with('"') && val.ends_with('"')) || (val.starts_with('\'') && val.ends_with('\'')) { val = &val[1..val.len() - 1]; } @@ -338,7 +284,6 @@ fn kernel_cmdline_topology() -> Option { /// Helper to parse known topology tokens in kebab- or snake-case. fn parse_topology_token(s: &str) -> Option { - // Normalize underscores to hyphens for simpler matching. let k = s.trim().to_ascii_lowercase().replace('_', "-"); match k.as_str() { "btrfs-single" => Some(Topology::BtrfsSingle), diff --git a/src/main.rs b/src/main.rs index 4b38e95..82dd4c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,6 +54,8 @@ fn real_main() -> Result<()> { .with_apply(cli.apply) .with_mount_existing(cli.mount_existing) .with_report_current(cli.report_current) - .with_report_path(cli.report.clone()); + .with_report_path(cli.report.clone()) + .with_topology_from_cli(cli.topology.is_some()) + .with_topology_from_cmdline(config::loader::kernel_cmdline_topology().is_some() && cli.topology.is_none()); orchestrator::run(&ctx) } diff --git a/src/orchestrator/run.rs b/src/orchestrator/run.rs index ad9cc9e..59f0a9f 100644 --- a/src/orchestrator/run.rs +++ b/src/orchestrator/run.rs @@ -43,11 +43,12 @@ //! - Report generation and write use crate::{ - types::Config, + types::{Config, Topology}, logging::LogOptions, device::{discover, DeviceFilter, Disk}, idempotency, partition, + report::StateReport, fs as zfs, Error, Result, }; @@ -75,6 +76,10 @@ pub struct Context { pub report_current: bool, /// Optional report path override (when provided by CLI --report). pub report_path_override: Option, + /// True when topology was provided via CLI (--topology), giving it precedence. + pub topo_from_cli: bool, + /// True when topology was provided via kernel cmdline, giving it precedence if CLI omitted it. + pub topo_from_cmdline: bool, } impl Context { @@ -88,6 +93,8 @@ impl Context { mount_existing: false, report_current: false, report_path_override: None, + topo_from_cli: false, + topo_from_cmdline: false, } } @@ -136,6 +143,44 @@ impl Context { self.report_current = report_current; self } + + /// Mark that topology was provided via CLI (--topology). + pub fn with_topology_from_cli(mut self, v: bool) -> Self { + self.topo_from_cli = v; + self + } + + /// Mark that topology was provided via kernel cmdline (zosstorage.topology=). + pub fn with_topology_from_cmdline(mut self, v: bool) -> Self { + self.topo_from_cmdline = v; + self + } +} +#[derive(Debug, Clone, Copy)] +enum ProvisioningMode { + Apply, + Preview, +} + +#[derive(Debug, Clone, Copy)] +enum AutoDecision { + Apply, + MountExisting, +} + +#[derive(Debug)] +struct AutoSelection { + decision: AutoDecision, + fs_results: Option>, + state: Option, +} +#[derive(Debug, Clone, Copy)] +enum ExecutionMode { + ReportCurrent, + MountExisting, + Apply, + Preview, + Auto, } /// Run the one-shot provisioning flow. @@ -143,9 +188,8 @@ impl Context { /// Returns Ok(()) on success and also on success-noop when already provisioned. /// Any validation or execution failure aborts with an error. pub fn run(ctx: &Context) -> Result<()> { - info!("orchestrator: starting run() with topology {:?}", ctx.cfg.topology); + info!("orchestrator: starting run()"); - // Enforce mutually exclusive execution modes among: --mount-existing, --report-current, --apply let selected_modes = (ctx.mount_existing as u8) + (ctx.report_current as u8) + @@ -156,106 +200,166 @@ pub fn run(ctx: &Context) -> Result<()> { )); } - // Mode 1: Mount existing filesystems (non-destructive), based on on-disk headers. - if ctx.mount_existing { - info!("orchestrator: mount-existing mode"); - let fs_results = zfs::probe_existing_filesystems()?; - if fs_results.is_empty() { - return Err(Error::Mount( - "no existing filesystems with reserved labels (ZOSBOOT/ZOSDATA) were found".into(), - )); - } - let mplan = crate::mount::plan_mounts(&fs_results, &ctx.cfg)?; - let mres = crate::mount::apply_mounts(&mplan)?; - crate::mount::maybe_write_fstab(&mres, &ctx.cfg)?; + let preview_requested = ctx.show || ctx.report_path_override.is_some(); - // Optional JSON summary for mount-existing - if ctx.show || ctx.report_path_override.is_some() || ctx.report_current { - let now = format_rfc3339(SystemTime::now()).to_string(); - let fs_json: Vec = fs_results - .iter() - .map(|r| { - let kind_str = match r.kind { - zfs::FsKind::Vfat => "vfat", - zfs::FsKind::Btrfs => "btrfs", - zfs::FsKind::Bcachefs => "bcachefs", - }; - json!({ - "kind": kind_str, - "uuid": r.uuid, - "label": r.label, - "devices": r.devices, - }) - }) - .collect(); + let initial_mode = if ctx.report_current { + ExecutionMode::ReportCurrent + } else if ctx.mount_existing { + ExecutionMode::MountExisting + } else if ctx.apply { + ExecutionMode::Apply + } else if preview_requested { + ExecutionMode::Preview + } else { + ExecutionMode::Auto + }; - let mounts_json: Vec = mres - .iter() - .map(|m| { - json!({ - "source": m.source, - "target": m.target, - "fstype": m.fstype, - "options": m.options, - }) - }) - .collect(); - - let summary = json!({ - "version": "v1", - "timestamp": now, - "status": "mounted_existing", - "filesystems": fs_json, - "mounts": mounts_json, - }); - - if ctx.show || ctx.report_current { - println!("{}", summary); - } - if let Some(path) = &ctx.report_path_override { - fs::write(path, summary.to_string()).map_err(|e| { - Error::Report(format!("failed to write report to {}: {}", path, e)) - })?; - info!("orchestrator: wrote mount-existing report to {}", path); + match initial_mode { + ExecutionMode::ReportCurrent => run_report_current(ctx), + ExecutionMode::MountExisting => run_mount_existing(ctx, None, None), + ExecutionMode::Apply => run_provisioning(ctx, ProvisioningMode::Apply, None), + ExecutionMode::Preview => run_provisioning(ctx, ProvisioningMode::Preview, None), + ExecutionMode::Auto => { + let selection = auto_select_mode(ctx)?; + match selection.decision { + AutoDecision::MountExisting => { + run_mount_existing(ctx, selection.fs_results, selection.state) + } + AutoDecision::Apply => { + run_provisioning(ctx, ProvisioningMode::Apply, selection.state) + } } } + } +} - return Ok(()); +fn auto_select_mode(ctx: &Context) -> Result { + info!("orchestrator: auto-selecting execution mode"); + let state = idempotency::detect_existing_state()?; + let fs_results = zfs::probe_existing_filesystems()?; + + if let Some(state) = state { + info!("orchestrator: provisioned state detected; attempting mount-existing flow"); + return Ok(AutoSelection { + decision: AutoDecision::MountExisting, + fs_results: if fs_results.is_empty() { None } else { Some(fs_results) }, + state: Some(state), + }); } - // Mode 3: Report current initialized filesystems and mounts (non-destructive). - if ctx.report_current { - info!("orchestrator: report-current mode"); - let fs_results = zfs::probe_existing_filesystems()?; + if !fs_results.is_empty() { + info!( + "orchestrator: detected {} filesystem(s) with reserved labels; selecting mount-existing", + fs_results.len() + ); + return Ok(AutoSelection { + decision: AutoDecision::MountExisting, + fs_results: Some(fs_results), + state: None, + }); + } - // Parse /proc/mounts and include only our relevant targets. - let mounts_content = fs::read_to_string("/proc/mounts").unwrap_or_default(); - let mounts_json: Vec = mounts_content - .lines() - .filter_map(|line| { - let mut it = line.split_whitespace(); - let source = it.next()?; - let target = it.next()?; - let fstype = it.next()?; - let options = it.next().unwrap_or(""); - if target.starts_with("/var/mounts/") - || target == "/var/cache/system" - || target == "/var/cache/etc" - || target == "/var/cache/modules" - || target == "/var/cache/vm-meta" - { - Some(json!({ - "source": source, - "target": target, - "fstype": fstype, - "options": options - })) - } else { - None - } + info!( + "orchestrator: no provisioned state or labeled filesystems detected; selecting apply mode (topology={:?})", + ctx.cfg.topology + ); + + Ok(AutoSelection { + decision: AutoDecision::Apply, + fs_results: None, + state: None, + }) +} + +fn run_report_current(ctx: &Context) -> Result<()> { + info!("orchestrator: report-current mode"); + let fs_results = zfs::probe_existing_filesystems()?; + + let mounts_content = fs::read_to_string("/proc/mounts").unwrap_or_default(); + let mounts_json: Vec = mounts_content + .lines() + .filter_map(|line| { + let mut it = line.split_whitespace(); + let source = it.next()?; + let target = it.next()?; + let fstype = it.next()?; + let options = it.next().unwrap_or(""); + if target.starts_with("/var/mounts/") + || target == "/var/cache/system" + || target == "/var/cache/etc" + || target == "/var/cache/modules" + || target == "/var/cache/vm-meta" + { + Some(json!({ + "source": source, + "target": target, + "fstype": fstype, + "options": options + })) + } else { + None + } + }) + .collect(); + + let fs_json: Vec = fs_results + .iter() + .map(|r| { + let kind_str = match r.kind { + zfs::FsKind::Vfat => "vfat", + zfs::FsKind::Btrfs => "btrfs", + zfs::FsKind::Bcachefs => "bcachefs", + }; + json!({ + "kind": kind_str, + "uuid": r.uuid, + "label": r.label, + "devices": r.devices }) - .collect(); + }) + .collect(); + let now = format_rfc3339(SystemTime::now()).to_string(); + let summary = json!({ + "version": "v1", + "timestamp": now, + "status": "observed", + "filesystems": fs_json, + "mounts": mounts_json + }); + + println!("{}", summary); + if let Some(path) = &ctx.report_path_override { + fs::write(path, summary.to_string()).map_err(|e| { + Error::Report(format!("failed to write report to {}: {}", path, e)) + })?; + info!("orchestrator: wrote report-current to {}", path); + } + Ok(()) +} + +fn run_mount_existing( + ctx: &Context, + fs_results_override: Option>, + state_hint: Option, +) -> Result<()> { + info!("orchestrator: mount-existing mode"); + let fs_results = match fs_results_override { + Some(results) => results, + None => zfs::probe_existing_filesystems()?, + }; + if fs_results.is_empty() { + return Err(Error::Mount( + "no existing filesystems with reserved labels (ZOSBOOT/ZOSDATA) were found".into(), + )); + } + + let mplan = crate::mount::plan_mounts(&fs_results, &ctx.cfg)?; + let mres = crate::mount::apply_mounts(&mplan)?; + crate::mount::maybe_write_fstab(&mres, &ctx.cfg)?; + + if ctx.show || ctx.report_path_override.is_some() || ctx.report_current { + let now = format_rfc3339(SystemTime::now()).to_string(); let fs_json: Vec = fs_results .iter() .map(|r| { @@ -268,82 +372,108 @@ pub fn run(ctx: &Context) -> Result<()> { "kind": kind_str, "uuid": r.uuid, "label": r.label, - "devices": r.devices + "devices": r.devices, }) }) .collect(); - let now = format_rfc3339(SystemTime::now()).to_string(); - let summary = json!({ + let mounts_json: Vec = mres + .iter() + .map(|m| { + json!({ + "source": m.source, + "target": m.target, + "fstype": m.fstype, + "options": m.options, + }) + }) + .collect(); + + let mut summary = json!({ "version": "v1", "timestamp": now, - "status": "observed", + "status": "mounted_existing", "filesystems": fs_json, - "mounts": mounts_json + "mounts": mounts_json, }); - // In report-current mode, default to stdout; also honor --report path when provided. - println!("{}", summary); + if let Some(state) = state_hint { + if let Ok(state_json) = to_value(&state) { + if let Some(obj) = summary.as_object_mut() { + obj.insert("state".to_string(), state_json); + } + } + } + + if ctx.show || ctx.report_current { + println!("{}", summary); + } if let Some(path) = &ctx.report_path_override { fs::write(path, summary.to_string()).map_err(|e| { Error::Report(format!("failed to write report to {}: {}", path, e)) })?; - info!("orchestrator: wrote report-current to {}", path); - } - return Ok(()); - } - - // Default path: plan (and optionally apply) for empty-disk initialization workflow. - - // 1) Idempotency pre-flight: if already provisioned, optionally emit summary then exit success. - match idempotency::detect_existing_state()? { - Some(state) => { - info!("orchestrator: already provisioned"); - if ctx.show || ctx.report_path_override.is_some() { - let now = format_rfc3339(SystemTime::now()).to_string(); - let state_json = to_value(&state) - .map_err(|e| Error::Report(format!("failed to serialize StateReport: {}", e)))?; - let summary = json!({ - "version": "v1", - "timestamp": now, - "status": "already_provisioned", - "state": state_json - }); - if ctx.show { - println!("{}", summary); - } - if let Some(path) = &ctx.report_path_override { - fs::write(path, summary.to_string()).map_err(|e| { - Error::Report(format!("failed to write report to {}: {}", path, e)) - })?; - info!("orchestrator: wrote idempotency report to {}", path); - } - } - return Ok(()); - } - None => { - debug!("orchestrator: not provisioned; continuing"); + info!("orchestrator: wrote mount-existing report to {}", path); } } - // 2) Device discovery using compiled filter from config. + Ok(()) +} + +fn run_provisioning( + ctx: &Context, + mode: ProvisioningMode, + state_hint: Option, +) -> Result<()> { + let preview_outputs = ctx.show || ctx.report_path_override.is_some(); + + let mut state_opt = state_hint; + if state_opt.is_none() { + state_opt = idempotency::detect_existing_state()?; + } + + if let Some(state) = state_opt { + info!("orchestrator: already provisioned; ensuring mounts are active"); + return run_mount_existing(ctx, None, Some(state)); + } + let filter = build_device_filter(&ctx.cfg)?; let disks = discover(&filter)?; info!("orchestrator: discovered {} eligible disk(s)", disks.len()); - // 3) Emptiness enforcement: skip in preview mode (--show/--report) to allow planning output. - let preview = ctx.show || ctx.report_path_override.is_some(); - if ctx.cfg.partitioning.require_empty_disks && !preview { - enforce_empty_disks(&disks)?; - info!("orchestrator: all target disks verified empty"); - } else if ctx.cfg.partitioning.require_empty_disks && preview { - warn!("orchestrator: preview mode detected (--show/--report); skipping empty-disk enforcement"); - } else { + if ctx.cfg.partitioning.require_empty_disks { + if matches!(mode, ProvisioningMode::Apply) { + enforce_empty_disks(&disks)?; + info!("orchestrator: all target disks verified empty"); + } else { + warn!("orchestrator: preview mode detected (--show/--report); skipping empty-disk enforcement"); + } + } else if matches!(mode, ProvisioningMode::Apply) { warn!("orchestrator: require_empty_disks=false; proceeding without emptiness enforcement"); } - // 4) Partition planning (declarative). - let plan = partition::plan_partitions(&disks, &ctx.cfg)?; + let effective_cfg = { + let mut c = ctx.cfg.clone(); + if !(ctx.topo_from_cli || ctx.topo_from_cmdline) { + let auto_topo = if disks.len() == 1 { + Topology::BtrfsSingle + } else if disks.len() == 2 { + Topology::DualIndependent + } else { + Topology::BtrfsRaid1 + }; + if c.topology != auto_topo { + info!("orchestrator: topology auto-selected {:?}", auto_topo); + c.topology = auto_topo; + } else { + info!("orchestrator: using configured topology {:?}", c.topology); + } + } else { + info!("orchestrator: using overridden topology {:?}", c.topology); + } + c + }; + + let plan = partition::plan_partitions(&disks, &effective_cfg)?; debug!( "orchestrator: partition plan ready (alignment={} MiB, disks={})", plan.alignment_mib, @@ -353,8 +483,7 @@ pub fn run(ctx: &Context) -> Result<()> { debug!("plan for {}: {} part(s)", dp.disk.path, dp.parts.len()); } - // Apply mode: perform destructive partition application now. - if ctx.apply { + if matches!(mode, ProvisioningMode::Apply) { info!("orchestrator: apply mode enabled; applying partition plan"); let part_results = partition::apply_partitions(&plan)?; info!( @@ -363,34 +492,28 @@ pub fn run(ctx: &Context) -> Result<()> { part_results.len() ); - // Filesystem planning and creation - let fs_plan = zfs::plan_filesystems(&part_results, &ctx.cfg)?; + let fs_plan = zfs::plan_filesystems(&part_results, &effective_cfg)?; info!( "orchestrator: filesystem plan contains {} spec(s)", fs_plan.specs.len() ); - let fs_results = zfs::make_filesystems(&fs_plan, &ctx.cfg)?; + let fs_results = zfs::make_filesystems(&fs_plan, &effective_cfg)?; info!("orchestrator: created {} filesystem(s)", fs_results.len()); - // Mount planning and application - let mplan = crate::mount::plan_mounts(&fs_results, &ctx.cfg)?; + let mplan = crate::mount::plan_mounts(&fs_results, &effective_cfg)?; let mres = crate::mount::apply_mounts(&mplan)?; - crate::mount::maybe_write_fstab(&mres, &ctx.cfg)?; + crate::mount::maybe_write_fstab(&mres, &effective_cfg)?; return Ok(()); } - // Preview-only path info!("orchestrator: pre-flight complete (idempotency checked, devices discovered, plan computed)"); - // Optional: emit JSON summary via --show or write via --report - if ctx.show || ctx.report_path_override.is_some() { - let summary = build_summary_json(&disks, &plan, &ctx.cfg)?; + if preview_outputs { + let summary = build_summary_json(&disks, &plan, &effective_cfg)?; if ctx.show { - // Print compact JSON to stdout println!("{}", summary); } if let Some(path) = &ctx.report_path_override { - // Best-effort write (non-atomic for now, pending report::write_report implementation) fs::write(path, summary.to_string()).map_err(|e| { Error::Report(format!("failed to write report to {}: {}", path, e)) })?; @@ -511,14 +634,7 @@ fn build_summary_json(disks: &[Disk], plan: &partition::PartitionPlan, cfg: &Con } // Decide filesystem kinds and planned mountpoints (template) from plan + cfg.topology - let topo_str = match cfg.topology { - crate::types::Topology::BtrfsSingle => "btrfs_single", - crate::types::Topology::BcachefsSingle => "bcachefs_single", - crate::types::Topology::DualIndependent => "dual_independent", - crate::types::Topology::SsdHddBcachefs => "ssd_hdd_bcachefs", - crate::types::Topology::Bcachefs2Copy => "bcachefs2_copy", - crate::types::Topology::BtrfsRaid1 => "btrfs_raid1", - }; + let topo_str = cfg.topology.to_string(); // Count roles across plan to infer filesystems let mut esp_count = 0usize; diff --git a/src/types.rs b/src/types.rs index b1270b1..c8f3d28 100644 --- a/src/types.rs +++ b/src/types.rs @@ -38,7 +38,7 @@ pub struct DeviceSelection { pub min_size_gib: u64, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, ValueEnum)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)] #[serde(rename_all = "snake_case")] #[value(rename_all = "snake_case")] pub enum Topology {