24 KiB
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 optionssrc/kargs.rs- New file for kernel argument managementsrc/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 logicsrc/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(¤t_kargs).await?;
}
// 4. Validate final kernel arguments
self.validate_kernel_arguments(¤t_kargs).await?;
// 5. Apply changes
let transaction_id = self.apply_kernel_arguments(¤t_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 methodsrc/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