use crate::error::RegentError;
use crate::hosts::command::CommandResult;
use crate::hosts::managed_host::InternalApiCallOutcome;
use crate::hosts::properties::{HostProperties, LinuxFlavor, OsKind};
use crate::state::attribute::HostHandler;
use crate::state::attribute::Privilege;
use crate::state::attribute::container::container_spec::quoted_command;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum PackageFamily {
Apt,
Dnf,
Zypper,
}
impl fmt::Display for PackageFamily {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PackageFamily::Apt => write!(f, "apt"),
PackageFamily::Dnf => write!(f, "dnf"),
PackageFamily::Zypper => write!(f, "zypper"),
}
}
}
impl PackageFamily {
pub(crate) fn repository_locations(&self) -> &'static [&'static str] {
match self {
PackageFamily::Apt => &["/etc/apt/sources.list", "/etc/apt/sources.list.d"],
PackageFamily::Dnf => &["/etc/yum.repos.d"],
PackageFamily::Zypper => &["/etc/zypp/repos.d"],
}
}
pub(crate) fn repository_check_command(&self, url: &str) -> String {
let mut args = vec!["grep", "-rlsF", url];
args.extend(self.repository_locations());
quoted_command(&args)
}
pub(crate) fn installed_packages_command(&self, patterns: &[&str]) -> String {
let mut args = match self {
PackageFamily::Apt => vec![
"dpkg-query",
"-W",
"-f=${Package} ${Version} ${db:Status-Abbrev}\\n",
],
PackageFamily::Dnf | PackageFamily::Zypper => {
vec!["rpm", "-qa", "--qf", "%{NAME} %{VERSION}-%{RELEASE} ii\\n"]
}
};
args.extend(patterns);
quoted_command(&args)
}
pub(crate) fn install_commands(
&self,
packages: &[String],
allow_downgrade: bool,
) -> Vec<String> {
let mut commands = Vec::new();
if *self == PackageFamily::Apt {
commands.push(quoted_command(&["apt-get", "update"]));
}
commands.push(self.install_command(packages, allow_downgrade));
commands
}
pub(crate) fn install_command(&self, packages: &[String], allow_downgrade: bool) -> String {
let mut args: Vec<&str> = match self {
PackageFamily::Apt => vec!["apt-get", "install", "-y"],
PackageFamily::Dnf if allow_downgrade => vec!["dnf", "-y", "downgrade"],
PackageFamily::Dnf => vec!["dnf", "-y", "install"],
PackageFamily::Zypper => vec![
"zypper",
"--non-interactive",
"--gpg-auto-import-keys",
"install",
"--force-resolution",
],
};
if allow_downgrade {
match self {
PackageFamily::Apt => args.push("--allow-downgrades"),
PackageFamily::Zypper => args.push("--oldpackage"),
PackageFamily::Dnf => {}
}
}
args.extend(packages.iter().map(String::as_str));
let command = quoted_command(&args);
match self {
PackageFamily::Apt => format!("DEBIAN_FRONTEND=noninteractive {}", command),
_ => command,
}
}
pub(crate) fn remove_command(&self, packages: &[String]) -> String {
let mut args: Vec<&str> = match self {
PackageFamily::Apt => vec!["apt-get", "remove", "--autoremove", "--purge", "-y"],
PackageFamily::Dnf => vec!["dnf", "-y", "remove"],
PackageFamily::Zypper => vec!["zypper", "--non-interactive", "remove"],
};
args.extend(packages.iter().map(String::as_str));
let command = quoted_command(&args);
match self {
PackageFamily::Apt => format!("DEBIAN_FRONTEND=noninteractive {}", command),
_ => command,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct OsRelease {
pub id: String,
pub id_like: Vec<String>,
pub version_id: String,
pub pretty_name: String,
}
impl OsRelease {
pub fn major_version(&self) -> &str {
self.version_id
.split('.')
.next()
.unwrap_or(&self.version_id)
}
fn description(&self) -> String {
if self.pretty_name.is_empty() {
format!("{} {}", self.id, self.version_id)
} else {
self.pretty_name.clone()
}
}
}
pub fn parse_os_release(content: &str) -> OsRelease {
let mut os_release = OsRelease::default();
for line in content.lines() {
let Some((key, value)) = line.trim().split_once('=') else {
continue;
};
let value = value.trim().trim_matches(|c| c == '"' || c == '\'');
match key {
"ID" => os_release.id = value.to_lowercase(),
"ID_LIKE" => {
os_release.id_like = value.split_whitespace().map(str::to_lowercase).collect()
}
"VERSION_ID" => os_release.version_id = value.to_string(),
"PRETTY_NAME" => os_release.pretty_name = value.to_string(),
_ => {}
}
}
os_release
}
pub fn package_family(os_release: &OsRelease) -> Option<PackageFamily> {
std::iter::once(&os_release.id)
.chain(&os_release.id_like)
.find_map(|id| match id.as_str() {
"debian" | "ubuntu" => Some(PackageFamily::Apt),
"rhel" | "fedora" | "centos" | "rocky" | "almalinux" | "ol" | "amzn" => {
Some(PackageFamily::Dnf)
}
"suse" | "sles" | "sled" | "opensuse" | "opensuse-leap" | "opensuse-tumbleweed" => {
Some(PackageFamily::Zypper)
}
_ => None,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum CudaArch {
X86_64,
Sbsa,
}
impl CudaArch {
pub fn repo_name(&self) -> &'static str {
match self {
CudaArch::X86_64 => "x86_64",
CudaArch::Sbsa => "sbsa",
}
}
}
pub fn parse_machine_arch(uname_m: &str) -> Result<CudaArch, String> {
match uname_m.trim() {
"x86_64" | "amd64" => Ok(CudaArch::X86_64),
"aarch64" | "arm64" => Ok(CudaArch::Sbsa),
other => Err(format!(
"CPU architecture {:?} is not supported by the NVIDIA CUDA repositories (x86_64 and aarch64 only)",
other
)),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum DriverDistro {
Ubuntu,
Debian,
RhelModular,
AmazonLinux,
Rhel10,
Fedora,
Suse,
}
impl DriverDistro {
pub fn package_family(&self) -> PackageFamily {
match self {
DriverDistro::Ubuntu | DriverDistro::Debian => PackageFamily::Apt,
DriverDistro::RhelModular
| DriverDistro::AmazonLinux
| DriverDistro::Rhel10
| DriverDistro::Fedora => PackageFamily::Dnf,
DriverDistro::Suse => PackageFamily::Zypper,
}
}
pub fn kernel_headers_packages(&self, kernel_release: &str) -> Vec<String> {
match self {
DriverDistro::Ubuntu | DriverDistro::Debian => {
vec![format!("linux-headers-{}", kernel_release)]
}
DriverDistro::RhelModular => vec![
format!("kernel-devel-{}", kernel_release),
"kernel-headers".to_string(),
],
DriverDistro::AmazonLinux => vec![
format!("kernel-devel-{}", kernel_release),
format!("kernel-headers-{}", kernel_release),
],
DriverDistro::Rhel10 | DriverDistro::Fedora => vec![
"kernel-devel-matched".to_string(),
"kernel-headers".to_string(),
],
DriverDistro::Suse => {
match kernel_release.rsplit_once('-') {
Some((version, variant))
if !variant.is_empty() && !variant.starts_with(char::is_numeric) =>
{
vec![format!("kernel-{}-devel={}", variant, version)]
}
_ => vec!["kernel-default-devel".to_string()],
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct CudaRepository {
pub distro_name: String,
pub arch: CudaArch,
pub distro: DriverDistro,
}
pub(crate) const CUDA_REPOS_URL: &str = "https://developer.download.nvidia.com/compute/cuda/repos";
impl CudaRepository {
pub fn base_url(&self) -> String {
format!(
"{}/{}/{}/",
CUDA_REPOS_URL,
self.distro_name,
self.arch.repo_name()
)
}
pub fn url_fragment(&self) -> String {
format!(
"developer.download.nvidia.com/compute/cuda/repos/{}/{}",
self.distro_name,
self.arch.repo_name()
)
}
pub(crate) async fn setup<Handler: HostHandler>(
&self,
host_handler: &mut Handler,
privilege: &Privilege,
) -> Result<InternalApiCallOutcome, RegentError> {
let family = self.distro.package_family();
if family != PackageFamily::Apt {
return add_repo_file(
host_handler,
family,
&format!("{}cuda-{}.repo", self.base_url(), self.distro_name),
&format!("cuda-{}.repo", self.distro_name),
privilege,
)
.await;
}
let tmp = match make_temp_dir(host_handler, privilege).await? {
Ok(tmp) => tmp,
Err(failure) => return Ok(failure),
};
let keyring = format!("{}/cuda-keyring.deb", tmp);
let mut outcome = download(
host_handler,
&format!("{}cuda-keyring_1.1-1_all.deb", self.base_url()),
&keyring,
privilege,
)
.await;
if is_success(&outcome) {
outcome = run_steps(
host_handler,
&[
quoted_command(&["dpkg", "-i", &keyring]),
quoted_command(&["apt-get", "update"]),
],
privilege,
)
.await;
}
remove_temp_dir(host_handler, &tmp, privilege).await;
Ok(outcome)
}
}
const SUPPORTED_DISTROS: &str = "Ubuntu 22.04/24.04/26.04, Debian 12/13, RHEL/Rocky/AlmaLinux/Oracle Linux/CentOS Stream 8/9/10, Fedora 42/43/44, Amazon Linux 2023, SLES 15/16, openSUSE Leap 15";
pub fn cuda_repository(os_release: &OsRelease, uname_m: &str) -> Result<CudaRepository, String> {
let arch = parse_machine_arch(uname_m)?;
let major = os_release.major_version();
let unsupported = || {
format!(
"{} is not supported by the NVIDIA CUDA network repository (supported: {})",
os_release.description(),
SUPPORTED_DISTROS
)
};
let (distro_name, distro) = match os_release.id.as_str() {
"ubuntu" => match os_release.version_id.as_str() {
"22.04" | "24.04" | "26.04" => (
format!("ubuntu{}", os_release.version_id.replace('.', "")),
DriverDistro::Ubuntu,
),
_ => return Err(unsupported()),
},
"debian" => match major {
"12" | "13" => (format!("debian{}", major), DriverDistro::Debian),
_ => return Err(unsupported()),
},
"fedora" => match major {
"42" | "43" | "44" => (format!("fedora{}", major), DriverDistro::Fedora),
_ => return Err(unsupported()),
},
"amzn" => match major {
"2023" => ("amzn2023".to_string(), DriverDistro::AmazonLinux),
_ => return Err(unsupported()),
},
"sles" | "sled" => match major {
"15" | "16" => (format!("sles{}", major), DriverDistro::Suse),
_ => return Err(unsupported()),
},
"opensuse-leap" => match major {
"15" => ("opensuse15".to_string(), DriverDistro::Suse),
_ => return Err(unsupported()),
},
"rhel" | "rocky" | "almalinux" | "ol" | "centos" => {
rhel_like(major).ok_or_else(unsupported)?
}
_ if os_release.id_like.iter().any(|id| id == "rhel") => {
rhel_like(major).ok_or_else(unsupported)?
}
_ => return Err(unsupported()),
};
Ok(CudaRepository {
distro_name,
arch,
distro,
})
}
fn rhel_like(major: &str) -> Option<(String, DriverDistro)> {
match major {
"8" | "9" => Some((format!("rhel{}", major), DriverDistro::RhelModular)),
"10" => Some(("rhel10".to_string(), DriverDistro::Rhel10)),
_ => None,
}
}
pub fn parse_installed_packages(output: &str) -> BTreeMap<String, String> {
output
.lines()
.filter_map(|line| {
let mut parts = line.split_whitespace();
let (name, version, status) = (parts.next()?, parts.next()?, parts.next()?);
let is_installed = status.chars().nth(1) == Some('i');
let is_version = version.starts_with(|c: char| c.is_ascii_digit());
(is_installed && is_version && parts.next().is_none())
.then(|| (name.to_string(), version.to_string()))
})
.collect()
}
pub fn upstream_version(version: &str) -> &str {
let without_epoch = version.split_once(':').map_or(version, |(_, rest)| rest);
without_epoch
.split_once('-')
.map_or(without_epoch, |(upstream, _)| upstream)
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum UnitEnablement {
#[default]
NotFound,
Enabled,
Disabled,
Static,
Masked,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct UnitState {
pub enablement: UnitEnablement,
pub active: bool,
}
impl UnitState {
pub fn exists(&self) -> bool {
self.enablement != UnitEnablement::NotFound
}
}
pub fn parse_unit_state(is_enabled_output: &str, is_active_output: &str) -> UnitState {
let last_line = |output: &str| {
output
.lines()
.map(str::trim)
.rfind(|line| !line.is_empty())
.unwrap_or_default()
.to_string()
};
let enablement = match last_line(is_enabled_output).as_str() {
"enabled" | "enabled-runtime" | "alias" | "linked" | "linked-runtime" => {
UnitEnablement::Enabled
}
"disabled" => UnitEnablement::Disabled,
"static" | "indirect" | "generated" | "transient" => UnitEnablement::Static,
"masked" | "masked-runtime" => UnitEnablement::Masked,
_ => UnitEnablement::NotFound,
};
let active = matches!(last_line(is_active_output).as_str(), "active" | "reloading");
UnitState { enablement, active }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum KernelModuleFlavor {
Open,
Proprietary,
}
impl fmt::Display for KernelModuleFlavor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KernelModuleFlavor::Open => write!(f, "open"),
KernelModuleFlavor::Proprietary => write!(f, "proprietary"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadedDriver {
pub version: String,
pub flavor: KernelModuleFlavor,
}
pub(crate) const PROC_DRIVER_VERSION: &str = "/proc/driver/nvidia/version";
pub fn parse_loaded_driver(proc_version: &str) -> Option<LoadedDriver> {
let line = proc_version
.lines()
.find(|line| line.trim_start().starts_with("NVRM version:"))?;
let version = line.split_whitespace().find(|token| {
let parts: Vec<&str> = token.split('.').collect();
parts.len() >= 2
&& parts
.iter()
.all(|part| !part.is_empty() && part.chars().all(|c| c.is_ascii_digit()))
})?;
Some(LoadedDriver {
version: version.to_string(),
flavor: if line.contains("Open Kernel Module") {
KernelModuleFlavor::Open
} else {
KernelModuleFlavor::Proprietary
},
})
}
pub(crate) async fn loaded_driver<Handler: HostHandler>(
host_handler: &mut Handler,
privilege: &Privilege,
) -> Result<Option<LoadedDriver>, RegentError> {
let result = query(
host_handler,
"ed_command(&["cat", PROC_DRIVER_VERSION]),
privilege,
)
.await?;
if result.return_code != 0 {
return Ok(None);
}
Ok(parse_loaded_driver(&result.stdout))
}
pub(crate) fn check_linux_host(
host_properties: &HostProperties,
attribute: &str,
) -> Result<(), RegentError> {
match host_properties.os_kind() {
OsKind::Linux(specifics) => match specifics.linux_flavor {
LinuxFlavor::Arch | LinuxFlavor::Gentoo => Err(RegentError::IncompatibleHost(format!(
"Host is {:?} Linux but {} only supports distributions with an NVIDIA network repository (apt, dnf or zypper based)",
specifics.linux_flavor, attribute
))),
_ => Ok(()),
},
incompatible_os_kind => Err(RegentError::IncompatibleHost(format!(
"Host is {:?} but {} is only supported on Linux",
incompatible_os_kind, attribute
))),
}
}
pub(crate) async fn query<Handler: HostHandler>(
host_handler: &mut Handler,
command: &str,
privilege: &Privilege,
) -> Result<CommandResult, RegentError> {
host_handler
.run_command(command, privilege)
.await
.map_err(|e| {
RegentError::FailedDryRunEvaluation(format!("Unable to run {:?}: {:?}", command, e))
})
}
pub(crate) async fn query_output<Handler: HostHandler>(
host_handler: &mut Handler,
command: &str,
privilege: &Privilege,
) -> Result<String, RegentError> {
let result = query(host_handler, command, privilege).await?;
if result.return_code != 0 {
return Err(RegentError::FailedDryRunEvaluation(format!(
"{:?} failed (RC {}): {}{}",
command, result.return_code, result.stdout, result.stderr
)));
}
Ok(result.stdout)
}
pub(crate) async fn installed_packages<Handler: HostHandler>(
host_handler: &mut Handler,
family: PackageFamily,
patterns: &[&str],
privilege: &Privilege,
) -> Result<BTreeMap<String, String>, RegentError> {
let result = query(
host_handler,
&family.installed_packages_command(patterns),
privilege,
)
.await?;
Ok(parse_installed_packages(&result.stdout))
}
pub(crate) async fn is_repository_configured<Handler: HostHandler>(
host_handler: &mut Handler,
family: PackageFamily,
url_fragment: &str,
privilege: &Privilege,
) -> Result<bool, RegentError> {
let result = query(
host_handler,
&family.repository_check_command(url_fragment),
privilege,
)
.await?;
Ok(result.return_code == 0 && !result.stdout.trim().is_empty())
}
pub(crate) async fn unit_state<Handler: HostHandler>(
host_handler: &mut Handler,
unit: &str,
privilege: &Privilege,
) -> Result<UnitState, RegentError> {
let is_enabled = query(
host_handler,
"ed_command(&["systemctl", "is-enabled", unit]),
privilege,
)
.await?;
let is_active = query(
host_handler,
"ed_command(&["systemctl", "is-active", unit]),
privilege,
)
.await?;
Ok(parse_unit_state(&is_enabled.stdout, &is_active.stdout))
}
pub(crate) async fn loaded_kernel_modules<Handler: HostHandler>(
host_handler: &mut Handler,
privilege: &Privilege,
) -> Result<Vec<String>, RegentError> {
let modules = query_output(
host_handler,
"ed_command(&["cat", "/proc/modules"]),
privilege,
)
.await?;
Ok(parse_proc_modules(&modules))
}
pub fn parse_proc_modules(content: &str) -> Vec<String> {
content
.lines()
.filter_map(|line| line.split_whitespace().next())
.map(str::to_string)
.collect()
}
pub(crate) async fn host_package_family<Handler: HostHandler>(
host_handler: &mut Handler,
privilege: &Privilege,
) -> Result<Option<PackageFamily>, RegentError> {
let os_release = parse_os_release(
&query_output(
host_handler,
"ed_command(&["cat", "/etc/os-release"]),
privilege,
)
.await?,
);
Ok(package_family(&os_release))
}
pub(crate) async fn download<Handler: HostHandler>(
host_handler: &mut Handler,
url: &str,
destination: &str,
privilege: &Privilege,
) -> InternalApiCallOutcome {
let has_curl = host_handler
.is_this_command_available("curl", privilege)
.await
.unwrap_or(false);
let command = if has_curl {
quoted_command(&["curl", "-fsSL", "-o", destination, url])
} else {
quoted_command(&["wget", "-q", "-O", destination, url])
};
run_step(host_handler, &command, privilege).await
}
pub(crate) async fn make_temp_dir<Handler: HostHandler>(
host_handler: &mut Handler,
privilege: &Privilege,
) -> Result<Result<String, InternalApiCallOutcome>, RegentError> {
let result = query(host_handler, "ed_command(&["mktemp", "-d"]), privilege).await?;
let path = result.stdout.trim();
if result.return_code != 0 || !path.starts_with('/') || path.contains(char::is_whitespace) {
return Ok(Err(InternalApiCallOutcome::Failure(format!(
"mktemp -d failed (RC {}): {}{}",
result.return_code, result.stdout, result.stderr
))));
}
Ok(Ok(path.to_string()))
}
pub(crate) async fn remove_temp_dir<Handler: HostHandler>(
host_handler: &mut Handler,
path: &str,
privilege: &Privilege,
) {
let _ = host_handler
.run_command("ed_command(&["rm", "-rf", path]), privilege)
.await;
}
pub(crate) async fn add_repo_file<Handler: HostHandler>(
host_handler: &mut Handler,
family: PackageFamily,
repo_file_url: &str,
repo_file_name: &str,
privilege: &Privilege,
) -> Result<InternalApiCallOutcome, RegentError> {
Ok(match family {
PackageFamily::Apt => InternalApiCallOutcome::Failure(
"apt repositories are not described by a .repo file".to_string(),
),
PackageFamily::Dnf => {
let outcome = download(
host_handler,
repo_file_url,
&format!("/etc/yum.repos.d/{}", repo_file_name),
privilege,
)
.await;
if !is_success(&outcome) {
return Ok(outcome);
}
run_step(
host_handler,
"ed_command(&["dnf", "clean", "expire-cache"]),
privilege,
)
.await
}
PackageFamily::Zypper => {
run_steps(
host_handler,
&[
quoted_command(&["zypper", "--non-interactive", "addrepo", repo_file_url]),
quoted_command(&[
"zypper",
"--non-interactive",
"--gpg-auto-import-keys",
"refresh",
]),
],
privilege,
)
.await
}
})
}
pub(crate) fn is_success(outcome: &InternalApiCallOutcome) -> bool {
!matches!(outcome, InternalApiCallOutcome::Failure(_))
}
pub(crate) async fn run_step<Handler: HostHandler>(
host_handler: &mut Handler,
command: &str,
privilege: &Privilege,
) -> InternalApiCallOutcome {
match host_handler.run_command(command, privilege).await {
Ok(result) if result.return_code == 0 => InternalApiCallOutcome::Success(None),
Ok(result) => InternalApiCallOutcome::Failure(format!(
"RC: {}, STDOUT: {}, STDERR: {}",
result.return_code, result.stdout, result.stderr
)),
Err(e) => InternalApiCallOutcome::Failure(format!("Command execution failed: {:?}", e)),
}
}
pub(crate) async fn run_steps<Handler: HostHandler>(
host_handler: &mut Handler,
commands: &[String],
privilege: &Privilege,
) -> InternalApiCallOutcome {
for command in commands {
let outcome = run_step(host_handler, command, privilege).await;
if let InternalApiCallOutcome::Failure(_) = outcome {
return outcome;
}
}
InternalApiCallOutcome::Success(None)
}
pub(crate) fn is_container_toolkit_package(name: &str) -> bool {
name.starts_with("nvidia-container") || name.starts_with("libnvidia-container")
}