apt-ostree/.notes/rpm-ostree/how-commands-work/07-kargs-command.md

24 KiB

Kargs Command Implementation Guide

Overview

The kargs command is medium complexity (376 lines in rpm-ostree) and handles kernel argument management with interactive editor mode, multiple modification modes, and boot configuration updates.

Current Implementation Status

  • Kargs command does not exist in apt-ostree
  • Missing kernel argument parsing and validation
  • Missing interactive editor integration
  • Missing boot configuration updates

Implementation Requirements

Phase 1: Option Parsing and Mode Determination

Files to Modify:

  • src/main.rs - Add kargs command options
  • src/kargs.rs - New file for kernel argument management
  • src/daemon.rs - Add kargs D-Bus method

Implementation Steps:

1.1 Add Kargs Command Structure (src/main.rs)

#[derive(Debug, Parser)]
pub struct KargsOpts {
    /// Kernel arguments to append
    #[arg(short = 'a', long)]
    append: Vec<String>,
    
    /// Kernel arguments to prepend
    #[arg(short = 'p', long)]
    prepend: Vec<String>,
    
    /// Kernel arguments to delete
    #[arg(short = 'd', long)]
    delete: Vec<String>,
    
    /// Kernel arguments to replace
    #[arg(short = 'r', long)]
    replace: Vec<String>,
    
    /// Edit kernel arguments in an editor
    #[arg(short = 'e', long)]
    editor: bool,
    
    /// Initiate a reboot after operation is complete
    #[arg(short = 'r', long)]
    reboot: bool,
    
    /// Exit after printing the transaction
    #[arg(short = 'n', long)]
    dry_run: bool,
    
    /// Operate on provided STATEROOT
    #[arg(long)]
    stateroot: Option<String>,
    
    /// Use system root SYSROOT (default: /)
    #[arg(long)]
    sysroot: Option<String>,
    
    /// Force a peer-to-peer connection instead of using the system message bus
    #[arg(long)]
    peer: bool,
    
    /// Avoid printing most informational messages
    #[arg(short = 'q', long)]
    quiet: bool,
    
    /// Output JSON format
    #[arg(long)]
    json: bool,
}

1.2 Add Option Validation (src/main.rs)

impl KargsOpts {
    pub fn validate(&self) -> Result<(), Box<dyn std::error::Error>> {
        // Check for valid stateroot if provided
        if let Some(ref stateroot) = self.stateroot {
            if !Path::new(stateroot).exists() {
                return Err(format!("Stateroot '{}' does not exist", stateroot).into());
            }
        }
        
        // Check for valid sysroot if provided
        if let Some(ref sysroot) = self.sysroot {
            if !Path::new(sysroot).exists() {
                return Err(format!("Sysroot '{}' does not exist", sysroot).into());
            }
        }
        
        // Validate kernel argument format
        for arg in &self.append {
            self.validate_kernel_arg(arg)?;
        }
        
        for arg in &self.prepend {
            self.validate_kernel_arg(arg)?;
        }
        
        for arg in &self.delete {
            self.validate_kernel_arg(arg)?;
        }
        
        for arg in &self.replace {
            self.validate_kernel_arg(arg)?;
        }
        
        // Check that at least one operation is specified
        if self.append.is_empty() && self.prepend.is_empty() && 
           self.delete.is_empty() && self.replace.is_empty() && !self.editor {
            return Err("No kernel argument operation specified".into());
        }
        
        Ok(())
    }
    
    fn validate_kernel_arg(&self, arg: &str) -> Result<(), Box<dyn std::error::Error>> {
        // Basic validation: no spaces in argument names
        if arg.contains(' ') && !arg.contains('=') {
            return Err(format!("Invalid kernel argument format: {}", arg).into());
        }
        
        // Check for dangerous arguments
        let dangerous_args = ["init=", "root=", "ro", "rw"];
        for dangerous in &dangerous_args {
            if arg.starts_with(dangerous) {
                return Err(format!("Dangerous kernel argument not allowed: {}", arg).into());
            }
        }
        
        Ok(())
    }
}

