apt-ostree/.notes/rpm-ostree/how-commands-work/01-status-command.md

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 options
  • src/system.rs - Enhance status method
  • src/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 logic
  • src/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 logic
  • src/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 logic
  • src/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