- 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.
20 KiB
20 KiB
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/errors.rs
- src/main.rs
- src/cli/args.rs
- src/logging/mod.rs
- src/types.rs
- src/config/loader.rs
- src/device/discovery.rs
- src/partition/plan.rs
- src/fs/plan.rs
- src/mount/ops.rs
- src/report/state.rs
- src/orchestrator/run.rs
- src/idempotency/mod.rs
- src/util/mod.rs
Conventions
- Shared type Result and enum Error.
- No stdout prints; use tracing only.
- External tools invoked via util wrappers.
Crate root
References
Skeleton (for later implementation in code mode)
//! 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
Skeleton
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
Skeleton
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
Skeleton
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
Skeleton
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
- enum Topology
- struct DeviceSelection
- struct Partitioning
- struct FsOptions
- struct MountScheme
- struct ReportOptions
Skeleton
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
Skeleton
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
- struct DeviceFilter
- trait DeviceProvider
- fn discover(filter: &DeviceFilter) -> Result<Vec>
Skeleton
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
- struct PartitionSpec
- struct PartitionPlan
- struct PartitionResult
- fn plan_partitions(disks: &[Disk], cfg: &Config) -> Result
- fn apply_partitions(plan: &PartitionPlan) -> Result<Vec>
Skeleton
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
- struct FsSpec
- struct FsPlan
- struct FsResult
- fn plan_filesystems(...)
- fn make_filesystems(...)
Skeleton
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
- struct MountResult
- fn plan_mounts(...)
- fn apply_mounts(...)
- fn maybe_write_fstab(...)
Skeleton
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
Skeleton
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
Skeleton
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
Skeleton
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
- fn which_tool(name: &str) -> Result<Option>
- fn run_cmd(args: &[&str]) -> Result<()>
- fn run_cmd_capture(args: &[&str]) -> Result
- fn udev_settle(timeout_ms: u64) -> Result<()>
Skeleton
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()
- fn from_args()
- fn init_logging(opts: &LogOptions)
- fn load_and_merge(cli: &Cli)
- fn validate(cfg: &Config)
- fn discover(filter: &DeviceFilter)
- fn plan_partitions(disks: &[Disk], cfg: &Config)
- fn apply_partitions(plan: &PartitionPlan)
- fn plan_filesystems(parts: &[PartitionResult], cfg: &Config)
- fn make_filesystems(plan: &FsPlan)
- fn plan_mounts(fs_results: &[FsResult], cfg: &Config)
- fn apply_mounts(plan: &MountPlan)
- fn maybe_write_fstab(mounts: &[MountResult], cfg: &Config)
- fn build_report(...)
- fn write_report(report: &StateReport)
- fn detect_existing_state()
- fn is_empty_disk(disk: &Disk)
- fn which_tool(name: &str)
- fn run_cmd(args: &[&str])
- fn run_cmd_capture(args: &[&str])
- fn udev_settle(timeout_ms: u64)