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
This commit is contained in:
36
README.md
36
README.md
@@ -24,10 +24,10 @@ Key modules
|
|||||||
- [src/mount/ops.rs](src/mount/ops.rs)
|
- [src/mount/ops.rs](src/mount/ops.rs)
|
||||||
|
|
||||||
Features at a glance
|
Features at a glance
|
||||||
- Topology-driven planning with built-in defaults: BtrfsSingle, BcachefsSingle, DualIndependent, Bcachefs2Copy, BtrfsRaid1, SsdHddBcachefs
|
- 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)
|
- 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
|
- 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
|
Requirements
|
||||||
- Linux with /proc and /sys mounted (initramfs friendly)
|
- Linux with /proc and /sys mounted (initramfs friendly)
|
||||||
@@ -45,8 +45,6 @@ Install and build
|
|||||||
Binary is target/release/zosstorage.
|
Binary is target/release/zosstorage.
|
||||||
|
|
||||||
CLI usage
|
CLI usage
|
||||||
- Topology selection (config optional):
|
|
||||||
-t, --topology btrfs-single|bcachefs-single|dual-independent|bcachefs-2copy|btrfs-raid1|ssd-hdd-bcachefs
|
|
||||||
- Preview (non-destructive):
|
- Preview (non-destructive):
|
||||||
--show Print JSON summary to stdout
|
--show Print JSON summary to stdout
|
||||||
--report PATH Write JSON summary to a file
|
--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-level LEVEL error|warn|info|debug (default: info)
|
||||||
-L, --log-to-file Also write logs to /run/zosstorage/zosstorage.log
|
-L, --log-to-file Also write logs to /run/zosstorage/zosstorage.log
|
||||||
- Other:
|
- Other:
|
||||||
-c, --config PATH Merge a YAML config file (overrides defaults)
|
|
||||||
-s, --fstab Enable writing /etc/fstab entries (when mounts are applied)
|
-s, --fstab Enable writing /etc/fstab entries (when mounts are applied)
|
||||||
-a, --apply Perform partitioning, filesystem creation, and mounts (destructive)
|
-a, --apply Perform partitioning, filesystem creation, and mounts (destructive)
|
||||||
-f, --force Present but not implemented (returns an error)
|
-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
|
Examples
|
||||||
- Single disk plan with debug logs:
|
- Single disk plan with debug logs (defaults to btrfs_single automatically):
|
||||||
sudo ./zosstorage --show -t btrfs-single -l debug
|
sudo ./zosstorage --show -l debug
|
||||||
- RAID1 btrfs across two disks; print and write summary:
|
- Two-disk plan (defaults to dual_independent automatically), write summary:
|
||||||
sudo ./zosstorage --show --report /run/zosstorage/plan.json -t btrfs-raid1 -l debug -L
|
sudo ./zosstorage --show --report /run/zosstorage/plan.json -l debug -L
|
||||||
- SSD+HDD bcachefs plan, include removable devices (for lab cases):
|
- Include removable devices for lab scenarios:
|
||||||
sudo ./zosstorage --show -t ssd-hdd-bcachefs --allow-removable -l debug
|
sudo ./zosstorage --show --allow-removable -l debug
|
||||||
- Quiet plan to file:
|
- Quiet plan to file:
|
||||||
sudo ./zosstorage --report /run/zosstorage/plan.json -t dual-independent
|
sudo ./zosstorage --report /run/zosstorage/plan.json
|
||||||
- Apply single-disk btrfs (DESTRUCTIVE; wipes target disk):
|
- Apply single-disk plan (DESTRUCTIVE; wipes target disk; defaults select topology automatically):
|
||||||
sudo ./zosstorage --apply -t btrfs-single
|
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)
|
Preview JSON shape (examples)
|
||||||
1) Already provisioned (idempotency success):
|
1) Already provisioned (idempotency success):
|
||||||
|
|||||||
1716
docs/Callgraph.svg
Normal file
1716
docs/Callgraph.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 122 KiB |
294
docs/FUNCTION_LIST.md
Normal file
294
docs/FUNCTION_LIST.md
Normal file
@@ -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<T>` - Type alias for `std::result::Result<T, Error>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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)
|
||||||
@@ -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
|
Active behavior (ADR-0002)
|
||||||
- Kernel cmdline key: zosstorage.config=
|
- Defaults-only: all settings are defined in code. No /etc/zosstorage/config.yaml is read.
|
||||||
- Default config file path: /etc/zosstorage/config.yaml
|
- Optional kernel cmdline override: `zosstorage.topology=VALUE` can override only the topology. Legacy alias `zosstorage.topo=` is accepted.
|
||||||
- JSON state report path: /run/zosstorage/state.json
|
- CLI: `--config` and `--topology` are deprecated and ignored (warnings emitted). Operational flags remain (`--apply`, `--show`, `--report`, `--fstab`, logging).
|
||||||
- Optional log file path: /run/zosstorage/zosstorage.log
|
- Report path: `/run/zosstorage/state.json`. Optional log file: `/run/zosstorage/zosstorage.log`.
|
||||||
- fstab generation: disabled by default
|
- Reserved labels: `ZOSBOOT` (ESP), `ZOSDATA` (data). GPT names: `zosboot`, `zosdata`, `zoscache`.
|
||||||
- Reserved filesystem labels: ZOSBOOT (ESP), ZOSDATA (all data filesystems)
|
|
||||||
- GPT partition names: zosboot, zosdata, zoscache
|
|
||||||
|
|
||||||
Precedence and merge strategy
|
Historical reference (original YAML-based schema, no longer used at runtime)
|
||||||
1. Start from built-in defaults documented here.
|
The remainder of this document preserves the previous YAML schema for archival purposes only.
|
||||||
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).
|
|
||||||
|
|
||||||
Top-level YAML structure
|
Top-level YAML structure
|
||||||
|
|
||||||
|
|||||||
109
docs/adr/0002-defaults-only-no-external-config.md
Normal file
109
docs/adr/0002-defaults-only-no-external-config.md
Normal file
@@ -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<Topology>
|
||||||
|
- new internal fn normalize_topology(s: &str) -> Option<Topology>
|
||||||
|
- 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)
|
||||||
2932
docs/adr/callgraph.html
Normal file
2932
docs/adr/callgraph.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -58,8 +58,8 @@ impl std::fmt::Display for LogLevelArg {
|
|||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
#[command(name = "zosstorage", disable_help_subcommand = true)]
|
#[command(name = "zosstorage", disable_help_subcommand = true)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
/// Path to YAML configuration (mirrors kernel cmdline key 'zosstorage.config=')
|
/// DEPRECATED: external YAML configuration is not used at runtime (ADR-0002). Ignored with a warning.
|
||||||
#[arg(short = 'c', long = "config")]
|
#[arg(short = 'c', long = "config", hide = true)]
|
||||||
pub config: Option<String>,
|
pub config: Option<String>,
|
||||||
|
|
||||||
/// Log level: error, warn, info, debug
|
/// Log level: error, warn, info, debug
|
||||||
@@ -74,7 +74,7 @@ pub struct Cli {
|
|||||||
#[arg(short = 's', long = "fstab", default_value_t = false)]
|
#[arg(short = 's', long = "fstab", default_value_t = false)]
|
||||||
pub fstab: bool,
|
pub fstab: bool,
|
||||||
|
|
||||||
/// Select topology (overrides config topology)
|
/// Select topology (CLI has precedence over kernel cmdline)
|
||||||
#[arg(short = 't', long = "topology", value_enum)]
|
#[arg(short = 't', long = "topology", value_enum)]
|
||||||
pub topology: Option<crate::types::Topology>,
|
pub topology: Option<crate::types::Topology>,
|
||||||
|
|
||||||
@@ -82,8 +82,7 @@ pub struct Cli {
|
|||||||
#[arg(short = 'f', long = "force")]
|
#[arg(short = 'f', long = "force")]
|
||||||
pub force: bool,
|
pub force: bool,
|
||||||
|
|
||||||
/// Allow removable devices (e.g., USB sticks) to be considered during discovery
|
/// Include removable devices (e.g., USB sticks) during discovery (default: false)
|
||||||
/// Overrides config.device_selection.allow_removable when provided
|
|
||||||
#[arg(long = "allow-removable", default_value_t = false)]
|
#[arg(long = "allow-removable", default_value_t = false)]
|
||||||
pub allow_removable: bool,
|
pub allow_removable: bool,
|
||||||
|
|
||||||
@@ -100,7 +99,7 @@ pub struct Cli {
|
|||||||
#[arg(long = "show", default_value_t = false)]
|
#[arg(long = "show", default_value_t = false)]
|
||||||
pub show: bool,
|
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")]
|
#[arg(long = "report")]
|
||||||
pub report: Option<String>,
|
pub report: Option<String>,
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
//! Configuration loading, merging, and validation (loader).
|
//! Configuration loading, merging, and validation (loader).
|
||||||
//!
|
//!
|
||||||
//! Precedence (highest to lowest):
|
//// Precedence and policy (ADR-0002):
|
||||||
//! - CLI flags (and optional `--config PATH` when provided)
|
//// - Built-in sane defaults for all settings.
|
||||||
//! - Kernel cmdline key `zosstorage.topo=`
|
//// - Kernel cmdline key `zosstorage.topology=` (legacy alias `zosstorage.topo=`) may override topology only.
|
||||||
//! - Built-in defaults
|
//// - CLI flags control operational toggles only (logging, fstab, allow-removable).
|
||||||
//!
|
//// - `--config` and `--topology` are deprecated and ignored (warnings emitted).
|
||||||
//! See [docs/SCHEMA.md](../../docs/SCHEMA.md) for the schema details.
|
////
|
||||||
|
//// Note: [docs/SCHEMA.md](../../docs/SCHEMA.md) is deprecated for runtime configuration; defaults are code-defined.
|
||||||
//
|
//
|
||||||
// REGION: API
|
// REGION: API
|
||||||
// api: config::load_and_merge(cli: &crate::cli::Cli) -> crate::Result<crate::config::types::Config>
|
// api: config::load_and_merge(cli: &crate::cli::Cli) -> crate::Result<crate::config::types::Config>
|
||||||
@@ -40,23 +41,21 @@
|
|||||||
// REGION: TODO-END
|
// REGION: TODO-END
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use crate::{cli::Cli, Error, Result};
|
use crate::{cli::Cli, Error, Result};
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
use serde_json::{Map, Value, json};
|
use serde_json::{Map, Value, json};
|
||||||
use base64::Engine as _;
|
use tracing::warn;
|
||||||
|
|
||||||
//// Load defaults, merge optional CLI --config, overlay CLI flags (highest precedence),
|
//// Build configuration from built-in defaults and minimal operational CLI overlays.
|
||||||
//// then consider kernel cmdline topology only if CLI omitted it.
|
|
||||||
/// Returns a validated Config on success.
|
/// Returns a validated Config on success.
|
||||||
///
|
///
|
||||||
/// Behavior:
|
/// Behavior (ADR-0002):
|
||||||
/// - Starts from built-in defaults (documented in docs/SCHEMA.md)
|
/// - Start from built-in defaults (code-defined).
|
||||||
/// - Skips implicit /etc reads in initramfs
|
/// - Ignore on-disk YAML and `--config` (deprecated); emit a warning if provided.
|
||||||
/// - If CLI --config is provided, merge that (overrides defaults)
|
/// - CLI `--topology` is supported and has precedence when provided.
|
||||||
/// - If kernel cmdline provides `zosstorage.topo=...` and CLI did NOT specify `--topology`, apply it
|
/// - If CLI does not provide topology, apply kernel cmdline `zosstorage.topology=` (or legacy `zosstorage.topo=`).
|
||||||
/// - Returns Error::Unimplemented when --force is used
|
/// - Returns Error::Unimplemented when --force is used.
|
||||||
pub fn load_and_merge(cli: &Cli) -> Result<Config> {
|
pub fn load_and_merge(cli: &Cli) -> Result<Config> {
|
||||||
if cli.force {
|
if cli.force {
|
||||||
return Err(Error::Unimplemented("--force flag is not implemented"));
|
return Err(Error::Unimplemented("--force flag is not implemented"));
|
||||||
@@ -68,17 +67,17 @@ pub fn load_and_merge(cli: &Cli) -> Result<Config> {
|
|||||||
// 2) (initramfs) Skipped reading default on-disk config to avoid dependency on /etc.
|
// 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=...`.
|
// If a config is needed, pass it via --config PATH or kernel cmdline `zosstorage.config=...`.
|
||||||
|
|
||||||
// 3) Merge CLI referenced config (if any)
|
// 3) Deprecated config file flag: warn and ignore
|
||||||
if let Some(cfg_path) = &cli.config {
|
if cli.config.is_some() {
|
||||||
let v = load_yaml_value(cfg_path)?;
|
warn!("--config is deprecated and ignored (ADR-0002: defaults-only)");
|
||||||
merge_value(&mut merged, v);
|
|
||||||
}
|
}
|
||||||
|
// (no file merge)
|
||||||
|
|
||||||
// 4) Overlay CLI flags (non-path flags)
|
// 4) Overlay CLI flags (non-path flags)
|
||||||
let cli_overlay = cli_overlay_value(cli);
|
let cli_overlay = cli_overlay_value(cli);
|
||||||
merge_value(&mut merged, cli_overlay);
|
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 cli.topology.is_none() {
|
||||||
if let Some(topo) = kernel_cmdline_topology() {
|
if let Some(topo) = kernel_cmdline_topology() {
|
||||||
merge_value(&mut merged, json!({"topology": topo.to_string()}));
|
merge_value(&mut merged, json!({"topology": topo.to_string()}));
|
||||||
@@ -204,14 +203,6 @@ fn to_value<T: serde::Serialize>(t: T) -> Result<Value> {
|
|||||||
serde_json::to_value(t).map_err(|e| Error::Other(e.into()))
|
serde_json::to_value(t).map_err(|e| Error::Other(e.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_yaml_value(path: &str) -> Result<Value> {
|
|
||||||
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:
|
/// Merge b into a in-place:
|
||||||
/// - Objects are merged key-by-key (recursively)
|
/// - 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));
|
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() {
|
if let Some(t) = cli.topology.as_ref() {
|
||||||
root.insert("topology".into(), Value::String(t.to_string()));
|
root.insert("topology".into(), Value::String(t.to_string()));
|
||||||
}
|
}
|
||||||
@@ -267,63 +258,18 @@ fn cli_overlay_value(cli: &Cli) -> Value {
|
|||||||
Value::Object(root)
|
Value::Object(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum KernelConfigSource {
|
//// Parse kernel cmdline for topology override.
|
||||||
Path(String),
|
//// Accepts `zosstorage.topology=` and legacy alias `zosstorage.topo=`.
|
||||||
/// Raw YAML from a data: URL payload after decoding (if base64-encoded).
|
pub fn kernel_cmdline_topology() -> Option<Topology> {
|
||||||
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<Option<KernelConfigSource>> {
|
|
||||||
let cmdline = fs::read_to_string("/proc/cmdline").unwrap_or_default();
|
let cmdline = fs::read_to_string("/proc/cmdline").unwrap_or_default();
|
||||||
for token in cmdline.split_whitespace() {
|
for token in cmdline.split_whitespace() {
|
||||||
if let Some(rest) = token.strip_prefix("zosstorage.config=") {
|
let mut val_opt = None;
|
||||||
let mut val = rest.to_string();
|
if let Some(v) = token.strip_prefix("zosstorage.topology=") {
|
||||||
// Trim surrounding quotes if any
|
val_opt = Some(v);
|
||||||
if (val.starts_with('"') && val.ends_with('"')) || (val.starts_with('\'') && val.ends_with('\'')) {
|
} else if let Some(v) = token.strip_prefix("zosstorage.topo=") {
|
||||||
val = val[1..val.len() - 1].to_string();
|
val_opt = Some(v);
|
||||||
}
|
}
|
||||||
if let Some(path) = val.strip_prefix("file:") {
|
if let Some(mut val) = val_opt {
|
||||||
return Ok(Some(KernelConfigSource::Path(path.to_string())));
|
|
||||||
}
|
|
||||||
if let Some(data_url) = val.strip_prefix("data:") {
|
|
||||||
// data:[<mediatype>][;base64],<data>
|
|
||||||
// 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)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn kernel_cmdline_topology() -> Option<Topology> {
|
|
||||||
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 (val.starts_with('"') && val.ends_with('"')) || (val.starts_with('\'') && val.ends_with('\'')) {
|
if (val.starts_with('"') && val.ends_with('"')) || (val.starts_with('\'') && val.ends_with('\'')) {
|
||||||
val = &val[1..val.len() - 1];
|
val = &val[1..val.len() - 1];
|
||||||
}
|
}
|
||||||
@@ -338,7 +284,6 @@ fn kernel_cmdline_topology() -> Option<Topology> {
|
|||||||
|
|
||||||
/// Helper to parse known topology tokens in kebab- or snake-case.
|
/// Helper to parse known topology tokens in kebab- or snake-case.
|
||||||
fn parse_topology_token(s: &str) -> Option<Topology> {
|
fn parse_topology_token(s: &str) -> Option<Topology> {
|
||||||
// Normalize underscores to hyphens for simpler matching.
|
|
||||||
let k = s.trim().to_ascii_lowercase().replace('_', "-");
|
let k = s.trim().to_ascii_lowercase().replace('_', "-");
|
||||||
match k.as_str() {
|
match k.as_str() {
|
||||||
"btrfs-single" => Some(Topology::BtrfsSingle),
|
"btrfs-single" => Some(Topology::BtrfsSingle),
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ fn real_main() -> Result<()> {
|
|||||||
.with_apply(cli.apply)
|
.with_apply(cli.apply)
|
||||||
.with_mount_existing(cli.mount_existing)
|
.with_mount_existing(cli.mount_existing)
|
||||||
.with_report_current(cli.report_current)
|
.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)
|
orchestrator::run(&ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,11 +43,12 @@
|
|||||||
//! - Report generation and write
|
//! - Report generation and write
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
types::Config,
|
types::{Config, Topology},
|
||||||
logging::LogOptions,
|
logging::LogOptions,
|
||||||
device::{discover, DeviceFilter, Disk},
|
device::{discover, DeviceFilter, Disk},
|
||||||
idempotency,
|
idempotency,
|
||||||
partition,
|
partition,
|
||||||
|
report::StateReport,
|
||||||
fs as zfs,
|
fs as zfs,
|
||||||
Error, Result,
|
Error, Result,
|
||||||
};
|
};
|
||||||
@@ -75,6 +76,10 @@ pub struct Context {
|
|||||||
pub report_current: bool,
|
pub report_current: bool,
|
||||||
/// Optional report path override (when provided by CLI --report).
|
/// Optional report path override (when provided by CLI --report).
|
||||||
pub report_path_override: Option<String>,
|
pub report_path_override: Option<String>,
|
||||||
|
/// 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 {
|
impl Context {
|
||||||
@@ -88,6 +93,8 @@ impl Context {
|
|||||||
mount_existing: false,
|
mount_existing: false,
|
||||||
report_current: false,
|
report_current: false,
|
||||||
report_path_override: None,
|
report_path_override: None,
|
||||||
|
topo_from_cli: false,
|
||||||
|
topo_from_cmdline: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +143,44 @@ impl Context {
|
|||||||
self.report_current = report_current;
|
self.report_current = report_current;
|
||||||
self
|
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<Vec<zfs::FsResult>>,
|
||||||
|
state: Option<StateReport>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum ExecutionMode {
|
||||||
|
ReportCurrent,
|
||||||
|
MountExisting,
|
||||||
|
Apply,
|
||||||
|
Preview,
|
||||||
|
Auto,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the one-shot provisioning flow.
|
/// Run the one-shot provisioning flow.
|
||||||
@@ -143,9 +188,8 @@ impl Context {
|
|||||||
/// Returns Ok(()) on success and also on success-noop when already provisioned.
|
/// Returns Ok(()) on success and also on success-noop when already provisioned.
|
||||||
/// Any validation or execution failure aborts with an error.
|
/// Any validation or execution failure aborts with an error.
|
||||||
pub fn run(ctx: &Context) -> Result<()> {
|
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 =
|
let selected_modes =
|
||||||
(ctx.mount_existing as u8) +
|
(ctx.mount_existing as u8) +
|
||||||
(ctx.report_current as u8) +
|
(ctx.report_current as u8) +
|
||||||
@@ -156,79 +200,81 @@ pub fn run(ctx: &Context) -> Result<()> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mode 1: Mount existing filesystems (non-destructive), based on on-disk headers.
|
let preview_requested = ctx.show || ctx.report_path_override.is_some();
|
||||||
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)?;
|
|
||||||
|
|
||||||
// Optional JSON summary for mount-existing
|
let initial_mode = if ctx.report_current {
|
||||||
if ctx.show || ctx.report_path_override.is_some() || ctx.report_current {
|
ExecutionMode::ReportCurrent
|
||||||
let now = format_rfc3339(SystemTime::now()).to_string();
|
} else if ctx.mount_existing {
|
||||||
let fs_json: Vec<serde_json::Value> = fs_results
|
ExecutionMode::MountExisting
|
||||||
.iter()
|
} else if ctx.apply {
|
||||||
.map(|r| {
|
ExecutionMode::Apply
|
||||||
let kind_str = match r.kind {
|
} else if preview_requested {
|
||||||
zfs::FsKind::Vfat => "vfat",
|
ExecutionMode::Preview
|
||||||
zfs::FsKind::Btrfs => "btrfs",
|
} else {
|
||||||
zfs::FsKind::Bcachefs => "bcachefs",
|
ExecutionMode::Auto
|
||||||
};
|
};
|
||||||
json!({
|
|
||||||
"kind": kind_str,
|
|
||||||
"uuid": r.uuid,
|
|
||||||
"label": r.label,
|
|
||||||
"devices": r.devices,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mounts_json: Vec<serde_json::Value> = mres
|
match initial_mode {
|
||||||
.iter()
|
ExecutionMode::ReportCurrent => run_report_current(ctx),
|
||||||
.map(|m| {
|
ExecutionMode::MountExisting => run_mount_existing(ctx, None, None),
|
||||||
json!({
|
ExecutionMode::Apply => run_provisioning(ctx, ProvisioningMode::Apply, None),
|
||||||
"source": m.source,
|
ExecutionMode::Preview => run_provisioning(ctx, ProvisioningMode::Preview, None),
|
||||||
"target": m.target,
|
ExecutionMode::Auto => {
|
||||||
"fstype": m.fstype,
|
let selection = auto_select_mode(ctx)?;
|
||||||
"options": m.options,
|
match selection.decision {
|
||||||
})
|
AutoDecision::MountExisting => {
|
||||||
})
|
run_mount_existing(ctx, selection.fs_results, selection.state)
|
||||||
.collect();
|
}
|
||||||
|
AutoDecision::Apply => {
|
||||||
|
run_provisioning(ctx, ProvisioningMode::Apply, selection.state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let summary = json!({
|
fn auto_select_mode(ctx: &Context) -> Result<AutoSelection> {
|
||||||
"version": "v1",
|
info!("orchestrator: auto-selecting execution mode");
|
||||||
"timestamp": now,
|
let state = idempotency::detect_existing_state()?;
|
||||||
"status": "mounted_existing",
|
let fs_results = zfs::probe_existing_filesystems()?;
|
||||||
"filesystems": fs_json,
|
|
||||||
"mounts": mounts_json,
|
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),
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(());
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mode 3: Report current initialized filesystems and mounts (non-destructive).
|
info!(
|
||||||
if ctx.report_current {
|
"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");
|
info!("orchestrator: report-current mode");
|
||||||
let fs_results = zfs::probe_existing_filesystems()?;
|
let fs_results = zfs::probe_existing_filesystems()?;
|
||||||
|
|
||||||
// Parse /proc/mounts and include only our relevant targets.
|
|
||||||
let mounts_content = fs::read_to_string("/proc/mounts").unwrap_or_default();
|
let mounts_content = fs::read_to_string("/proc/mounts").unwrap_or_default();
|
||||||
let mounts_json: Vec<serde_json::Value> = mounts_content
|
let mounts_json: Vec<serde_json::Value> = mounts_content
|
||||||
.lines()
|
.lines()
|
||||||
@@ -282,7 +328,6 @@ pub fn run(ctx: &Context) -> Result<()> {
|
|||||||
"mounts": mounts_json
|
"mounts": mounts_json
|
||||||
});
|
});
|
||||||
|
|
||||||
// In report-current mode, default to stdout; also honor --report path when provided.
|
|
||||||
println!("{}", summary);
|
println!("{}", summary);
|
||||||
if let Some(path) = &ctx.report_path_override {
|
if let Some(path) = &ctx.report_path_override {
|
||||||
fs::write(path, summary.to_string()).map_err(|e| {
|
fs::write(path, summary.to_string()).map_err(|e| {
|
||||||
@@ -290,60 +335,145 @@ pub fn run(ctx: &Context) -> Result<()> {
|
|||||||
})?;
|
})?;
|
||||||
info!("orchestrator: wrote report-current to {}", path);
|
info!("orchestrator: wrote report-current to {}", path);
|
||||||
}
|
}
|
||||||
return Ok(());
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default path: plan (and optionally apply) for empty-disk initialization workflow.
|
fn run_mount_existing(
|
||||||
|
ctx: &Context,
|
||||||
|
fs_results_override: Option<Vec<zfs::FsResult>>,
|
||||||
|
state_hint: Option<StateReport>,
|
||||||
|
) -> 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(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// 1) Idempotency pre-flight: if already provisioned, optionally emit summary then exit success.
|
let mplan = crate::mount::plan_mounts(&fs_results, &ctx.cfg)?;
|
||||||
match idempotency::detect_existing_state()? {
|
let mres = crate::mount::apply_mounts(&mplan)?;
|
||||||
Some(state) => {
|
crate::mount::maybe_write_fstab(&mres, &ctx.cfg)?;
|
||||||
info!("orchestrator: already provisioned");
|
|
||||||
if ctx.show || ctx.report_path_override.is_some() {
|
if ctx.show || ctx.report_path_override.is_some() || ctx.report_current {
|
||||||
let now = format_rfc3339(SystemTime::now()).to_string();
|
let now = format_rfc3339(SystemTime::now()).to_string();
|
||||||
let state_json = to_value(&state)
|
let fs_json: Vec<serde_json::Value> = fs_results
|
||||||
.map_err(|e| Error::Report(format!("failed to serialize StateReport: {}", e)))?;
|
.iter()
|
||||||
let summary = json!({
|
.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 mounts_json: Vec<serde_json::Value> = mres
|
||||||
|
.iter()
|
||||||
|
.map(|m| {
|
||||||
|
json!({
|
||||||
|
"source": m.source,
|
||||||
|
"target": m.target,
|
||||||
|
"fstype": m.fstype,
|
||||||
|
"options": m.options,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut summary = json!({
|
||||||
"version": "v1",
|
"version": "v1",
|
||||||
"timestamp": now,
|
"timestamp": now,
|
||||||
"status": "already_provisioned",
|
"status": "mounted_existing",
|
||||||
"state": state_json
|
"filesystems": fs_json,
|
||||||
|
"mounts": mounts_json,
|
||||||
});
|
});
|
||||||
if ctx.show {
|
|
||||||
|
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);
|
println!("{}", summary);
|
||||||
}
|
}
|
||||||
if let Some(path) = &ctx.report_path_override {
|
if let Some(path) = &ctx.report_path_override {
|
||||||
fs::write(path, summary.to_string()).map_err(|e| {
|
fs::write(path, summary.to_string()).map_err(|e| {
|
||||||
Error::Report(format!("failed to write report to {}: {}", path, e))
|
Error::Report(format!("failed to write report to {}: {}", path, e))
|
||||||
})?;
|
})?;
|
||||||
info!("orchestrator: wrote idempotency report to {}", path);
|
info!("orchestrator: wrote mount-existing report to {}", path);
|
||||||
}
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
debug!("orchestrator: not provisioned; continuing");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Device discovery using compiled filter from config.
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_provisioning(
|
||||||
|
ctx: &Context,
|
||||||
|
mode: ProvisioningMode,
|
||||||
|
state_hint: Option<StateReport>,
|
||||||
|
) -> 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 filter = build_device_filter(&ctx.cfg)?;
|
||||||
let disks = discover(&filter)?;
|
let disks = discover(&filter)?;
|
||||||
info!("orchestrator: discovered {} eligible disk(s)", disks.len());
|
info!("orchestrator: discovered {} eligible disk(s)", disks.len());
|
||||||
|
|
||||||
// 3) Emptiness enforcement: skip in preview mode (--show/--report) to allow planning output.
|
if ctx.cfg.partitioning.require_empty_disks {
|
||||||
let preview = ctx.show || ctx.report_path_override.is_some();
|
if matches!(mode, ProvisioningMode::Apply) {
|
||||||
if ctx.cfg.partitioning.require_empty_disks && !preview {
|
|
||||||
enforce_empty_disks(&disks)?;
|
enforce_empty_disks(&disks)?;
|
||||||
info!("orchestrator: all target disks verified empty");
|
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 {
|
} 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");
|
warn!("orchestrator: require_empty_disks=false; proceeding without emptiness enforcement");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) Partition planning (declarative).
|
let effective_cfg = {
|
||||||
let plan = partition::plan_partitions(&disks, &ctx.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!(
|
debug!(
|
||||||
"orchestrator: partition plan ready (alignment={} MiB, disks={})",
|
"orchestrator: partition plan ready (alignment={} MiB, disks={})",
|
||||||
plan.alignment_mib,
|
plan.alignment_mib,
|
||||||
@@ -353,8 +483,7 @@ pub fn run(ctx: &Context) -> Result<()> {
|
|||||||
debug!("plan for {}: {} part(s)", dp.disk.path, dp.parts.len());
|
debug!("plan for {}: {} part(s)", dp.disk.path, dp.parts.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply mode: perform destructive partition application now.
|
if matches!(mode, ProvisioningMode::Apply) {
|
||||||
if ctx.apply {
|
|
||||||
info!("orchestrator: apply mode enabled; applying partition plan");
|
info!("orchestrator: apply mode enabled; applying partition plan");
|
||||||
let part_results = partition::apply_partitions(&plan)?;
|
let part_results = partition::apply_partitions(&plan)?;
|
||||||
info!(
|
info!(
|
||||||
@@ -363,34 +492,28 @@ pub fn run(ctx: &Context) -> Result<()> {
|
|||||||
part_results.len()
|
part_results.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filesystem planning and creation
|
let fs_plan = zfs::plan_filesystems(&part_results, &effective_cfg)?;
|
||||||
let fs_plan = zfs::plan_filesystems(&part_results, &ctx.cfg)?;
|
|
||||||
info!(
|
info!(
|
||||||
"orchestrator: filesystem plan contains {} spec(s)",
|
"orchestrator: filesystem plan contains {} spec(s)",
|
||||||
fs_plan.specs.len()
|
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());
|
info!("orchestrator: created {} filesystem(s)", fs_results.len());
|
||||||
|
|
||||||
// Mount planning and application
|
let mplan = crate::mount::plan_mounts(&fs_results, &effective_cfg)?;
|
||||||
let mplan = crate::mount::plan_mounts(&fs_results, &ctx.cfg)?;
|
|
||||||
let mres = crate::mount::apply_mounts(&mplan)?;
|
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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preview-only path
|
|
||||||
info!("orchestrator: pre-flight complete (idempotency checked, devices discovered, plan computed)");
|
info!("orchestrator: pre-flight complete (idempotency checked, devices discovered, plan computed)");
|
||||||
|
|
||||||
// Optional: emit JSON summary via --show or write via --report
|
if preview_outputs {
|
||||||
if ctx.show || ctx.report_path_override.is_some() {
|
let summary = build_summary_json(&disks, &plan, &effective_cfg)?;
|
||||||
let summary = build_summary_json(&disks, &plan, &ctx.cfg)?;
|
|
||||||
if ctx.show {
|
if ctx.show {
|
||||||
// Print compact JSON to stdout
|
|
||||||
println!("{}", summary);
|
println!("{}", summary);
|
||||||
}
|
}
|
||||||
if let Some(path) = &ctx.report_path_override {
|
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| {
|
fs::write(path, summary.to_string()).map_err(|e| {
|
||||||
Error::Report(format!("failed to write report to {}: {}", path, 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
|
// Decide filesystem kinds and planned mountpoints (template) from plan + cfg.topology
|
||||||
let topo_str = match cfg.topology {
|
let topo_str = cfg.topology.to_string();
|
||||||
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",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Count roles across plan to infer filesystems
|
// Count roles across plan to infer filesystems
|
||||||
let mut esp_count = 0usize;
|
let mut esp_count = 0usize;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ pub struct DeviceSelection {
|
|||||||
pub min_size_gib: u64,
|
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")]
|
#[serde(rename_all = "snake_case")]
|
||||||
#[value(rename_all = "snake_case")]
|
#[value(rename_all = "snake_case")]
|
||||||
pub enum Topology {
|
pub enum Topology {
|
||||||
|
|||||||
Reference in New Issue
Block a user