Phase 2: Kernel Argument Management

Files to Modify:

  • src/kargs.rs - Add kernel argument management logic
  • src/boot.rs - New file for boot configuration management

Implementation Steps:

2.1 Add Kernel Argument Management (src/kargs.rs)

use std::collections::HashMap;
use std::process::Command;

pub struct KargsManager {
    sysroot_path: String,
}

impl KargsManager {
    pub fn new(sysroot_path: Option<&str>) -> Self {
        Self {
            sysroot_path: sysroot_path.unwrap_or("/").to_string(),
        }
    }
    
    pub async fn get_current_kargs(&self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
        // Read current kernel arguments from /proc/cmdline
        let cmdline = tokio::fs::read_to_string("/proc/cmdline").await?;
        let args: Vec<String> = cmdline
            .split_whitespace()
            .map(|s| s.to_string())
            .collect();
        
        Ok(args)
    }
    
    pub async fn get_deployment_kargs(&self, deployment: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
        // Get kernel arguments for a specific deployment
        let sysroot = ostree::Sysroot::new_at(libc::AT_FDCWD, &self.sysroot_path);
        sysroot.load(None)?;
        
        let deployments = sysroot.get_deployments();
        for deployment_obj in deployments {
            if deployment_obj.get_csum() == deployment {
                return self.get_deployment_kernel_args(deployment_obj).await;
            }
        }
        
        Err("Deployment not found".into())
    }
    
    async fn get_deployment_kernel_args(&self, deployment: &ostree::Deployment) -> Result<Vec<String>, Box<dyn std::error::Error>> {
        // Extract kernel arguments from deployment metadata
        let origin = deployment.get_origin().unwrap_or("");
        
        // Parse kernel arguments from origin
        if let Some(kargs) = self.parse_kargs_from_origin(origin) {
            Ok(kargs)
        } else {
            // Fallback to default kernel arguments
            Ok(Vec::new())
        }
    }
    
    fn parse_kargs_from_origin(&self, origin: &str) -> Option<Vec<String>> {
        // Parse kernel arguments from OSTree origin
        // This would parse the origin file to extract kernel arguments
        if origin.contains("kargs=") {
            let kargs_start = origin.find("kargs=")?;
            let kargs_end = origin[kargs_start..].find(' ').unwrap_or(origin.len());
            let kargs_str = &origin[kargs_start + 6..kargs_start + kargs_end];
            
            let args: Vec<String> = kargs_str
                .split(',')
                .map(|s| s.to_string())
                .collect();
            
            Some(args)
        } else {
            None
        }
    }
    
    pub async fn modify_kargs(&self, opts: &KargsOpts) -> Result<String, Box<dyn std::error::Error>> {
        // 1. Get current kernel arguments
        let mut current_kargs = self.get_current_kargs().await?;
        
        // 2. Apply modifications
        if !opts.delete.is_empty() {
            self.delete_kargs(&mut current_kargs, &opts.delete).await?;
        }
        
        if !opts.replace.is_empty() {
            self.replace_kargs(&mut current_kargs, &opts.replace).await?;
        }
        
        if !opts.prepend.is_empty() {
            self.prepend_kargs(&mut current_kargs, &opts.prepend).await?;
        }
        
        if !opts.append.is_empty() {
            self.append_kargs(&mut current_kargs, &opts.append).await?;
        }
        
        // 3. Handle editor mode
        if opts.editor {
            current_kargs = self.edit_kargs_interactive(&current_kargs).await?;
        }
        
        // 4. Validate final kernel arguments
        self.validate_kernel_arguments(&current_kargs).await?;
        
        // 5. Apply changes
        let transaction_id = self.apply_kernel_arguments(&current_kargs, opts).await?;
        
        Ok(transaction_id)
    }
    
