15 KiB
15 KiB
Status Command Implementation Guide
Overview
The status command is the highest complexity command (1506 lines in rpm-ostree) and provides rich system status information with multiple output formats.
Current Implementation Status
- ✅ Basic status command exists in apt-ostree
- ❌ Missing rich formatting, JSON output, advisory expansion
- ❌ Missing deployment state analysis and tree structures
Implementation Requirements
Phase 1: Option Parsing and D-Bus Data Collection
Files to Modify:
src/main.rs- Add status command optionssrc/system.rs- Enhance status methodsrc/daemon.rs- Add deployment data collection
Implementation Steps:
1.1 Update CLI Options (src/main.rs)
// Add to status command options
#[derive(Debug, Parser)]
pub struct StatusOpts {
/// Output JSON format
#[arg(long)]
json: bool,
/// Filter JSONPath expression
#[arg(short = 'J', long)]
jsonpath: Option<String>,
/// Print additional fields (implies -a)
#[arg(short = 'v', long)]
verbose: bool,
/// Expand advisories listing
#[arg(short = 'a', long)]
advisories: bool,
/// Only print the booted deployment
#[arg(short = 'b', long)]
booted: bool,
/// If pending deployment available, exit 77
#[arg(long)]
pending_exit_77: bool,
}
1.2 Enhance D-Bus Interface (src/daemon.rs)
// Add to D-Bus interface
#[dbus_interface(name = "org.aptostree.dev")]
impl AptOstreeDaemon {
/// Get all deployments
async fn get_deployments(&self) -> Result<Vec<DeploymentInfo>, Box<dyn std::error::Error>> {
// Implementation here
}
/// Get booted deployment
async fn get_booted_deployment(&self) -> Result<Option<DeploymentInfo>, Box<dyn std::error::Error>> {
// Implementation here
}
/// Get pending deployment
async fn get_pending_deployment(&self) -> Result<Option<DeploymentInfo>, Box<dyn std::error::Error>> {
// Implementation here
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DeploymentInfo {
pub checksum: String,
pub version: String,
pub origin: String,
pub timestamp: u64,
pub packages: Vec<String>,
pub advisories: Vec<AdvisoryInfo>,
}
1.3 Implement Deployment Data Collection (src/system.rs)
impl AptOstreeSystem {
pub async fn get_deployments(&self) -> Result<Vec<DeploymentInfo>, Box<dyn std::error::Error>> {
// 1. Load OSTree sysroot
let sysroot = ostree::Sysroot::new_default();
sysroot.load(None)?;
// 2. Get all deployments
let deployments = sysroot.get_deployments();
// 3. Convert to DeploymentInfo
let mut result = Vec::new();
for deployment in deployments {
let checksum = deployment.get_csum().to_string();
let version = deployment.get_version().unwrap_or("").to_string();
let origin = deployment.get_origin().unwrap_or("").to_string();
// 4. Get deployment metadata
let repo = sysroot.get_repo(None)?;
let commit = repo.load_commit(&checksum, None)?;
let timestamp = commit.get_timestamp();
// 5. Get package list from commit
let packages = self.get_packages_from_commit(&checksum).await?;
// 6. Get advisory information
let advisories = self.get_advisories_for_deployment(&checksum).await?;
result.push(DeploymentInfo {
checksum,
version,
origin,
timestamp,
packages,
advisories,
});
}
Ok(result)
}
}
Phase 2: Deployment Data Processing
Files to Modify:
src/system.rs- Add deployment processing logicsrc/apt.rs- Add package and advisory extraction
Implementation Steps:
2.1 Package Extraction from Commits (src/apt.rs)
impl AptManager {
pub async fn get_packages_from_commit(&self, commit_checksum: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
// 1. Get commit filesystem
let repo = ostree::Repo::open_at(libc::AT_FDCWD, "/ostree/repo", None)?;
let commit = repo.load_commit(commit_checksum, None)?;
// 2. Checkout commit to temporary directory
let temp_dir = tempfile::tempdir()?;
repo.checkout_tree(ostree::ObjectType::Dir, commit_checksum, temp_dir.path(), None)?;
// 3. Load APT database from checkout
let status_file = temp_dir.path().join("var/lib/dpkg/status");
if status_file.exists() {
let packages = self.parse_dpkg_status(&status_file).await?;
Ok(packages)
} else {
Ok(Vec::new())
}
}
async fn parse_dpkg_status(&self, status_file: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let content = tokio::fs::read_to_string(status_file).await?;
let mut packages = Vec::new();
for paragraph in content.split("\n\n") {
if let Some(package) = self.extract_package_name(paragraph) {
packages.push(package);
}
}
Ok(packages)
}
}
2.2 Advisory Information Extraction (src/apt.rs)
impl AptManager {
pub async fn get_advisories_for_deployment(&self, commit_checksum: &str) -> Result<Vec<AdvisoryInfo>, Box<dyn std::error::Error>> {
// 1. Get packages from commit
let packages = self.get_packages_from_commit(commit_checksum).await?;
// 2. Check for security advisories
let mut advisories = Vec::new();
for package in packages {
if let Some(advisory) = self.get_package_advisory(&package).await? {
advisories.push(advisory);
}
}
Ok(advisories)
}
async fn get_package_advisory(&self, package: &str) -> Result<Option<AdvisoryInfo>, Box<dyn std::error::Error>> {
// Use APT to check for security advisories
// This would integrate with Debian/Ubuntu security databases
// For now, return None
Ok(None)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AdvisoryInfo {
pub id: String,
pub severity: String,
pub description: String,
pub affected_packages: Vec<String>,
}
Phase 3: Rich Output Formatting
Files to Modify:
src/main.rs- Add output formatting logicsrc/formatting.rs- New file for formatting utilities
Implementation Steps:
3.1 Create Formatting Module (src/formatting.rs)
use serde_json::Value;
use std::collections::HashMap;
pub struct StatusFormatter {
max_key_len: usize,
columns: usize,
}
impl StatusFormatter {
pub fn new() -> Self {
let columns = term_size::dimensions().map(|(w, _)| w).unwrap_or(80);
Self {
max_key_len: 0,
columns,
}
}
pub fn format_deployments(&self, deployments: &[DeploymentInfo], opts: &StatusOpts) -> String {
if opts.json {
self.format_json(deployments, opts)
} else {
self.format_text(deployments, opts)
}
}
fn format_json(&self, deployments: &[DeploymentInfo], opts: &StatusOpts) -> String {
let mut json = serde_json::Map::new();
// Add deployments array
let deployments_json: Vec<Value> = deployments
.iter()
.map(|d| serde_json::to_value(d).unwrap())
.collect();
json.insert("deployments".to_string(), Value::Array(deployments_json));
// Add booted deployment
if let Some(booted) = deployments.iter().find(|d| d.is_booted) {
json.insert("booted".to_string(), serde_json::to_value(booted).unwrap());
}
// Add pending deployment
if let Some(pending) = deployments.iter().find(|d| d.is_pending) {
json.insert("pending".to_string(), serde_json::to_value(pending).unwrap());
}
// Apply JSONPath filter if specified
if let Some(ref jsonpath) = opts.jsonpath {
self.apply_jsonpath_filter(&mut json, jsonpath);
}
serde_json::to_string_pretty(&Value::Object(json)).unwrap()
}
fn format_text(&self, deployments: &[DeploymentInfo], opts: &StatusOpts) -> String {
let mut output = String::new();
for (i, deployment) in deployments.iter().enumerate() {
// Add deployment header
output.push_str(&format!("Deployment {}:\n", i));
// Add basic info
output.push_str(&format!(" Checksum: {}\n", deployment.checksum));
output.push_str(&format!(" Version: {}\n", deployment.version));
output.push_str(&format!(" Origin: {}\n", deployment.origin));
// Add state indicators
if deployment.is_booted {
output.push_str(" State: booted\n");
} else if deployment.is_pending {
output.push_str(" State: pending\n");
}
// Add verbose info if requested
if opts.verbose {
output.push_str(&format!(" Timestamp: {}\n", deployment.timestamp));
output.push_str(&format!(" Packages: {}\n", deployment.packages.len()));
}
// Add advisory information if requested
if opts.advisories && !deployment.advisories.is_empty() {
output.push_str(" Advisories:\n");
for advisory in &deployment.advisories {
output.push_str(&format!(" {}: {}\n", advisory.id, advisory.severity));
}
}
output.push('\n');
}
output
}
fn apply_jsonpath_filter(&self, json: &mut serde_json::Map<String, Value>, jsonpath: &str) {
// Implement JSONPath filtering
// This would use a JSONPath library like jsonpath-rust
}
}
3.2 Update Main Status Command (src/main.rs)
async fn status_command(opts: StatusOpts) -> Result<(), Box<dyn std::error::Error>> {
// 1. Get deployment data
let system = AptOstreeSystem::new().await?;
let deployments = system.get_deployments().await?;
let booted = system.get_booted_deployment().await?;
let pending = system.get_pending_deployment().await?;
// 2. Mark deployment states
let mut deployments_with_state = deployments;
for deployment in &mut deployments_with_state {
deployment.is_booted = booted.as_ref().map(|b| b.checksum == deployment.checksum).unwrap_or(false);
deployment.is_pending = pending.as_ref().map(|p| p.checksum == deployment.checksum).unwrap_or(false);
}
// 3. Filter if booted-only requested
let deployments_to_show = if opts.booted {
deployments_with_state.into_iter().filter(|d| d.is_booted).collect()
} else {
deployments_with_state
};
// 4. Format and display
let formatter = StatusFormatter::new();
let output = formatter.format_deployments(&deployments_to_show, &opts);
println!("{}", output);
// 5. Handle pending exit 77
if opts.pending_exit_77 && pending.is_some() {
std::process::exit(77);
}
Ok(())
}
Phase 4: Special Case Handling
Files to Modify:
src/main.rs- Add special case logicsrc/error.rs- Add error handling
Implementation Steps:
4.1 Add Error Handling (src/error.rs)
#[derive(Debug, thiserror::Error)]
pub enum StatusError {
#[error("Failed to load OSTree sysroot: {0}")]
SysrootError(#[from] ostree::Error),
#[error("Failed to parse deployment data: {0}")]
ParseError(String),
#[error("Failed to format output: {0}")]
FormatError(String),
}
impl From<StatusError> for Box<dyn std::error::Error> {
fn from(err: StatusError) -> Self {
Box::new(err)
}
}
4.2 Add Special Case Logic (src/main.rs)
async fn status_command(opts: StatusOpts) -> Result<(), Box<dyn std::error::Error>> {
// ... existing code ...
// Handle empty deployments
if deployments_to_show.is_empty() {
if opts.json {
println!("{{\"deployments\": []}}");
} else {
println!("No deployments found");
}
return Ok(());
}
// Handle single deployment with booted-only
if opts.booted && deployments_to_show.len() == 1 {
// Special formatting for single booted deployment
}
// ... rest of implementation ...
}
Testing Strategy
Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_get_deployments() {
let system = AptOstreeSystem::new().await.unwrap();
let deployments = system.get_deployments().await.unwrap();
assert!(!deployments.is_empty());
}
#[test]
fn test_json_formatting() {
let formatter = StatusFormatter::new();
let deployments = vec![
DeploymentInfo {
checksum: "test123".to_string(),
version: "1.0".to_string(),
origin: "test".to_string(),
timestamp: 1234567890,
packages: vec!["package1".to_string()],
advisories: vec![],
is_booted: true,
is_pending: false,
}
];
let opts = StatusOpts {
json: true,
jsonpath: None,
verbose: false,
advisories: false,
booted: false,
pending_exit_77: false,
};
let output = formatter.format_deployments(&deployments, &opts);
assert!(output.contains("test123"));
assert!(output.contains("booted"));
}
}
Integration Tests
#[tokio::test]
async fn test_status_command_integration() {
// Test full status command with real OSTree repository
let opts = StatusOpts {
json: false,
jsonpath: None,
verbose: true,
advisories: true,
booted: false,
pending_exit_77: false,
};
let result = status_command(opts).await;
assert!(result.is_ok());
}
Dependencies to Add
Add to Cargo.toml:
[dependencies]
serde_json = "1.0"
term_size = "0.3"
jsonpath-rust = "0.1" # For JSONPath filtering
tempfile = "3.0" # For temporary directories
Implementation Checklist
- Add CLI options for JSON output, verbose mode, advisory expansion
- Implement D-Bus methods for deployment data collection
- Add package extraction from OSTree commits
- Implement advisory information extraction
- Create rich text formatting with tree structures
- Implement JSON output with filtering
- Add special case handling (pending exit 77, booted-only)
- Add comprehensive error handling
- Write unit and integration tests
- Update documentation
References
- rpm-ostree source:
src/app/rpmostree-builtin-status.cxx(1506 lines) - OSTree API documentation
- APT package database format
- Debian/Ubuntu security advisory format