use sysknife_core::distro::{DistroFamily, DistroId};
use sysknife_core::action_family::{
action_matches_distro, action_requires_supported_host, DEBIAN_ONLY_ACTIONS,
FEDORA_ONLY_ACTIONS, UBUNTU_ONLY_ACTIONS,
};
pub fn check_action_distro(action_name: &str, distro: Option<&DistroId>) -> Result<(), String> {
let distro = match distro {
Some(d) => d,
None => return Ok(()), };
let family = distro.family();
if UBUNTU_ONLY_ACTIONS.contains(&action_name) && !action_matches_distro(action_name, distro) {
return Err(format!(
"{action_name} requires Ubuntu itself; current distro is {distro}"
));
}
if DEBIAN_ONLY_ACTIONS.contains(&action_name) && family != DistroFamily::Debian {
return Err(format!(
"{action_name} is only valid on Debian-family distros (apt/dpkg/GRUB); \
current distro is {distro} ({family_name})",
family_name = family_label(&family),
));
}
if FEDORA_ONLY_ACTIONS.contains(&action_name) && family != DistroFamily::Fedora {
return Err(format!(
"{action_name} is only valid on Fedora-family distros (rpm-ostree/dnf); \
current distro is {distro} ({family_name})",
family_name = family_label(&family),
));
}
if action_requires_supported_host(action_name) && !distro.is_supported() {
return Err(format!(
"{action_name} is disabled on unsupported distro {distro}; \
see docs/distro-support.md"
));
}
Ok(())
}
fn family_label(f: &DistroFamily) -> &'static str {
match f {
DistroFamily::Fedora => "Fedora family",
DistroFamily::Debian => "Debian family",
DistroFamily::Other => "unknown family",
}
}
#[cfg(test)]
mod tests {
use super::*;
use sysknife_core::distro::DistroId;
#[test]
fn apt_install_on_fedora_returns_error() {
let distro = DistroId::Fedora { version: 41 };
let result = check_action_distro("AptInstall", Some(&distro));
assert!(result.is_err());
let msg = result.unwrap_err();
assert!(
msg.contains("Debian-family"),
"error must mention Debian-family; got: {msg}"
);
assert!(
msg.contains("Fedora 41"),
"error must mention current distro; got: {msg}"
);
}
#[test]
fn snap_install_on_fedora_silverblue_is_not_hard_fenced() {
let distro = DistroId::FedoraSilverblue { version: 41 };
let result = check_action_distro("SnapInstall", Some(&distro));
assert!(result.is_ok());
}
#[test]
fn ufw_allow_on_fedora_is_not_hard_fenced() {
let distro = DistroId::FedoraSilverblue { version: 41 };
let result = check_action_distro("UfwAllow", Some(&distro));
assert!(result.is_ok());
}
#[test]
fn netplan_apply_on_fedora_is_not_hard_fenced() {
let distro = DistroId::FedoraSilverblue { version: 41 };
let result = check_action_distro("NetplanApply", Some(&distro));
assert!(result.is_ok());
}
#[test]
fn distrobox_create_on_fedora_is_not_hard_fenced() {
let distro = DistroId::FedoraSilverblue { version: 41 };
let result = check_action_distro("DistroboxCreate", Some(&distro));
assert!(result.is_ok());
}
#[test]
fn apt_install_on_ubuntu_is_ok() {
let distro = DistroId::Ubuntu {
major: 24,
minor: 4,
};
assert!(check_action_distro("AptInstall", Some(&distro)).is_ok());
}
#[test]
fn apt_install_on_ubuntu_core_is_rejected() {
let distro = DistroId::UbuntuCore {
major: 24,
minor: 4,
};
assert!(check_action_distro("AptInstall", Some(&distro)).is_err());
}
#[test]
fn apt_install_on_unsupported_ubuntu_release_is_rejected() {
let distro = DistroId::Ubuntu {
major: 18,
minor: 4,
};
assert!(check_action_distro("AptInstall", Some(&distro)).is_err());
}
#[test]
fn apt_install_is_allowed_on_every_supported_ubuntu_release() {
for (major, minor) in [(20, 4), (22, 4), (24, 4), (25, 10), (26, 4), (26, 10)] {
let distro = DistroId::Ubuntu { major, minor };
assert!(
check_action_distro("AptInstall", Some(&distro)).is_ok(),
"AptInstall must route on Ubuntu {major}.{minor:02}"
);
}
}
#[test]
fn portable_tools_require_supported_hosts_before_approval() {
for action in [
"SnapInstall",
"UfwEnable",
"UfwAllow",
"UfwLimit",
"AppArmorEnforce",
"Fail2banBanIp",
"ConfigureFail2banJail",
"ConfigureFirewall",
"CreateToolbox",
"AptUpdate",
] {
for distro in [
DistroId::Debian { version: Some(11) },
DistroId::Debian { version: None },
DistroId::Ubuntu {
major: 18,
minor: 4,
},
DistroId::Fedora { version: 41 },
DistroId::Other {
id: "arch".into(),
version_id: None,
id_like: vec![],
},
] {
assert!(
check_action_distro(action, Some(&distro)).is_err(),
"{action} must be refused before approval on {distro}"
);
}
for distro in [
DistroId::Debian { version: Some(12) },
DistroId::Debian { version: Some(13) },
DistroId::Ubuntu {
major: 24,
minor: 4,
},
DistroId::FedoraSilverblue { version: 41 },
] {
if action != "AptUpdate" || matches!(distro, DistroId::Debian { .. }) {
assert!(
check_action_distro(action, Some(&distro)).is_ok(),
"portable {action} must remain usable on {distro}"
);
}
}
}
}
#[test]
fn rebase_system_on_ubuntu_returns_error() {
let distro = DistroId::Ubuntu {
major: 24,
minor: 4,
};
let result = check_action_distro("RebaseSystem", Some(&distro));
assert!(result.is_err());
let msg = result.unwrap_err();
assert!(msg.contains("Fedora-family"), "got: {msg}");
assert!(msg.contains("Ubuntu 24.04"), "got: {msg}");
}
#[test]
fn add_layered_package_on_ubuntu_returns_error() {
let distro = DistroId::Ubuntu {
major: 24,
minor: 4,
};
let result = check_action_distro("AddLayeredPackage", Some(&distro));
assert!(result.is_err());
}
#[test]
fn no_distro_allows_all_actions() {
assert!(check_action_distro("AptInstall", None).is_ok());
assert!(check_action_distro("RebaseSystem", None).is_ok());
assert!(check_action_distro("GetSystemState", None).is_ok());
}
#[test]
fn get_system_state_allowed_on_fedora_atomic() {
let distro = DistroId::FedoraSilverblue { version: 41 };
assert!(check_action_distro("GetSystemState", Some(&distro)).is_ok());
}
#[test]
fn get_system_state_is_refused_on_ubuntu_before_approval() {
let distro = DistroId::Ubuntu {
major: 24,
minor: 4,
};
assert!(
check_action_distro("GetSystemState", Some(&distro)).is_err(),
"an rpm-ostree action must not reach an apt host's approval prompt"
);
}
#[test]
fn get_host_state_is_the_ubuntu_counterpart() {
let ubuntu = DistroId::Ubuntu {
major: 24,
minor: 4,
};
assert!(check_action_distro("GetHostState", Some(&ubuntu)).is_ok());
let fedora = DistroId::FedoraSilverblue { version: 41 };
assert!(
check_action_distro("GetHostState", Some(&fedora)).is_ok(),
"hostnamectl is portable even when Fedora prefers deployment state"
);
}
#[test]
fn install_flatpak_allowed_on_fedora() {
let distro = DistroId::Fedora { version: 41 };
assert!(check_action_distro("InstallFlatpak", Some(&distro)).is_ok());
}
#[test]
fn install_flatpak_allowed_on_ubuntu() {
let distro = DistroId::Ubuntu {
major: 24,
minor: 4,
};
assert!(check_action_distro("InstallFlatpak", Some(&distro)).is_ok());
}
#[test]
fn all_debian_only_actions_rejected_on_fedora() {
let distro = DistroId::Fedora { version: 41 };
for action in super::DEBIAN_ONLY_ACTIONS {
let result = check_action_distro(action, Some(&distro));
assert!(
result.is_err(),
"{action} should be rejected on Fedora but was allowed"
);
}
}
#[test]
fn ubuntu_only_actions_require_identity_and_eligibility() {
let ubuntu = DistroId::Ubuntu {
major: 24,
minor: 4,
};
for action in UBUNTU_ONLY_ACTIONS {
assert!(
check_action_distro(action, Some(&ubuntu)).is_ok(),
"{action}"
);
for distro in [
DistroId::Debian { version: Some(13) },
DistroId::FedoraSilverblue { version: 41 },
DistroId::UbuntuCore {
major: 24,
minor: 4,
},
DistroId::Ubuntu {
major: 18,
minor: 4,
},
] {
assert!(
check_action_distro(action, Some(&distro)).is_err(),
"{action} on {distro}"
);
}
}
}
#[test]
fn all_fedora_only_actions_rejected_on_ubuntu() {
let distro = DistroId::Ubuntu {
major: 24,
minor: 4,
};
for action in super::FEDORA_ONLY_ACTIONS {
let result = check_action_distro(action, Some(&distro));
assert!(
result.is_err(),
"{action} should be rejected on Ubuntu but was allowed"
);
}
}
#[test]
fn distro_id_display_fedora() {
assert_eq!(format!("{}", DistroId::Fedora { version: 41 }), "Fedora 41");
}
#[test]
fn distro_id_display_fedora_silverblue() {
assert_eq!(
format!("{}", DistroId::FedoraSilverblue { version: 41 }),
"FedoraSilverblue 41"
);
}
#[test]
fn distro_id_display_ubuntu() {
assert_eq!(
format!(
"{}",
DistroId::Ubuntu {
major: 24,
minor: 4
}
),
"Ubuntu 24.04"
);
}
#[test]
fn distro_id_display_debian() {
assert_eq!(
format!("{}", DistroId::Debian { version: Some(12) }),
"Debian 12"
);
}
}