    async fn delete_kargs(&self, kargs: &mut Vec<String>, to_delete: &[String]) -> Result<(), Box<dyn std::error::Error>> {
        for delete_arg in to_delete {
            kargs.retain(|arg| {
                if delete_arg.contains('=') {
                    // Delete by key=value
                    !arg.starts_with(&format!("{}=", delete_arg.split('=').next().unwrap()))
                } else {
                    // Delete by key only
                    !arg.starts_with(delete_arg)
                }
            });
        }
        Ok(())
    }
    
    async fn replace_kargs(&self, kargs: &mut Vec<String>, replacements: &[String]) -> Result<(), Box<dyn std::error::Error>> {
        for replacement in replacements {
            if let Some((key, _)) = replacement.split_once('=') {
                // Remove existing key
                kargs.retain(|arg| !arg.starts_with(&format!("{}=", key)));
                // Add new key=value
                kargs.push(replacement.clone());
            }
        }
        Ok(())
    }
    
    async fn prepend_kargs(&self, kargs: &mut Vec<String>, to_prepend: &[String]) -> Result<(), Box<dyn std::error::Error>> {
        for arg in to_prepend.iter().rev() {
            kargs.insert(0, arg.clone());
        }
        Ok(())
    }
    
    async fn append_kargs(&self, kargs: &mut Vec<String>, to_append: &[String]) -> Result<(), Box<dyn std::error::Error>> {
        kargs.extend_from_slice(to_append);
        Ok(())
    }
    
    async fn edit_kargs_interactive(&self, current_kargs: &[String]) -> Result<Vec<String>, Box<dyn std::error::Error>> {
        // 1. Create temporary file with current kernel arguments
        let temp_file = tempfile::NamedTempFile::new()?;
        let kargs_content = current_kargs.join(" ");
        tokio::fs::write(&temp_file, kargs_content).await?;
        
        // 2. Get editor
        let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
        
        // 3. Launch editor
        let status = Command::new(&editor)
            .arg(temp_file.path())
            .status()?;
        
        if !status.success() {
            return Err("Editor exited with error".into());
        }
        
        // 4. Read modified content
        let modified_content = tokio::fs::read_to_string(temp_file.path()).await?;
        let modified_kargs: Vec<String> = modified_content
            .split_whitespace()
            .map(|s| s.to_string())
            .collect();
        
        Ok(modified_kargs)
    }
    
    async fn validate_kernel_arguments(&self, kargs: &[String]) -> Result<(), Box<dyn std::error::Error>> {
        // Check for dangerous arguments
        let dangerous_args = ["init=", "root="];
        for arg in kargs {
            for dangerous in &dangerous_args {
                if arg.starts_with(dangerous) {
                    return Err(format!("Dangerous kernel argument not allowed: {}", arg).into());
                }
            }
        }
        
        // Check for duplicate arguments
        let mut seen = HashMap::new();
        for arg in kargs {
            if let Some(key) = arg.split('=').next() {
                if seen.contains_key(key) {
                    return Err(format!("Duplicate kernel argument: {}", key).into());
                }
                seen.insert(key.to_string(), true);
            }
        }
        
        Ok(())
    }
    
    async fn apply_kernel_arguments(&self, kargs: &[String], opts: &KargsOpts) -> Result<String, Box<dyn std::error::Error>> {
        // 1. Create transaction
        let transaction_id = format!("kargs-{}", uuid::Uuid::new_v4());
        
        // 2. Update boot configuration
        self.update_boot_configuration(kargs).await?;
        
        // 3. Update OSTree deployment metadata
        self.update_deployment_kargs(kargs).await?;
        
        // 4. Handle reboot if requested
        if opts.reboot {
            self.schedule_reboot().await?;
        }
        
        Ok(transaction_id)
    }
    
    async fn update_boot_configuration(&self, kargs: &[String]) -> Result<(), Box<dyn std::error::Error>> {
        // Update GRUB configuration
        self.update_grub_configuration(kargs).await?;
        
        // Update systemd-boot configuration
        self.update_systemd_boot_configuration(kargs).await?;
        
        Ok(())
    }
    
