use super::{command_mechanism, ActionSpec};
use sysknife_types::RiskLevel;
const GRUB_KARGS_HELPER: &str = "/usr/lib/sysknife/grub-kargs-edit";
pub fn specs() -> Vec<ActionSpec> {
vec![
grub_get_kargs(),
grub_set_kargs(&["quiet"], &["splash"]).expect("non-empty kargs"),
]
}
pub fn grub_get_kargs() -> ActionSpec {
ActionSpec {
action_name: "GrubGetKargs",
mechanism: command_mechanism("grep", ["-E", r"^GRUB_CMDLINE_LINUX", "/etc/default/grub"]),
risk_level: RiskLevel::Low,
reboot_required: false,
rollback_available: false,
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum KargsError {
#[error("at least one of append or delete must be non-empty")]
BothEmpty,
}
pub fn grub_set_kargs(append: &[&str], delete: &[&str]) -> Result<ActionSpec, KargsError> {
if append.is_empty() && delete.is_empty() {
return Err(KargsError::BothEmpty);
}
let append_csv = append.join(",");
let delete_csv = delete.join(",");
Ok(ActionSpec {
action_name: "GrubSetKargs",
mechanism: command_mechanism(
"sudo",
[
GRUB_KARGS_HELPER,
"--append",
&append_csv,
"--delete",
&delete_csv,
],
),
risk_level: RiskLevel::High,
reboot_required: true,
rollback_available: false,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::actions::ActionMechanism;
fn extract_cmd(spec: &ActionSpec) -> (&'static str, Vec<String>) {
match &spec.mechanism {
ActionMechanism::Command { program, args } => (*program, args.clone()),
_ => panic!("expected Command mechanism"),
}
}
#[test]
fn grub_get_kargs_action_name() {
assert_eq!(grub_get_kargs().action_name, "GrubGetKargs");
}
#[test]
fn grub_get_kargs_uses_grep_on_grub_default() {
let spec = grub_get_kargs();
let (prog, args) = extract_cmd(&spec);
assert_eq!(prog, "grep");
let joined = args.join(" ");
assert!(
joined.contains("/etc/default/grub"),
"missing grub path: {joined}"
);
assert!(
joined.contains("GRUB_CMDLINE_LINUX"),
"missing pattern: {joined}"
);
}
#[test]
fn grub_get_kargs_risk_is_low() {
assert_eq!(grub_get_kargs().risk_level, RiskLevel::Low);
}
#[test]
fn grub_get_kargs_no_reboot_no_rollback() {
let spec = grub_get_kargs();
assert!(!spec.reboot_required);
assert!(!spec.rollback_available);
}
#[test]
fn grub_set_kargs_action_name() {
assert_eq!(
grub_set_kargs(&["quiet"], &[]).unwrap().action_name,
"GrubSetKargs"
);
}
#[test]
fn grub_set_kargs_invokes_sudo_helper() {
let spec = grub_set_kargs(&["quiet"], &["splash"]).unwrap();
let (prog, args) = extract_cmd(&spec);
assert_eq!(prog, "sudo", "program must be sudo");
assert_eq!(
args[0], GRUB_KARGS_HELPER,
"first arg must be the helper path"
);
}
#[test]
fn grub_set_kargs_argv_shape_append_only() {
let spec = grub_set_kargs(&["quiet"], &[]).unwrap();
let (prog, args) = extract_cmd(&spec);
assert_eq!(prog, "sudo");
assert_eq!(args[0], GRUB_KARGS_HELPER);
assert_eq!(args[1], "--append");
assert_eq!(args[2], "quiet");
assert_eq!(args[3], "--delete");
assert_eq!(args[4], "");
}
#[test]
fn grub_set_kargs_argv_shape_delete_only() {
let spec = grub_set_kargs(&[], &["splash"]).unwrap();
let (prog, args) = extract_cmd(&spec);
assert_eq!(prog, "sudo");
assert_eq!(args[0], GRUB_KARGS_HELPER);
assert_eq!(args[1], "--append");
assert_eq!(args[2], "");
assert_eq!(args[3], "--delete");
assert_eq!(args[4], "splash");
}
#[test]
fn grub_set_kargs_argv_shape_both() {
let spec = grub_set_kargs(&["nomodeset", "quiet"], &["splash", "quiet"]).unwrap();
let (prog, args) = extract_cmd(&spec);
assert_eq!(prog, "sudo");
assert_eq!(args[0], GRUB_KARGS_HELPER);
assert_eq!(args[2], "nomodeset,quiet", "append CSV");
assert_eq!(args[4], "splash,quiet", "delete CSV");
}
#[test]
fn grub_set_kargs_risk_is_high() {
assert_eq!(
grub_set_kargs(&["quiet"], &[]).unwrap().risk_level,
RiskLevel::High
);
}
#[test]
fn grub_set_kargs_reboot_required() {
assert!(grub_set_kargs(&["quiet"], &[]).unwrap().reboot_required);
}
#[test]
fn grub_set_kargs_no_automatic_rollback() {
assert!(!grub_set_kargs(&["quiet"], &[]).unwrap().rollback_available);
}
#[test]
fn grub_set_kargs_rejects_both_empty() {
let err = grub_set_kargs(&[], &[]).unwrap_err();
assert_eq!(err, KargsError::BothEmpty);
}
#[test]
fn specs_covers_all_action_names() {
let expected = ["GrubGetKargs", "GrubSetKargs"];
let spec_names: Vec<&str> = specs().iter().map(|s| s.action_name).collect();
for name in &expected {
assert!(spec_names.contains(name), "specs() missing {name}");
}
}
}