From 3c62c789e9eb3238d929471c0c8e3a9a6c216b77 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Tue, 3 Feb 2026 01:47:25 +0800 Subject: [PATCH] feat(show): unify volume status reporting with structured enum and description - Replace scattered boolean fields (`device_exists`, `needs_initialize`, `initialized`, `opened`) in JSON and table output with a single, machine-readable `status` field using the new `VolumeStatusKind` enum. - Introduce human-readable `description` field to explain current volume state. - Update table output to show only the unified `Status` column with color coding. - Restructure JSON output to wrap volumes in a top-level `volumes` array. - Add `determine_status()` method on `VolumeConfig` to encapsulate status logic. - Align `init` command to use the same status determination for validation. Signed-off-by: Kun Lai --- cryptpilot-crypt/README.md | 55 +++--- cryptpilot-crypt/README_zh.md | 55 +++--- cryptpilot-crypt/docs/quick-start.md | 14 +- cryptpilot-crypt/docs/quick-start_zh.md | 14 +- cryptpilot-crypt/src/cmd/init.rs | 27 ++- cryptpilot-crypt/src/cmd/show.rs | 225 ++++++++++++++++-------- 6 files changed, 239 insertions(+), 151 deletions(-) diff --git a/cryptpilot-crypt/README.md b/cryptpilot-crypt/README.md index f4176ac..caab07a 100644 --- a/cryptpilot-crypt/README.md +++ b/cryptpilot-crypt/README.md @@ -93,46 +93,45 @@ cryptpilot-crypt show data0 --json Example table output: ``` -╭────────┬───────────────────┬─────────────────┬──────────────┬──────────────────┬──────────────┬────────╮ -│ Volume ┆ Volume Path ┆ Underlay Device ┆ Key Provider ┆ Extra Options ┆ Initialized ┆ Opened │ -╞════════╪═══════════════════╪═════════════════╪══════════════╪══════════════════╪══════════════╪════════╡ -│ data0 ┆ /dev/mapper/data0 ┆ /dev/nvme1n1p1 ┆ otp ┆ auto_open = true ┆ Not Required ┆ True │ -│ ┆ ┆ ┆ ┆ makefs = "ext4" ┆ ┆ │ -│ ┆ ┆ ┆ ┆ integrity = true ┆ ┆ │ -╰────────┴───────────────────┴─────────────────┴──────────────┴──────────────────┴──────────────┴────────╯ +╭────────┬───────────────────┬─────────────────┬──────────────┬──────────────────┬───────────────╮ +│ Volume ┆ Volume Path ┆ Underlay Device ┆ Key Provider ┆ Extra Options ┆ Status │ +╞════════╪═══════════════════╪═════════════════╪══════════════╪══════════════════╪═══════════════╡ +│ data0 ┆ /dev/mapper/data0 ┆ /dev/nvme1n1p1 ┆ otp ┆ auto_open = true ┆ ReadyToOpen │ +│ ┆ ┆ ┆ ┆ makefs = "ext4" ┆ │ +│ ┆ ┆ ┆ ┆ integrity = true ┆ │ +╰────────┴───────────────────┴─────────────────┴──────────────┴──────────────────┴───────────────╯ ``` Example JSON output: ```json -[ - { - "volume": "data0", - "volume_path": "/dev/mapper/data0", - "underlay_device": "/dev/nvme1n1p1", - "device_exists": true, - "key_provider": "otp", - "extra_options": { - "auto_open": true, - "makefs": "ext4", - "integrity": true - }, - "needs_initialize": false, - "initialized": true, - "opened": true - } -] +{ + "volumes": [ + { + "volume": "data0", + "volume_path": "/dev/mapper/data0", + "underlay_device": "/dev/nvme1n1p1", + "key_provider": "otp", + "extra_options": { + "auto_open": true, + "makefs": "ext4", + "integrity": true + }, + "status": "ReadyToOpen", + "description": "Volume 'data0' uses otp key provider (temporary volume) and is ready to open" + } + ] +} ``` JSON output fields: +- `volumes`: Array of volume status objects - `volume`: Volume name - `volume_path`: Path to the decrypted volume (always shows the mapper path) - `underlay_device`: Underlying encrypted block device path -- `device_exists`: Whether the underlying device exists - `key_provider`: Key provider type (e.g., `otp`, `kbs`, `kms`, `oidc`, `exec`) - `extra_options`: Additional volume configuration (`null` if serialization fails) -- `needs_initialize`: Whether the volume needs initialization (false for temporary volumes like OTP, true for persistent volumes) -- `initialized`: Whether LUKS2 is initialized (false if device doesn't exist or initialization check fails, true if device exists and volume doesn't need initialization, or actual initialization status for persistent volumes) -- `opened`: Whether the volume is currently opened/decrypted +- `status`: Current status of the volume (`DeviceNotFound`, `CheckFailed`, `RequiresInit`, `ReadyToOpen`, `Opened`) +- `description`: Human-readable description of the current status ### `cryptpilot-crypt init` diff --git a/cryptpilot-crypt/README_zh.md b/cryptpilot-crypt/README_zh.md index cc29c31..bd8eec6 100644 --- a/cryptpilot-crypt/README_zh.md +++ b/cryptpilot-crypt/README_zh.md @@ -93,46 +93,45 @@ cryptpilot-crypt show data0 --json 表格输出示例: ``` -╭────────┬───────────────────┬─────────────────┬──────────────┬──────────────────┬──────────────┬────────╮ -│ Volume ┆ Volume Path ┆ Underlay Device ┆ Key Provider ┆ Extra Options ┆ Initialized ┆ Opened │ -╞════════╪═══════════════════╪═════════════════╪══════════════╪══════════════════╪══════════════╪════════╡ -│ data0 ┆ /dev/mapper/data0 ┆ /dev/nvme1n1p1 ┆ otp ┆ auto_open = true ┆ Not Required ┆ True │ -│ ┆ ┆ ┆ ┆ makefs = "ext4" ┆ ┆ │ -│ ┆ ┆ ┆ ┆ integrity = true ┆ ┆ │ -╰────────┴───────────────────┴─────────────────┴──────────────┴──────────────────┴──────────────┴────────╯ +╭────────┬───────────────────┬─────────────────┬──────────────┬──────────────────┬───────────────╮ +│ Volume ┆ Volume Path ┆ Underlay Device ┆ Key Provider ┆ Extra Options ┆ Status │ +╞════════╪═══════════════════╪═════════════════╪══════════════╪══════════════════╪═══════════════╡ +│ data0 ┆ /dev/mapper/data0 ┆ /dev/nvme1n1p1 ┆ otp ┆ auto_open = true ┆ ReadyToOpen │ +│ ┆ ┆ ┆ ┆ makefs = "ext4" ┆ │ +│ ┆ ┆ ┆ ┆ integrity = true ┆ │ +╰────────┴───────────────────┴─────────────────┴──────────────┴──────────────────┴───────────────╯ ``` JSON 输出示例: ```json -[ - { - "volume": "data0", - "volume_path": "/dev/mapper/data0", - "underlay_device": "/dev/nvme1n1p1", - "device_exists": true, - "key_provider": "otp", - "extra_options": { - "auto_open": true, - "makefs": "ext4", - "integrity": true - }, - "needs_initialize": false, - "initialized": true, - "opened": true - } -] +{ + "volumes": [ + { + "volume": "data0", + "volume_path": "/dev/mapper/data0", + "underlay_device": "/dev/nvme1n1p1", + "key_provider": "otp", + "extra_options": { + "auto_open": true, + "makefs": "ext4", + "integrity": true + }, + "status": "ReadyToOpen", + "description": "Volume 'data0' uses otp key provider (temporary volume) and is ready to open" + } + ] +} ``` JSON 输出字段说明: +- `volumes`:卷状态对象数组 - `volume`:卷名称 - `volume_path`:解密后的卷路径(始终显示 mapper 路径) - `underlay_device`:底层加密块设备路径 -- `device_exists`:底层设备是否存在 - `key_provider`:密钥提供者类型(如 `otp`、`kbs`、`kms`、`oidc`、`exec`) - `extra_options`:额外的卷配置(序列化失败时为 `null`) -- `needs_initialize`:卷是否需要初始化(临时卷如 OTP 为 false,持久化卷为 true) -- `initialized`:LUKS2 是否已初始化(设备不存在或初始化检查失败时为 false,设备存在且卷无需初始化时为 true,持久化卷为实际初始化状态) -- `opened`:卷当前是否已打开/解密 +- `status`:卷的当前状态(`DeviceNotFound`、`CheckFailed`、`RequiresInit`、`ReadyToOpen`、`Opened`) +- `description`:当前状态的人类可读描述 ### `cryptpilot-crypt init` diff --git a/cryptpilot-crypt/docs/quick-start.md b/cryptpilot-crypt/docs/quick-start.md index 7c00a37..93137eb 100644 --- a/cryptpilot-crypt/docs/quick-start.md +++ b/cryptpilot-crypt/docs/quick-start.md @@ -80,13 +80,13 @@ cryptpilot-crypt show Example output: ``` -╭────────┬───────────────────┬─────────────────┬──────────────┬──────────────────┬──────────────┬────────╮ -│ Volume ┆ Volume Path ┆ Underlay Device ┆ Key Provider ┆ Extra Options ┆ Initialized ┆ Opened │ -╞════════╪═══════════════════╪═════════════════╪══════════════╪══════════════════╪══════════════╪════════╡ -│ data0 ┆ /dev/mapper/data0 ┆ /dev/nvme1n1p1 ┆ otp ┆ auto_open = true ┆ Not Required ┆ True │ -│ ┆ ┆ ┆ ┆ makefs = "ext4" ┆ ┆ │ -│ ┆ ┆ ┆ ┆ integrity = true ┆ ┆ │ -╰────────┴───────────────────┴─────────────────┴──────────────┴──────────────────┴──────────────┴────────╯ +╭────────┬───────────────────┬─────────────────┬──────────────┬──────────────────┬───────────────╮ +│ Volume ┆ Volume Path ┆ Underlay Device ┆ Key Provider ┆ Extra Options ┆ Status │ +╞════════╪═══════════════════╪═════════════════╪══════════════╪══════════════════╪═══════════════╡ +│ data0 ┆ /dev/mapper/data0 ┆ /dev/nvme1n1p1 ┆ otp ┆ auto_open = true ┆ ReadyToOpen │ +│ ┆ ┆ ┆ ┆ makefs = "ext4" ┆ │ +│ ┆ ┆ ┆ ┆ integrity = true ┆ │ +╰────────┴───────────────────┴─────────────────┴──────────────┴──────────────────┴───────────────╯ ``` ### Step 6: Mount and Use diff --git a/cryptpilot-crypt/docs/quick-start_zh.md b/cryptpilot-crypt/docs/quick-start_zh.md index 61381fa..707e203 100644 --- a/cryptpilot-crypt/docs/quick-start_zh.md +++ b/cryptpilot-crypt/docs/quick-start_zh.md @@ -88,13 +88,13 @@ cryptpilot-crypt show 示例输出: ``` -╭────────┬───────────────────┬─────────────────┬──────────────┬──────────────────┬──────────────┬────────╮ -│ Volume ┆ Volume Path ┆ Underlay Device ┆ Key Provider ┆ Extra Options ┆ Initialized ┆ Opened │ -╞════════╪═══════════════════╪═════════════════╪══════════════╪══════════════════╪══════════════╪════════╡ -│ data0 ┆ /dev/mapper/data0 ┆ /dev/nvme1n1p1 ┆ otp ┆ auto_open = true ┆ Not Required ┆ True │ -│ ┆ ┆ ┆ ┆ makefs = "ext4" ┆ ┆ │ -│ ┆ ┆ ┆ ┆ integrity = true ┆ ┆ │ -╰────────┴───────────────────┴─────────────────┴──────────────┴──────────────────┴──────────────┴────────╯ +╭────────┬───────────────────┬─────────────────┬──────────────┬──────────────────┬───────────────╮ +│ Volume ┆ Volume Path ┆ Underlay Device ┆ Key Provider ┆ Extra Options ┆ Status │ +╞════════╪═══════════════════╪═════════════════╪══════════════╪══════════════════╪═══════════════╡ +│ data0 ┆ /dev/mapper/data0 ┆ /dev/nvme1n1p1 ┆ otp ┆ auto_open = true ┆ ReadyToOpen │ +│ ┆ ┆ ┆ ┆ makefs = "ext4" ┆ │ +│ ┆ ┆ ┆ ┆ integrity = true ┆ │ +╰────────┴───────────────────┴─────────────────┴──────────────┴──────────────────┴───────────────╯ ``` ### 步骤 7:挂载并使用 diff --git a/cryptpilot-crypt/src/cmd/init.rs b/cryptpilot-crypt/src/cmd/init.rs index d16928d..94fb6ed 100644 --- a/cryptpilot-crypt/src/cmd/init.rs +++ b/cryptpilot-crypt/src/cmd/init.rs @@ -2,7 +2,7 @@ use anyhow::{bail, Context, Result}; use async_trait::async_trait; use dialoguer::{console::Term, Confirm}; -use crate::cli::InitOptions; +use crate::{cli::InitOptions, cmd::show::VolumeStatusKind}; use cryptpilot::{ fs::luks2::TempLuksVolume, provider::{IntoProvider, KeyProvider}, @@ -54,12 +54,27 @@ async fn persistent_disk_init( volume_config: &VolumeConfig, key_provider: &impl KeyProvider, ) -> Result<()> { - if cryptpilot::fs::luks2::is_initialized(&volume_config.dev).await? - && !init_options.force_reinit - { - bail!("The device {:?} is already initialized. Use '--force-reinit' to force re-initialize the volume.", volume_config.dev); + let status = volume_config.determine_status().await; + match status.kind { + VolumeStatusKind::DeviceNotFound + | VolumeStatusKind::CheckFailed + | VolumeStatusKind::Opened => { + bail!( + "The status of device {:?} is incorrect: {:?}({})", + volume_config.dev, + status.kind, + status.description + ); + } + VolumeStatusKind::RequiresInit => { + // This is expected, continue with initialization + } + VolumeStatusKind::ReadyToOpen => { + if !init_options.force_reinit { + bail!("The device {:?} is already initialized. Use '--force-reinit' to force re-initialize the volume.", volume_config.dev); + } + } } - if !init_options.yes { if !Term::stderr().is_term() { bail!("Standard error is not a terminal. Please use '--yes' to confirm the operation in non-interactive mode."); diff --git a/cryptpilot-crypt/src/cmd/show.rs b/cryptpilot-crypt/src/cmd/show.rs index 58961f6..c8925a8 100644 --- a/cryptpilot-crypt/src/cmd/show.rs +++ b/cryptpilot-crypt/src/cmd/show.rs @@ -12,6 +12,31 @@ use cryptpilot::provider::{IntoProvider, KeyProvider as _, VolumeType}; use crate::config::VolumeConfig; +/// Unified volume status enumeration +#[derive(Serialize, Clone, Debug, PartialEq)] +pub enum VolumeStatusKind { + /// Device does not exist physically + DeviceNotFound, + /// Device exists but initialization check failed (with error details) + CheckFailed, + /// Device requires initialization + RequiresInit, + /// Volume is ready to open (either initialized persistent volume or temporary volume) + ReadyToOpen, + /// Volume is currently opened/mapped + Opened, +} + +/// Detailed status information with human-readable description +#[derive(Serialize, Clone, Debug)] +pub struct VolumeStatus { + /// Machine-readable status code + #[serde(rename = "status")] + pub kind: VolumeStatusKind, + /// Human-readable detailed description + pub description: String, +} + pub struct ShowCommand { #[allow(dead_code)] pub show_options: ShowOptions, @@ -50,27 +75,29 @@ pub trait PrintAsJson { async fn print_as_json(&self) -> Result<()>; } -/// JSON serializable structure for volume status +/// Individual volume status information #[derive(Serialize)] -struct VolumeStatus { +pub struct ShowVolume { volume: String, volume_path: PathBuf, underlay_device: PathBuf, - device_exists: bool, key_provider: String, key_provider_options: serde_json::Value, extra_options: serde_json::Value, - needs_initialize: bool, - initialized: bool, - opened: bool, + /// Unified status representation (flattened) + #[serde(flatten)] + status: VolumeStatus, } -impl VolumeStatus { - /// Build volume status from config - async fn from_config(volume_config: &VolumeConfig) -> Result { - let dev_exist = Path::new(&volume_config.dev).exists(); - let is_open = cryptpilot::fs::luks2::is_active(&volume_config.volume); +/// Collection of volume statuses for JSON output +#[derive(Serialize)] +struct VolumesCollection { + volumes: Vec, +} +impl ShowVolume { + /// Build volume status from config + async fn from_config(volume_config: &VolumeConfig) -> Self { let volume_path = volume_config.volume_path(); let key_provider = serde_variant::to_variant_name(&volume_config.encrypt.key_provider) @@ -85,42 +112,18 @@ impl VolumeStatus { Err(_) => serde_json::Value::Null, }; - let needs_initialize = matches!( - volume_config - .encrypt - .key_provider - .clone() - .into_provider() - .volume_type(), - VolumeType::Persistent - ); - - let initialized = if !dev_exist { - false - } else if !needs_initialize { - true - } else { - match cryptpilot::fs::luks2::is_initialized(&volume_config.dev).await { - Ok(initialized) => initialized, - Err(e) => { - tracing::warn!("Failed to check initialization status: {e:?}"); - false - } - } - }; + // Determine unified status using VolumeConfig method + let status = volume_config.determine_status().await; - Ok(Self { + Self { volume: volume_config.volume.clone(), volume_path, underlay_device: volume_config.dev.clone(), - device_exists: dev_exist, key_provider, key_provider_options, extra_options, - needs_initialize, - initialized, - opened: is_open, - }) + status, + } } } @@ -145,32 +148,43 @@ impl PrintAsTable for [VolumeConfig] { "Underlay Device", "Key Provider", "Extra Options", - "Initialized", - "Opened", + "Status", ]); for volume_config in self { - let status = VolumeStatus::from_config(volume_config).await?; + let show_volume = ShowVolume::from_config(volume_config).await; + + // Determine color based on status code + let status_color = match show_volume.status.kind { + VolumeStatusKind::Opened => Color::Green, + VolumeStatusKind::ReadyToOpen => Color::Green, + VolumeStatusKind::RequiresInit => Color::Yellow, + VolumeStatusKind::CheckFailed => Color::Red, + VolumeStatusKind::DeviceNotFound => Color::Red, + }; table.add_row(vec![ - Cell::new(&status.volume), - if !status.device_exists { - Cell::new("N/A").fg(Color::Yellow) - } else if status.opened { - Cell::new(status.volume_path.to_string_lossy()).fg(Color::Green) - } else { - Cell::new("").fg(Color::Yellow) + Cell::new(&show_volume.volume), + match show_volume.status.kind { + VolumeStatusKind::DeviceNotFound => Cell::new("N/A").fg(Color::Yellow), + VolumeStatusKind::Opened => { + Cell::new(show_volume.volume_path.to_string_lossy().as_ref()) + .fg(Color::Green) + } + _ => Cell::new("").fg(Color::Yellow), }, - if status.device_exists { - Cell::new(status.underlay_device.to_string_lossy()) - } else { - tracing::warn!("Device {:?} does not exist", status.underlay_device); - Cell::new(format!("{:?} ", status.underlay_device)).fg(Color::Red) + match show_volume.status.kind { + VolumeStatusKind::DeviceNotFound => { + tracing::warn!("Device {:?} does not exist", show_volume.underlay_device); + Cell::new(format!("{:?} ", show_volume.underlay_device)) + .fg(Color::Red) + } + _ => Cell::new(show_volume.underlay_device.to_string_lossy().as_ref()), }, - Cell::new(&status.key_provider), + Cell::new(&show_volume.key_provider), { - if status.extra_options.is_null() - || status.extra_options == serde_json::json!({}) + if show_volume.extra_options.is_null() + || show_volume.extra_options == serde_json::json!({}) { Cell::new("").fg(Color::DarkGrey) } else { @@ -178,20 +192,7 @@ impl PrintAsTable for [VolumeConfig] { Cell::new(s) } }, - if !status.needs_initialize { - Cell::new("Not Required").fg(Color::Yellow) - } else if !status.device_exists { - Cell::new("N/A").fg(Color::Yellow) - } else if status.initialized { - Cell::new("True").fg(Color::Green) - } else { - Cell::new("False").fg(Color::Yellow) - }, - if status.opened { - Cell::new("True").fg(Color::Green) - } else { - Cell::new("False").fg(Color::Yellow) - }, + Cell::new(format!("{:?}", show_volume.status.kind)).fg(status_color), ]); } @@ -211,15 +212,89 @@ impl PrintAsJson for VolumeConfig { #[async_trait] impl PrintAsJson for [VolumeConfig] { async fn print_as_json(&self) -> Result<()> { - let mut statuses = Vec::new(); + let mut volumes = Vec::new(); for volume_config in self { - statuses.push(VolumeStatus::from_config(volume_config).await?); + volumes.push(ShowVolume::from_config(volume_config).await); } - let json = serde_json::to_string_pretty(&statuses)?; + let volumes_collection = VolumesCollection { volumes }; + let json = serde_json::to_string_pretty(&volumes_collection)?; println!("{}", json); Ok(()) } } + +// Implementation for VolumeConfig to determine status +impl VolumeConfig { + /// Determine unified volume status with detailed description + pub async fn determine_status(&self) -> VolumeStatus { + // Check if volume is already opened + let is_open = cryptpilot::fs::luks2::is_active(&self.volume); + if is_open { + return VolumeStatus { + kind: VolumeStatusKind::Opened, + description: format!( + "Volume '{}' is currently opened and mapped at '{}'", + self.volume, + self.volume_path().display() + ), + }; + } + + // Check if device exists + let dev_exist = Path::new(&self.dev).exists(); + if !dev_exist { + return VolumeStatus { + kind: VolumeStatusKind::DeviceNotFound, + description: format!("Device '{:?}' does not exist on filesystem", self.dev), + }; + } + + // Check volume type + let key_provider = self.encrypt.key_provider.clone().into_provider(); + let is_persistent = matches!(key_provider.volume_type(), VolumeType::Persistent); + + // For temporary volumes, they are ready to open without initialization + if !is_persistent { + return VolumeStatus { + kind: VolumeStatusKind::ReadyToOpen, + description: format!( + "Volume '{}' uses {} key provider (temporary volume) and is ready to open", + self.volume, + serde_variant::to_variant_name(&self.encrypt.key_provider).unwrap_or("unknown") + ), + }; + } + + // For persistent volumes, check initialization status + match cryptpilot::fs::luks2::is_initialized(&self.dev).await { + Ok(true) => VolumeStatus { + kind: VolumeStatusKind::ReadyToOpen, + description: format!( + "Device '{:?}' is properly initialized as LUKS2 volume and ready to open", + self.dev + ), + }, + Ok(false) => VolumeStatus { + kind: VolumeStatusKind::RequiresInit, + description: format!( + "Device '{:?}' exists but is not a valid LUKS2 volume - needs initialization", + self.dev + ), + }, + Err(e) => { + // This is the critical case - check failed + let error_msg = format!("{:?}", e); + VolumeStatus { + kind: VolumeStatusKind::CheckFailed, + description: format!( + "Failed to check initialization status for device '{:?}': {}", + self.dev, error_msg + ), + } + } + } + } +}