    async fn update_grub_configuration(&self, kargs: &[String]) -> Result<(), Box<dyn std::error::Error>> {
        let grub_cfg = "/boot/grub/grub.cfg";
        if Path::new(grub_cfg).exists() {
            // Update GRUB configuration with new kernel arguments
            let kargs_str = kargs.join(" ");
            
            // This would involve parsing and modifying the GRUB config
            // For now, just print what would be done
            println!("Would update GRUB configuration with kernel arguments: {}", kargs_str);
        }
        
        Ok(())
    }
    
    async fn update_systemd_boot_configuration(&self, kargs: &[String]) -> Result<(), Box<dyn std::error::Error>> {
        let loader_conf = "/boot/loader/loader.conf";
        if Path::new(loader_conf).exists() {
            // Update systemd-boot configuration
            let kargs_str = kargs.join(" ");
            println!("Would update systemd-boot configuration with kernel arguments: {}", kargs_str);
        }
        
        Ok(())
    }
    
    async fn update_deployment_kargs(&self, kargs: &[String]) -> Result<(), Box<dyn std::error::Error>> {
        // Update OSTree deployment metadata with new kernel arguments
        let sysroot = ostree::Sysroot::new_at(libc::AT_FDCWD, &self.sysroot_path);
        sysroot.load(None)?;
        
        let booted = sysroot.get_booted_deployment()
            .ok_or("No booted deployment found")?;
        
        // Create new deployment with updated kernel arguments
        let kargs_str = kargs.join(",");
        let new_origin = format!("{} kargs={}", booted.get_origin().unwrap_or(""), kargs_str);
        
        // This would involve creating a new deployment with the updated origin
        println!("Would create new deployment with kernel arguments: {}", kargs_str);
        
        Ok(())
    }
    
    async fn schedule_reboot(&self) -> Result<(), Box<dyn std::error::Error>> {
        let output = Command::new("systemctl")
            .arg("reboot")
            .output()?;
        
        if !output.status.success() {
            return Err("Failed to schedule reboot".into());
        }
        
        println!("Reboot scheduled");
        Ok(())
    }
}

Phase 3: D-Bus Integration

Files to Modify:

  • src/daemon.rs - Add kargs D-Bus method
  • src/client.rs - Add kargs client method

Implementation Steps:

3.1 Add Kargs D-Bus Method (src/daemon.rs)

#[dbus_interface(name = "org.aptostree.dev")]
impl AptOstreeDaemon {
    /// Modify kernel arguments
    async fn kernel_args(&self, options: HashMap<String, Value>) -> Result<String, Box<dyn std::error::Error>> {
        let kargs_manager = KargsManager::new(None);
        
        // Convert options to KargsOpts
        let opts = KargsOpts {
            append: options.get("append")
                .and_then(|v| v.as_array())
                .map(|arr| arr.iter().filter_map(|v| v.as_str()).map(|s| s.to_string()).collect())
                .unwrap_or_default(),
            prepend: options.get("prepend")
                .and_then(|v| v.as_array())
                .map(|arr| arr.iter().filter_map(|v| v.as_str()).map(|s| s.to_string()).collect())
                .unwrap_or_default(),
            delete: options.get("delete")
                .and_then(|v| v.as_array())
                .map(|arr| arr.iter().filter_map(|v| v.as_str()).map(|s| s.to_string()).collect())
                .unwrap_or_default(),
            replace: options.get("replace")
                .and_then(|v| v.as_array())
                .map(|arr| arr.iter().filter_map(|v| v.as_str()).map(|s| s.to_string()).collect())
                .unwrap_or_default(),
            editor: options.get("editor").and_then(|v| v.as_bool()).unwrap_or(false),
            reboot: options.get("reboot").and_then(|v| v.as_bool()).unwrap_or(false),
            dry_run: options.get("dry-run").and_then(|v| v.as_bool()).unwrap_or(false),
            stateroot: options.get("stateroot").and_then(|v| v.as_str()).map(|s| s.to_string()),
            sysroot: options.get("sysroot").and_then(|v| v.as_str()).map(|s| s.to_string()),
            peer: options.get("peer").and_then(|v| v.as_bool()).unwrap_or(false),
            quiet: options.get("quiet").and_then(|v| v.as_bool()).unwrap_or(false),
            json: options.get("json").and_then(|v| v.as_bool()).unwrap_or(false),
        };
        
        let transaction_id = kargs_manager.modify_kargs(&opts).await?;
        Ok(transaction_id)
    }
}

