feat: first-draft preview-capable zosstorage

- CLI: add topology selection (-t/--topology), preview flags (--show/--report), and removable policy override (--allow-removable) (src/cli/args.rs)
- Config: built-in sensible defaults; deterministic overlays for logging, fstab, removable, topology (src/config/loader.rs)
- Device: discovery via /proc + /sys with include/exclude regex and removable policy (src/device/discovery.rs)
- Idempotency: detection via blkid; safe emptiness checks (src/idempotency/mod.rs)
- Partition: topology-driven planning (Single, DualIndependent, BtrfsRaid1, SsdHddBcachefs) (src/partition/plan.rs)
- FS: planning + creation (mkfs.vfat, mkfs.btrfs, bcachefs format) and UUID capture via blkid (src/fs/plan.rs)
- Orchestrator: pre-flight with preview JSON (disks, partition_plan, filesystems_planned, mount scheme). Skips emptiness in preview; supports stdout+file (src/orchestrator/run.rs)
- Util/Logging/Types/Errors: process execution, tracing, shared types (src/util/mod.rs, src/logging/mod.rs, src/types.rs, src/errors.rs)
- Docs: add README with exhaustive usage and preview JSON shape (README.md)

Builds and unit tests pass: discovery, util, idempotency helpers, and fs parser tests.
This commit is contained in:
2025-09-29 11:37:07 +02:00
commit 507bc172c2
38 changed files with 6558 additions and 0 deletions

758
docs/API-SKELETONS.md Normal file
View File