3.2 Add Kargs Client Method (src/client.rs)

impl AptOstreeClient {
    pub async fn modify_kernel_args(&self, opts: &KargsOpts) -> Result<String, Box<dyn std::error::Error>> {
        // Try daemon first
        if let Ok(transaction_id) = self.modify_kernel_args_via_daemon(opts).await {
            return Ok(transaction_id);
        }
        
        // Fallback to direct kargs manager
        let kargs_manager = KargsManager::new(opts.sysroot.as_deref());
        kargs_manager.modify_kargs(opts).await
    }
    
    async fn modify_kernel_args_via_daemon(&self, opts: &KargsOpts) -> Result<String, Box<dyn std::error::Error>> {
        let mut options = HashMap::new();
        
        if !opts.append.is_empty() {
            options.insert("append".to_string(), Value::Array(
                opts.append.iter().map(|s| Value::String(s.clone())).collect()
            ));
        }
        
        if !opts.prepend.is_empty() {
            options.insert("prepend".to_string(), Value::Array(
                opts.prepend.iter().map(|s| Value::String(s.clone())).collect()
            ));
        }
        
        if !opts.delete.is_empty() {
            options.insert("delete".to_string(), Value::Array(
                opts.delete.iter().map(|s| Value::String(s.clone())).collect()
            ));
        }
        
        if !opts.replace.is_empty() {
            options.insert("replace".to_string(), Value::Array(
                opts.replace.iter().map(|s| Value::String(s.clone())).collect()
            ));
        }
        
        options.insert("editor".to_string(), Value::Bool(opts.editor));
        options.insert("reboot".to_string(), Value::Bool(opts.reboot));
        options.insert("dry-run".to_string(), Value::Bool(opts.dry_run));
        options.insert("peer".to_string(), Value::Bool(opts.peer));
        options.insert("quiet".to_string(), Value::Bool(opts.quiet));
        options.insert("json".to_string(), Value::Bool(opts.json));
        
        if let Some(ref stateroot) = opts.stateroot {
            options.insert("stateroot".to_string(), Value::String(stateroot.clone()));
        }
        
        if let Some(ref sysroot) = opts.sysroot {
            options.insert("sysroot".to_string(), Value::String(sysroot.clone()));
        }
        
        // Call daemon method
        let proxy = self.get_dbus_proxy().await?;
        let transaction_id: String = proxy.kernel_args(options).await?;
        Ok(transaction_id)
    }
}

Main Kargs Command Implementation

Files to Modify:

  • src/main.rs - Main kargs command logic

Implementation:

async fn kargs_command(opts: KargsOpts) -> Result<(), Box<dyn std::error::Error>> {
    // 1. Validate options
    opts.validate()?;
    
    // 2. Check permissions
    if !opts.dry_run {
        check_root_permissions()?;
    }
    
    // 3. Display current kernel arguments if no modifications
    if opts.append.is_empty() && opts.prepend.is_empty() && 
       opts.delete.is_empty() && opts.replace.is_empty() && !opts.editor {
        let kargs_manager = KargsManager::new(opts.sysroot.as_deref());
        let current_kargs = kargs_manager.get_current_kargs().await?;
        println!("Current kernel arguments: {}", current_kargs.join(" "));
        return Ok(());
    }
    
    // 4. Perform kernel argument modification
    let client = AptOstreeClient::new().await?;
    let transaction_id = client.modify_kernel_args(&opts).await?;
    
    // 5. Display results
    if !opts.quiet {
        println!("Kernel arguments modified successfully");
        if opts.reboot {
            println!("Reboot scheduled to apply changes");
        }
    }
    
    Ok(())
}

Testing Strategy

Unit Tests

#[cfg(test)]
mod tests {
    use super::*;
    
    #[tokio::test]
    async fn test_kargs_validation() {
        let opts = KargsOpts {
            append: vec!["console=ttyS0".to_string()],
            prepend: vec![],
            delete: vec![],
            replace: vec![],
            editor: false,
            reboot: false,
            dry_run: false,
            stateroot: None,
            sysroot: None,
            peer: false,
            quiet: false,
            json: false,
        };
        
        assert!(opts.validate().is_ok());
        
        let opts = KargsOpts {
            append: vec!["init=/bin/bash".to_string()],
            prepend: vec![],
            delete: vec![],
            replace: vec![],
            editor: false,
            reboot: false,
            dry_run: false,
            stateroot: None,
            sysroot: None,
            peer: false,
            quiet: false,
            json: false,
        };
        
        assert!(opts.validate().is_err());
    }
    
    #[tokio::test]
    async fn test_kernel_argument_parsing() {
        let kargs_manager = KargsManager::new(None);
        let kargs = vec!["console=ttyS0".to_string(), "quiet".to_string()];
        
        let result = kargs_manager.validate_kernel_arguments(&kargs).await;
        assert!(result.is_ok());
    }
}

Integration Tests

#[tokio::test]
async fn test_kargs_command_integration() {
    let opts = KargsOpts {
        append: vec!["console=ttyS0".to_string()],
        prepend: vec![],
        delete: vec![],
        replace: vec![],
        editor: false,
        reboot: false,
        dry_run: true, // Use dry-run for testing
        stateroot: None,
        sysroot: None,
        peer: false,
        quiet: false,
        json: false,
    };
    
    let result = kargs_command(opts).await;
    assert!(result.is_ok());
}

Error Handling

Files to Modify:

  • src/error.rs - Add kargs-specific errors

Implementation:

#[derive(Debug, thiserror::Error)]
pub enum KargsError {
    #[error("Invalid kernel argument format: {0}")]
    InvalidFormat(String),
    
    #[error("Dangerous kernel argument not allowed: {0}")]
    DangerousArgument(String),
    
    #[error("Duplicate kernel argument: {0}")]
    DuplicateArgument(String),
    
    #[error("Failed to read kernel arguments: {0}")]
    ReadError(String),
    
    #[error("Failed to update boot configuration: {0}")]
    BootConfigError(String),
    
    #[error("Editor exited with error")]
    EditorError,
    
    #[error("Kargs requires root privileges")]
    PermissionError,
}

impl From<KargsError> for Box<dyn std::error::Error> {
    fn from(err: KargsError) -> Self {
        Box::new(err)
    }
}

Dependencies to Add

Add to Cargo.toml:

[dependencies]
uuid = { version = "1.0", features = ["v4"] }
tempfile = "3.0"
tokio = { version = "1.0", features = ["fs", "process"] }
libc = "0.2"

Implementation Checklist

  • Add CLI structure for kargs command
  • Implement kernel argument parsing and validation
  • Add interactive editor integration
  • Implement kernel argument modification logic
  • Add boot configuration updates (GRUB, systemd-boot)
  • Add OSTree deployment metadata updates
  • Add D-Bus integration
  • Add comprehensive error handling
  • Write unit and integration tests
  • Update documentation

References

  • rpm-ostree source: src/app/rpmostree-builtin-kargs.cxx (376 lines)
  • GRUB configuration management
  • systemd-boot configuration
  • OSTree deployment metadata
  • Kernel argument parsing and validation