@@ -0,0 +1,758 @@
# zosstorage API Skeletons (Proposed)
Purpose
- This document proposes concrete Rust module skeletons with public API surface and doc comments.
- Bodies intentionally use todo!() or are omitted for approval-first workflow.
- After approval, these will be created in the src tree in Code mode.
Index
- [src/lib.rs](src/lib.rs)
- [src/errors.rs](src/errors.rs)
- [src/main.rs](src/main.rs)
- [src/cli/args.rs](src/cli/args.rs)
- [src/logging/mod.rs](src/logging/mod.rs)
- [src/types.rs](src/types.rs)
- [src/config/loader.rs](src/config/loader.rs)
- [src/device/discovery.rs](src/device/discovery.rs)
- [src/partition/plan.rs](src/partition/plan.rs)
- [src/fs/plan.rs](src/fs/plan.rs)
- [src/mount/ops.rs](src/mount/ops.rs)
- [src/report/state.rs](src/report/state.rs)
- [src/orchestrator/run.rs](src/orchestrator/run.rs)
- [src/idempotency/mod.rs](src/idempotency/mod.rs)
- [src/util/mod.rs](src/util/mod.rs)
Conventions
- Shared [type Result<T>](src/errors.rs:1) and [enum Error](src/errors.rs:1).
- No stdout prints; use tracing only.
- External tools invoked via [util](src/util/mod.rs) wrappers.
---
## Crate root
References
- [src/lib.rs](src/lib.rs)
- [type Result<T> = std::result::Result<T, Error>](src/errors.rs:1)
Skeleton (for later implementation in code mode)
```rust
//! Crate root for zosstorage: one-shot disk provisioning utility for initramfs.
pub mod cli;
pub mod logging;
pub mod config;
pub mod device;
pub mod partition;
pub mod fs;
pub mod mount;
pub mod report;
pub mod orchestrator;
pub mod idempotency;
pub mod util;
pub mod errors;
pub use errors::{Error, Result};
/// Crate version constants.
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
```
---
## Errors
References
- [enum Error](src/errors.rs:1)
- [type Result<T>](src/errors.rs:1)
Skeleton
```rust
use thiserror::Error as ThisError;
/// Top-level error for zosstorage.
#[derive(Debug, ThisError)]
pub enum Error {
#[error("configuration error: {0}")]
Config(String),
#[error("validation error: {0}")]
Validation(String),
#[error("device discovery error: {0}")]
Device(String),
#[error("partitioning error: {0}")]
Partition(String),
#[error("filesystem error: {0}")]
Filesystem(String),
#[error("mount error: {0}")]
Mount(String),
#[error("report error: {0}")]
Report(String),
#[error("external tool '{tool}' failed with status {status}: {stderr}")]
Tool {
tool: String,
status: i32,
stderr: String,
},
#[error("unimplemented: {0}")]
Unimplemented(&'static str),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
```
---
## Entrypoint
References
- [fn main()](src/main.rs:1)
- [fn run(ctx: &Context) -> Result<()>](src/orchestrator/run.rs:1)
Skeleton
```rust
use zosstorage::{cli, config, logging, orchestrator, Result};
fn main() {
// Initialize minimal logging early (fallback).
// Proper logging config will be applied after CLI parsing.
// No stdout printing.
if let Err(e) = real_main() {
// In initramfs, emit minimal error to stderr and exit non-zero.
eprintln!("error: {e}");
std::process::exit(1);
}
}
fn real_main() -> Result<()> {
let cli = cli::from_args();
let log_opts = logging::LogOptions::from_cli(&cli);
logging::init_logging(&log_opts)?;
let cfg = config::load_and_merge(&cli)?;
config::validate(&cfg)?;
let ctx = orchestrator::Context::new(cfg, log_opts);
orchestrator::run(&ctx)
}
```
---
## CLI
References
- [struct Cli](src/cli/args.rs:1)
- [fn from_args() -> Cli](src/cli/args.rs:1)
Skeleton
```rust
use clap::{Parser, ValueEnum};
/// zosstorage - one-shot disk initializer for initramfs.
#[derive(Debug, Parser)]
#[command(name = "zosstorage", disable_help_subcommand = true)]
pub struct Cli {
/// Path to YAML configuration (mirrors kernel cmdline key 'zosstorage.config=')
#[arg(long = "config")]
pub config: Option<String>,
/// Log level: error, warn, info, debug
#[arg(long = "log-level", default_value = "info")]
pub log_level: String,
/// Also log to /run/zosstorage/zosstorage.log
#[arg(long = "log-to-file", default_value_t = false)]
pub log_to_file: bool,
/// Enable writing /etc/fstab entries
#[arg(long = "fstab", default_value_t = false)]
pub fstab: bool,
/// Present but non-functional; returns unimplemented error
#[arg(long = "force")]
pub force: bool,
}
/// Parse CLI arguments (non-interactive; suitable for initramfs).
pub fn from_args() -> Cli {
Cli::parse()
}
```
---
## Logging
References
- [struct LogOptions](src/logging/mod.rs:1)
- [fn init_logging(opts: &LogOptions) -> Result<()>](src/logging/mod.rs:1)
Skeleton
```rust
use crate::Result;
/// Logging options resolved from CLI and/or config.
#[derive(Debug, Clone)]
pub struct LogOptions {
pub level: String, // "error" | "warn" | "info" | "debug"
pub to_file: bool, // when true, logs to /run/zosstorage/zosstorage.log
}
impl LogOptions {
pub fn from_cli(cli: &crate::cli::Cli) -> Self {
Self { level: cli.log_level.clone(), to_file: cli.log_to_file }
}
}
/// Initialize tracing subscriber according to options.
/// Must be idempotent when called once in process lifetime.
pub fn init_logging(opts: &LogOptions) -> Result<()> {
todo!("set up tracing for stderr and optional file layer")
}
```
---
## Configuration types
References
- [struct Config](src/types.rs:1)
- [enum Topology](src/types.rs:1)
- [struct DeviceSelection](src/types.rs:1)
- [struct Partitioning](src/types.rs:1)
- [struct FsOptions](src/types.rs:1)
- [struct MountScheme](src/types.rs:1)
- [struct ReportOptions](src/types.rs:1)
Skeleton
```rust
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
pub level: String, // default "info"
pub to_file: bool, // default false
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceSelection {
pub include_patterns: Vec<String>,
pub exclude_patterns: Vec<String>,
pub allow_removable: bool,
pub min_size_gib: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Topology {
Single,
DualIndependent,
SsdHddBcachefs,
BtrfsRaid1,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BiosBootSpec {
pub enabled: bool,
pub size_mib: u64,
pub gpt_name: String, // "zosboot"
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EspSpec {
pub size_mib: u64,
pub label: String, // "ZOSBOOT"
pub gpt_name: String, // "zosboot"
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataSpec {
pub gpt_name: String, // "zosdata"
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheSpec {
pub gpt_name: String, // "zoscache"
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Partitioning {
pub alignment_mib: u64,
pub require_empty_disks: bool,
pub bios_boot: BiosBootSpec,
pub esp: EspSpec,
pub data: DataSpec,
pub cache: CacheSpec,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BtrfsOptions {
pub label: String, // "ZOSDATA"
pub compression: String, // "zstd:3"
pub raid_profile: String // "none" | "raid1"
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BcachefsOptions {
pub label: String, // "ZOSDATA"
pub cache_mode: String, // "promote" | "writeback?"
pub compression: String, // "zstd"
pub checksum: String, // "crc32c"
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VfatOptions {
pub label: String, // "ZOSBOOT"
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FsOptions {
pub btrfs: BtrfsOptions,
pub bcachefs: BcachefsOptions,
pub vfat: VfatOptions,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MountSchemeKind {
PerUuid,
Custom, // reserved
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MountScheme {
pub base_dir: String, // "/var/cache"
pub scheme: MountSchemeKind,
pub fstab_enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReportOptions {
pub path: String, // "/run/zosstorage/state.json"
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub version: u32,
pub logging: LoggingConfig,
pub device_selection: DeviceSelection,
pub topology: Topology,
pub partitioning: Partitioning,
pub filesystem: FsOptions,
pub mount: MountScheme,
pub report: ReportOptions,
}
```
---
## Configuration I/O
References
- [fn load_and_merge(cli: &Cli) -> Result<Config>](src/config/loader.rs:1)
- [fn validate(cfg: &Config) -> Result<()>](src/config/loader.rs:1)
Skeleton
```rust
use crate::{cli::Cli, Result};
/// Load defaults, merge file config, overlay CLI, and finally kernel cmdline.
pub fn load_and_merge(cli: &Cli) -> Result<crate::config::types::Config> {
todo!("implement precedence: file < CLI < kernel cmdline key 'zosstorage.config='")
}
/// Validate semantic correctness of the configuration.
pub fn validate(cfg: &crate::config::types::Config) -> Result<()> {
todo!("ensure device filters, sizes, topology combinations are valid")
}
```
---
## Device discovery
References
- [struct Disk](src/device/discovery.rs:1)
- [struct DeviceFilter](src/device/discovery.rs:1)
- [trait DeviceProvider](src/device/discovery.rs:1)
- [fn discover(filter: &DeviceFilter) -> Result<Vec<Disk>>](src/device/discovery.rs:1)
Skeleton
```rust
use crate::Result;
/// Eligible block device.
#[derive(Debug, Clone)]
pub struct Disk {
pub path: String,
pub size_bytes: u64,
pub rotational: bool,
pub model: Option<String>,
pub serial: Option<String>,
}
/// Compiled device filters from config patterns.
#[derive(Debug, Clone)]
pub struct DeviceFilter {
pub include: Vec<regex::Regex>,
pub exclude: Vec<regex::Regex>,
pub min_size_gib: u64,
}
/// Abstract provider for devices to enable testing with doubles.
pub trait DeviceProvider {
fn list_block_devices(&self) -> Result<Vec<Disk>>;
fn probe_properties(&self, disk: &mut Disk) -> Result<()>;
}
/// Discover eligible disks according to the filter policy.
pub fn discover(filter: &DeviceFilter) -> Result<Vec<Disk>> {
todo!("enumerate /dev, apply include/exclude, probe properties")
}
```
---
## Partitioning
References
- [enum PartRole](src/partition/plan.rs:1)
- [struct PartitionSpec](src/partition/plan.rs:1)
- [struct PartitionPlan](src/partition/plan.rs:1)
- [struct PartitionResult](src/partition/plan.rs:1)
- [fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result<PartitionPlan>](src/partition/plan.rs:1)
- [fn apply_partitions(plan: &PartitionPlan) -> Result<Vec<PartitionResult>>](src/partition/plan.rs:1)
Skeleton
```rust
use crate::{config::types::Config, device::Disk, Result};
#[derive(Debug, Clone, Copy)]
pub enum PartRole {
BiosBoot,
Esp,
Data,
Cache,
}
#[derive(Debug, Clone)]
pub struct PartitionSpec {
pub role: PartRole,
pub size_mib: Option<u64>, // None means "remainder"
pub gpt_name: String, // zosboot | zosdata | zoscache
}
#[derive(Debug, Clone)]
pub struct DiskPlan {
pub disk: Disk,
pub parts: Vec<PartitionSpec>,
}
#[derive(Debug, Clone)]
pub struct PartitionPlan {
pub alignment_mib: u64,
pub disks: Vec<DiskPlan>,
pub require_empty_disks: bool,
}
#[derive(Debug, Clone)]
pub struct PartitionResult {
pub disk: String,
pub part_number: u32,
pub role: PartRole,
pub gpt_name: String,
pub uuid: String,
pub start_mib: u64,
pub size_mib: u64,
pub device_path: String, // e.g., /dev/nvme0n1p2
}
/// Compute GPT-only plan per topology and constraints.
pub fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result<PartitionPlan> {
todo!("layout bios boot, ESP, data (and cache for SSD/HDD); align to 1 MiB")
}
/// Apply plan using sgdisk and verify via blkid; require empty disks if configured.
pub fn apply_partitions(plan: &PartitionPlan) -> Result<Vec<PartitionResult>> {
todo!("shell out to sgdisk, trigger udev settle, collect partition GUIDs")
}
```
---
## Filesystems
References
- [enum FsKind](src/fs/plan.rs:1)
- [struct FsSpec](src/fs/plan.rs:1)
- [struct FsPlan](src/fs/plan.rs:1)
- [struct FsResult](src/fs/plan.rs:1)
- [fn plan_filesystems(...)](src/fs/plan.rs:1)
- [fn make_filesystems(...)](src/fs/plan.rs:1)
Skeleton
```rust
use crate::{partition::PartitionResult, config::types::Config, Result};
#[derive(Debug, Clone, Copy)]
pub enum FsKind {
Vfat,
Btrfs,
Bcachefs,
}
#[derive(Debug, Clone)]
pub struct FsSpec {
pub kind: FsKind,
pub devices: Vec<String>, // 1 device for vfat/btrfs; 2 for bcachefs (cache + backing)
pub label: String, // "ZOSBOOT" (vfat) or "ZOSDATA" (data)
}
#[derive(Debug, Clone)]
pub struct FsPlan {
pub specs: Vec<FsSpec>,
}
#[derive(Debug, Clone)]
pub struct FsResult {
pub kind: FsKind,
pub devices: Vec<String>,
pub uuid: String,
pub label: String,
}
/// Decide which partitions get which filesystem based on topology.
pub fn plan_filesystems(
parts: &[PartitionResult],
cfg: &Config,
) -> Result<FsPlan> {
todo!("map ESP to vfat, data to btrfs or bcachefs according to topology")
}
/// Create the filesystems and return identity info (UUIDs, labels).
pub fn make_filesystems(plan: &FsPlan) -> Result<Vec<FsResult>> {
todo!("invoke mkfs tools with configured options via util::run_cmd")
}
```
---
## Mounting
References
- [struct MountPlan](src/mount/ops.rs:1)
- [struct MountResult](src/mount/ops.rs:1)
- [fn plan_mounts(...)](src/mount/ops.rs:1)
- [fn apply_mounts(...)](src/mount/ops.rs:1)
- [fn maybe_write_fstab(...)](src/mount/ops.rs:1)
Skeleton
```rust
use crate::{fs::FsResult, config::types::Config, Result};
#[derive(Debug, Clone)]
pub struct MountPlan {
pub entries: Vec<(String /* source */, String /* target */, String /* fstype */, String /* options */)>,
}
#[derive(Debug, Clone)]
pub struct MountResult {
pub source: String,
pub target: String,
pub fstype: String,
pub options: String,
}
/// Build mount plan under /var/cache/<UUID> by default.
pub fn plan_mounts(fs_results: &[FsResult], cfg: &Config) -> Result<MountPlan> {
todo!("create per-UUID directories and mount mapping")
}
/// Apply mounts using syscalls (nix), ensuring directories exist.
pub fn apply_mounts(plan: &MountPlan) -> Result<Vec<MountResult>> {
todo!("perform mount syscalls and return results")
}
/// Optionally generate /etc/fstab entries in deterministic order.
pub fn maybe_write_fstab(mounts: &[MountResult], cfg: &Config) -> Result<()> {
todo!("when enabled, write fstab entries")
}
```
---
## Reporting
References
- [const REPORT_VERSION: &str](src/report/state.rs:1)
- [struct StateReport](src/report/state.rs:1)
- [fn build_report(...)](src/report/state.rs:1)
- [fn write_report(...)](src/report/state.rs:1)
Skeleton
```rust
use serde::{Serialize, Deserialize};
use crate::{device::Disk, partition::PartitionResult, fs::FsResult, mount::MountResult, Result};
pub const REPORT_VERSION: &str = "v1";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateReport {
pub version: String,
pub timestamp: String, // RFC3339
pub status: String, // "success" | "already_provisioned" | "error"
pub disks: Vec<serde_json::Value>,
pub partitions: Vec<serde_json::Value>,
pub filesystems: Vec<serde_json::Value>,
pub mounts: Vec<serde_json::Value>,
pub error: Option<String>,
}
/// Build the machine-readable state report.
pub fn build_report(
disks: &[Disk],
parts: &[PartitionResult],
fs: &[FsResult],
mounts: &[MountResult],
status: &str,
) -> StateReport {
todo!("assemble structured report in v1 format")
}
/// Write the state report to the configured path (default /run/zosstorage/state.json).
pub fn write_report(report: &StateReport, path: &str) -> Result<()> {
todo!("serialize to JSON and persist atomically")
}
```
---
## Orchestrator
References
- [struct Context](src/orchestrator/run.rs:1)
- [fn run(ctx: &Context) -> Result<()>](src/orchestrator/run.rs:1)
Skeleton
```rust
use crate::{config::types::Config, logging::LogOptions, Result};
/// Execution context holding resolved configuration and environment flags.
#[derive(Debug, Clone)]
pub struct Context {
pub cfg: Config,
pub log: LogOptions,
}
impl Context {
pub fn new(cfg: Config, log: LogOptions) -> Self { Self { cfg, log } }
}
/// High-level one-shot flow; aborts on any validation failure.
/// Returns Ok(()) on success and also on no-op when already provisioned.
pub fn run(ctx: &Context) -> Result<()> {
todo!("idempotency check, discovery, planning, application, reporting")
}
```
---
## Idempotency
References
- [fn detect_existing_state() -> Result<Option<StateReport>>](src/idempotency/mod.rs:1)
- [fn is_empty_disk(disk: &Disk) -> Result<bool>](src/idempotency/mod.rs:1)
Skeleton
```rust
use crate::{device::Disk, report::StateReport, Result};
/// Return existing state if system is already provisioned; otherwise None.
pub fn detect_existing_state() -> Result<Option<StateReport>> {
todo!("probe GPT names (zosboot, zosdata, zoscache) and FS labels (ZOSBOOT, ZOSDATA)")
}
/// Determine if a disk is empty (no partitions and no known FS signatures).
pub fn is_empty_disk(disk: &Disk) -> Result<bool> {
todo!("use blkid and partition table inspection to declare emptiness")
}
```
---
## Utilities
References
- [struct CmdOutput](src/util/mod.rs:1)
- [fn which_tool(name: &str) -> Result<Option<String>>](src/util/mod.rs:1)
- [fn run_cmd(args: &[&str]) -> Result<()>](src/util/mod.rs:1)
- [fn run_cmd_capture(args: &[&str]) -> Result<CmdOutput>](src/util/mod.rs:1)
- [fn udev_settle(timeout_ms: u64) -> Result<()>](src/util/mod.rs:1)
Skeleton
```rust
use crate::Result;
/// Captured output from an external tool invocation.
#[derive(Debug, Clone)]
pub struct CmdOutput {
pub status: i32,
pub stdout: String,
pub stderr: String,
}
/// Locate the absolute path to required tool if available.
pub fn which_tool(name: &str) -> Result<Option<String>> {
todo!("use 'which' crate or manual PATH scanning")
}
/// Run a command and return Ok if the exit status is zero.
pub fn run_cmd(args: &[&str]) -> Result<()> {
todo!("spawn process, log stderr on failure, map to Error::Tool")
}
/// Run a command and capture stdout/stderr for parsing (e.g., blkid).
pub fn run_cmd_capture(args: &[&str]) -> Result<CmdOutput> {
todo!("spawn process and collect output")
}
/// Call udevadm settle with a timeout; warn if unavailable, then no-op.
pub fn udev_settle(timeout_ms: u64) -> Result<()> {
todo!("invoke udevadm settle when present")
}
```
---
Approval gate
- This API skeleton is ready for implementation as source files with todo!() bodies.
- Upon approval, we will:
- Create the src/ files as outlined.
- Add dependencies via cargo add.
- Ensure all modules compile with placeholders.
- Add initial tests scaffolding and example configs.
References summary
- [fn main()](src/main.rs:1)
- [fn from_args()](src/cli/args.rs:1)
- [fn init_logging(opts: &LogOptions)](src/logging/mod.rs:1)
- [fn load_and_merge(cli: &Cli)](src/config/loader.rs:1)
- [fn validate(cfg: &Config)](src/config/loader.rs:1)
- [fn discover(filter: &DeviceFilter)](src/device/discovery.rs:1)
- [fn plan_partitions(disks: &[Disk], cfg: &Config)](src/partition/plan.rs:1)
- [fn apply_partitions(plan: &PartitionPlan)](src/partition/plan.rs:1)
- [fn plan_filesystems(parts: &[PartitionResult], cfg: &Config)](src/fs/plan.rs:1)
- [fn make_filesystems(plan: &FsPlan)](src/fs/plan.rs:1)
- [fn plan_mounts(fs_results: &[FsResult], cfg: &Config)](src/mount/ops.rs:1)
- [fn apply_mounts(plan: &MountPlan)](src/mount/ops.rs:1)
- [fn maybe_write_fstab(mounts: &[MountResult], cfg: &Config)](src/mount/ops.rs:1)
- [fn build_report(...)](src/report/state.rs:1)
- [fn write_report(report: &StateReport)](src/report/state.rs:1)
- [fn detect_existing_state()](src/idempotency/mod.rs:1)
- [fn is_empty_disk(disk: &Disk)](src/idempotency/mod.rs:1)
- [fn which_tool(name: &str)](src/util/mod.rs:1)
- [fn run_cmd(args: &[&str])](src/util/mod.rs:1)
- [fn run_cmd_capture(args: &[&str])](src/util/mod.rs:1)
- [fn udev_settle(timeout_ms: u64)](src/util/mod.rs:1)