1use crate::error::RegentError;
140use crate::hosts::managed_host::InternalApiCallOutcome;
141use crate::hosts::managed_host::{AssessCompliance, ReachCompliance, Timeout};
142use crate::hosts::properties::HostProperties;
143use crate::secrets::SecretProvidersPool;
144use crate::state::Check;
145use crate::state::attribute::HostHandler;
146use crate::state::attribute::Privilege;
147use crate::state::attribute::Remediation;
148use crate::state::attribute::RemediationsList;
149use crate::state::attribute::container::container_spec::quoted_command;
150use crate::state::compliance::AttributeComplianceAssessment;
151use serde::{Deserialize, Serialize};
152use std::collections::BTreeMap;
153use std::fmt;
154use std::str::FromStr;
155use std::time::Duration;
156
157use crate::attribute::shared::{
158 CudaRepository, DriverDistro, KernelModuleFlavor, LoadedDriver, PackageFamily, UnitEnablement,
159 UnitState, check_linux_host, cuda_repository, installed_packages, is_container_toolkit_package,
160 is_repository_configured, is_success, loaded_driver, loaded_kernel_modules, parse_os_release,
161 query, query_output, run_step, run_steps, unit_state, upstream_version,
162};
163
164const ATTRIBUTE: &str = "NvidiaDriver";
165
166const FIRST_SUPPORTED_BRANCH: u32 = 570;
168
169const LAST_PROPRIETARY_BRANCH: u32 = 610;
171
172const PERSISTENCE_DAEMON: &str = "nvidia-persistenced";
174
175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
176#[serde(deny_unknown_fields)]
177#[serde(rename_all = "PascalCase")]
178pub struct NvidiaDriverExpectedState {
179 state: NvidiaDriverState,
180}
181
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183#[serde(rename_all = "PascalCase")]
184pub enum NvidiaDriverState {
185 Present(Box<NvidiaDriverConfig>),
187 Absent,
188}
189
190impl NvidiaDriverExpectedState {
191 pub fn present(config: NvidiaDriverConfig) -> Self {
193 Self {
194 state: NvidiaDriverState::Present(Box::new(config)),
195 }
196 }
197
198 pub fn absent() -> Self {
199 Self {
200 state: NvidiaDriverState::Absent,
201 }
202 }
203}
204
205#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
207#[serde(deny_unknown_fields)]
208#[serde(rename_all = "PascalCase")]
209pub struct NvidiaDriverConfig {
210 #[serde(default)]
212 kernel_modules: KernelModules,
213 cuda_toolkit: Option<CudaVersion>,
215 persistence_mode: Option<bool>,
217}
218
219impl NvidiaDriverConfig {
220 pub fn new() -> Self {
222 Self::default()
223 }
224
225 pub fn with_kernel_modules(mut self, kernel_modules: KernelModules) -> Self {
226 self.kernel_modules = kernel_modules;
227 self
228 }
229
230 pub fn with_latest_open_kernel_modules(self) -> Self {
232 self.with_kernel_modules(KernelModules::Open(DriverBranch::Latest))
233 }
234
235 pub fn with_open_kernel_modules(self, branch: u32) -> Result<Self, RegentError> {
237 let branch = BranchNumber::try_from(branch)?;
238 Ok(self.with_kernel_modules(KernelModules::Open(DriverBranch::Branch(branch))))
239 }
240
241 pub fn with_proprietary_kernel_modules(self, branch: u32) -> Result<Self, RegentError> {
243 let branch = ProprietaryBranch::try_from(branch)?;
244 Ok(self.with_kernel_modules(KernelModules::Proprietary(branch)))
245 }
246
247 pub fn with_cuda_toolkit(mut self, cuda_toolkit: CudaVersion) -> Self {
248 self.cuda_toolkit = Some(cuda_toolkit);
249 self
250 }
251
252 pub fn with_persistence_mode(mut self, persistence_mode: bool) -> Self {
253 self.persistence_mode = Some(persistence_mode);
254 self
255 }
256}
257
258#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
260#[serde(rename_all = "PascalCase")]
261pub enum KernelModules {
262 Open(DriverBranch),
264 Proprietary(ProprietaryBranch),
267}
268
269impl Default for KernelModules {
270 fn default() -> Self {
271 KernelModules::Open(DriverBranch::Latest)
272 }
273}
274
275impl KernelModules {
276 pub fn flavor(&self) -> KernelModuleFlavor {
277 match self {
278 KernelModules::Open(_) => KernelModuleFlavor::Open,
279 KernelModules::Proprietary(_) => KernelModuleFlavor::Proprietary,
280 }
281 }
282
283 pub fn branch(&self) -> Option<u32> {
285 match self {
286 KernelModules::Open(DriverBranch::Latest) => None,
287 KernelModules::Open(DriverBranch::Branch(branch)) => Some(branch.number()),
288 KernelModules::Proprietary(branch) => Some(branch.number()),
289 }
290 }
291
292 pub fn package(&self) -> &'static str {
294 match self {
295 KernelModules::Open(_) => OPEN_PACKAGE,
296 KernelModules::Proprietary(_) => PROPRIETARY_PACKAGE,
297 }
298 }
299
300 fn install_package(&self, distro: DriverDistro) -> String {
305 match (distro, self.branch()) {
306 (DriverDistro::Ubuntu | DriverDistro::Debian, Some(branch)) => match self.flavor() {
307 KernelModuleFlavor::Proprietary => format!("nvidia-driver-{}", branch),
308 KernelModuleFlavor::Open => format!("nvidia-driver-{}-open", branch),
309 },
310 _ => self.package().to_string(),
311 }
312 }
313
314 fn branch_selection(&self, distro: DriverDistro) -> Option<BranchSelection> {
317 let branch = self.branch();
318 match distro {
319 DriverDistro::Ubuntu | DriverDistro::Debian => None,
320 DriverDistro::RhelModular | DriverDistro::AmazonLinux => {
321 Some(BranchSelection::ModuleStream(Some(match self {
322 KernelModules::Open(DriverBranch::Latest) => "open-dkms".to_string(),
323 KernelModules::Open(DriverBranch::Branch(branch)) => {
324 format!("{}-open", branch)
325 }
326 KernelModules::Proprietary(branch) => format!("{}-dkms", branch),
327 })))
328 }
329 DriverDistro::Rhel10 | DriverDistro::Fedora => {
330 Some(BranchSelection::VersionLock(branch))
331 }
332 DriverDistro::Suse => Some(BranchSelection::ZypperLock(branch)),
333 }
334 }
335}
336
337fn unpinned_selection(distro: DriverDistro) -> Option<BranchSelection> {
339 match distro {
340 DriverDistro::Ubuntu | DriverDistro::Debian => None,
341 DriverDistro::RhelModular | DriverDistro::AmazonLinux => {
342 Some(BranchSelection::ModuleStream(None))
343 }
344 DriverDistro::Rhel10 | DriverDistro::Fedora => Some(BranchSelection::VersionLock(None)),
345 DriverDistro::Suse => Some(BranchSelection::ZypperLock(None)),
346 }
347}
348
349#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
351#[serde(try_from = "RawBranch", into = "RawBranch")]
352pub enum DriverBranch {
353 Latest,
355 Branch(BranchNumber),
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
361#[serde(untagged)]
362enum RawBranch {
363 Number(u32),
364 Text(String),
365}
366
367impl TryFrom<RawBranch> for DriverBranch {
368 type Error = RegentError;
369
370 fn try_from(raw: RawBranch) -> Result<Self, Self::Error> {
371 match raw {
372 RawBranch::Number(number) => Ok(DriverBranch::Branch(BranchNumber::try_from(number)?)),
373 RawBranch::Text(text) => text.parse(),
374 }
375 }
376}
377
378impl From<DriverBranch> for RawBranch {
379 fn from(branch: DriverBranch) -> RawBranch {
380 match branch {
381 DriverBranch::Latest => RawBranch::Text("Latest".to_string()),
382 DriverBranch::Branch(branch) => RawBranch::Number(branch.number()),
383 }
384 }
385}
386
387impl FromStr for DriverBranch {
388 type Err = RegentError;
389
390 fn from_str(text: &str) -> Result<Self, Self::Err> {
391 if text.eq_ignore_ascii_case("latest") {
392 Ok(DriverBranch::Latest)
393 } else {
394 Ok(DriverBranch::Branch(text.parse()?))
395 }
396 }
397}
398
399impl fmt::Display for DriverBranch {
400 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
401 match self {
402 DriverBranch::Latest => write!(f, "Latest"),
403 DriverBranch::Branch(branch) => write!(f, "{}", branch),
404 }
405 }
406}
407
408#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
410#[serde(try_from = "u32", into = "u32")]
411pub struct BranchNumber(u32);
412
413impl TryFrom<u32> for BranchNumber {
414 type Error = RegentError;
415
416 fn try_from(number: u32) -> Result<Self, Self::Error> {
417 if (FIRST_SUPPORTED_BRANCH..1000).contains(&number) {
418 Ok(Self(number))
419 } else {
420 Err(invalid_value(
421 "NVIDIA driver branch",
422 &number.to_string(),
423 &format!(
424 "branches from {} on are supported (older branches use other package names)",
425 FIRST_SUPPORTED_BRANCH
426 ),
427 ))
428 }
429 }
430}
431
432impl FromStr for BranchNumber {
433 type Err = RegentError;
434
435 fn from_str(text: &str) -> Result<Self, Self::Err> {
436 let number = text.trim().parse::<u32>().map_err(|_| {
437 invalid_value(
438 "NVIDIA driver branch",
439 text,
440 "expected Latest or a branch number such as 580",
441 )
442 })?;
443 Self::try_from(number)
444 }
445}
446
447impl From<BranchNumber> for u32 {
448 fn from(branch: BranchNumber) -> u32 {
449 branch.0
450 }
451}
452
453impl fmt::Display for BranchNumber {
454 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
455 write!(f, "{}", self.0)
456 }
457}
458
459impl BranchNumber {
460 pub fn number(&self) -> u32 {
461 self.0
462 }
463}
464
465#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
467#[serde(try_from = "RawBranch", into = "u32")]
468pub struct ProprietaryBranch(BranchNumber);
469
470impl TryFrom<RawBranch> for ProprietaryBranch {
471 type Error = RegentError;
472
473 fn try_from(raw: RawBranch) -> Result<Self, Self::Error> {
474 match raw {
475 RawBranch::Number(number) => Self::try_from(number),
476 RawBranch::Text(text) => text.parse(),
477 }
478 }
479}
480
481impl TryFrom<u32> for ProprietaryBranch {
482 type Error = RegentError;
483
484 fn try_from(number: u32) -> Result<Self, Self::Error> {
485 let branch = BranchNumber::try_from(number)?;
486 if number <= LAST_PROPRIETARY_BRANCH {
487 Ok(Self(branch))
488 } else {
489 Err(invalid_value(
490 "proprietary kernel modules branch",
491 &number.to_string(),
492 &format!(
493 "the proprietary kernel modules were removed after branch {}, use the open kernel modules",
494 LAST_PROPRIETARY_BRANCH
495 ),
496 ))
497 }
498 }
499}
500
501impl FromStr for ProprietaryBranch {
502 type Err = RegentError;
503
504 fn from_str(text: &str) -> Result<Self, Self::Err> {
505 if text.eq_ignore_ascii_case("latest") {
506 return Err(invalid_value(
507 "proprietary kernel modules branch",
508 text,
509 &format!(
510 "the proprietary kernel modules need a pinned branch (570 to {}): the latest branches only ship the open kernel modules",
511 LAST_PROPRIETARY_BRANCH
512 ),
513 ));
514 }
515 Self::try_from(BranchNumber::from_str(text)?.number())
516 }
517}
518
519impl From<ProprietaryBranch> for u32 {
520 fn from(branch: ProprietaryBranch) -> u32 {
521 branch.0.number()
522 }
523}
524
525impl fmt::Display for ProprietaryBranch {
526 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
527 write!(f, "{}", self.0)
528 }
529}
530
531impl ProprietaryBranch {
532 pub fn new(branch: u32) -> Self {
539 Self::try_from(branch).unwrap_or_else(|error| panic!("{error}"))
540 }
541
542 pub fn number(&self) -> u32 {
543 self.0.number()
544 }
545}
546
547#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
549#[serde(try_from = "String", into = "String")]
550pub struct CudaVersion {
551 major: u16,
552 minor: u16,
553}
554
555impl TryFrom<String> for CudaVersion {
556 type Error = RegentError;
557
558 fn try_from(text: String) -> Result<Self, Self::Error> {
559 let parse = |part: &str| {
560 (!part.is_empty() && part.len() <= 2 && part.chars().all(|c| c.is_ascii_digit()))
561 .then(|| part.parse::<u16>().ok())
562 .flatten()
563 };
564 match text.split_once('.') {
565 Some((major, minor)) => match (parse(major), parse(minor)) {
566 (Some(major), Some(minor)) if major >= 10 => Ok(Self { major, minor }),
567 _ => Err(invalid_cuda_version(&text)),
568 },
569 None => Err(invalid_cuda_version(&text)),
570 }
571 }
572}
573
574fn invalid_cuda_version(text: &str) -> RegentError {
575 invalid_value(
576 "CUDA toolkit version",
577 text,
578 "expected <major>.<minor> such as 12.8 (quote it in YAML: \"12.10\")",
579 )
580}
581
582impl FromStr for CudaVersion {
583 type Err = RegentError;
584
585 fn from_str(text: &str) -> Result<Self, Self::Err> {
586 Self::try_from(text.to_string())
587 }
588}
589
590impl From<CudaVersion> for String {
591 fn from(version: CudaVersion) -> String {
592 version.to_string()
593 }
594}
595
596impl fmt::Display for CudaVersion {
597 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
598 write!(f, "{}.{}", self.major, self.minor)
599 }
600}
601
602impl CudaVersion {
603 pub fn package(&self) -> String {
605 format!("cuda-toolkit-{}-{}", self.major, self.minor)
606 }
607}
608
609fn invalid_value(what: &str, value: &str, details: &str) -> RegentError {
610 RegentError::IncoherentExpectedState(format!("Invalid {} {:?}: {}", what, value, details))
611}
612
613#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
615#[serde(rename_all = "PascalCase")]
616pub enum BranchSelection {
617 ModuleStream(Option<String>),
619 VersionLock(Option<u32>),
621 ZypperLock(Option<u32>),
623}
624
625impl fmt::Display for BranchSelection {
626 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
627 match self {
628 BranchSelection::ModuleStream(Some(stream)) => {
629 write!(f, "enable DNF module stream nvidia-driver:{}", stream)
630 }
631 BranchSelection::ModuleStream(None) => write!(f, "reset the nvidia-driver DNF module"),
632 BranchSelection::VersionLock(Some(branch)) => {
633 write!(
634 f,
635 "lock the NVIDIA packages to branch {} (dnf versionlock)",
636 branch
637 )
638 }
639 BranchSelection::VersionLock(None) => {
640 write!(f, "remove the NVIDIA package version locks")
641 }
642 BranchSelection::ZypperLock(Some(branch)) => {
643 write!(f, "lock the NVIDIA packages to branch {} (zypper)", branch)
644 }
645 BranchSelection::ZypperLock(None) => write!(f, "remove the NVIDIA package locks"),
646 }
647 }
648}
649
650#[derive(Debug, Clone, PartialEq, Eq)]
652enum CurrentSelection {
653 ModuleStream(Option<String>),
655 VersionLocks(Vec<LockedPackage>),
657 ZypperLocks(Vec<ZypperLock>),
659}
660
661impl CurrentSelection {
662 fn matches(&self, expected: &BranchSelection) -> bool {
663 match (self, expected) {
664 (CurrentSelection::ModuleStream(current), BranchSelection::ModuleStream(stream)) => {
665 current == stream
666 }
667 (CurrentSelection::VersionLocks(locks), BranchSelection::VersionLock(branch)) => {
668 match branch {
669 None => locks.is_empty(),
670 Some(branch) => {
671 !locks.is_empty()
672 && locks
673 .iter()
674 .all(|lock| version_branch(&lock.version) == Some(*branch))
675 }
676 }
677 }
678 (CurrentSelection::ZypperLocks(locks), BranchSelection::ZypperLock(branch)) => {
679 match branch {
680 None => locks.is_empty(),
681 Some(branch) => {
682 locks.len() == 1 && locks[0].version == zypper_lock_condition(*branch)
683 }
684 }
685 }
686 _ => false,
687 }
688 }
689
690 fn to_undo(&self) -> Vec<String> {
692 match self {
693 CurrentSelection::ModuleStream(_) => Vec::new(),
694 CurrentSelection::VersionLocks(locks) => {
695 let mut names: Vec<String> = locks.iter().map(|lock| lock.name.clone()).collect();
696 names.dedup();
697 names
698 }
699 CurrentSelection::ZypperLocks(locks) => locks.iter().map(ZypperLock::spec).collect(),
700 }
701 }
702}
703
704fn version_branch(version: &str) -> Option<u32> {
706 upstream_version(version)
707 .split('.')
708 .next()
709 .and_then(|major| major.parse().ok())
710}
711
712fn is_in_branch(version: &str, branch: Option<u32>) -> bool {
713 branch.is_none_or(|branch| version_branch(version) == Some(branch))
714}
715
716#[derive(Debug, Clone, PartialEq, Eq)]
718struct LockedPackage {
719 name: String,
720 version: String,
722}
723
724fn parse_versionlock_list(output: &str) -> Vec<LockedPackage> {
728 let mut locks = Vec::new();
729 let mut dnf5_name: Option<String> = None;
730 for line in output.lines().map(str::trim) {
731 if let Some(name) = line.strip_prefix("Package name:") {
732 dnf5_name = Some(name.trim().to_string());
733 continue;
734 }
735 if let Some(name) = &dnf5_name
736 && line.starts_with("evr")
737 && let Some((_, version)) = line.split_once('=')
738 {
739 locks.push(LockedPackage {
740 name: name.clone(),
741 version: version.trim().to_string(),
742 });
743 continue;
744 }
745 if line.is_empty() || line.starts_with('#') || line.starts_with('!') || line.contains(' ') {
746 continue;
747 }
748 let entry = line.trim_end_matches(".*");
750 let Some((name_and_epoch, version_release)) = entry.split_once(':') else {
751 continue;
752 };
753 if let Some((name, _epoch)) = name_and_epoch.rsplit_once('-') {
754 locks.push(LockedPackage {
755 name: name.to_string(),
756 version: version_release.to_string(),
757 });
758 }
759 }
760 locks.retain(|lock| lock.name.contains("nvidia") && !is_container_toolkit_package(&lock.name));
761 locks
762}
763
764#[derive(Debug, Clone, PartialEq, Eq)]
766struct ZypperLock {
767 name: String,
768 version: String,
770}
771
772impl ZypperLock {
773 fn spec(&self) -> String {
775 if self.version.is_empty() {
776 self.name.clone()
777 } else {
778 format!("{} {}", self.name, self.version)
779 }
780 }
781}
782
783const ZYPPER_LOCK_NAME: &str = "*nvidia*";
784
785fn zypper_lock_condition(branch: u32) -> String {
787 format!(">= {}", branch + 1)
788}
789
790fn parse_zypper_locks(content: &str) -> Vec<ZypperLock> {
792 let mut locks = Vec::new();
793 for block in content.split("\n\n") {
794 let mut name = None;
795 let mut version = String::new();
796 for line in block.lines() {
797 if let Some((key, value)) = line.split_once(':') {
798 match key.trim() {
799 "solvable_name" => name = Some(value.trim().to_string()),
800 "version" => version = value.split_whitespace().collect::<Vec<_>>().join(" "),
801 _ => {}
802 }
803 }
804 }
805 if let Some(name) = name
806 && name == ZYPPER_LOCK_NAME
807 {
808 locks.push(ZypperLock { name, version });
809 }
810 }
811 locks
812}
813
814fn parse_enabled_module_stream(output: &str) -> Option<String> {
816 output.lines().find_map(|line| {
817 let mut tokens = line.split_whitespace();
818 if tokens.next() != Some("nvidia-driver") {
819 return None;
820 }
821 let stream = tokens.next()?;
822 let markers: Vec<&str> = tokens.take_while(|token| token.starts_with('[')).collect();
823 markers
824 .iter()
825 .any(|marker| marker.contains("[e]"))
826 .then(|| stream.to_string())
827 })
828}
829
830fn branch_selection_commands(
832 selection: &BranchSelection,
833 distro: DriverDistro,
834 undo: &[String],
835) -> Vec<String> {
836 let mut commands = Vec::new();
837 match selection {
838 BranchSelection::ModuleStream(stream) => {
839 commands.push(quoted_command(&[
840 "dnf",
841 "-y",
842 "module",
843 "reset",
844 "nvidia-driver",
845 ]));
846 if let Some(stream) = stream {
847 commands.push(quoted_command(&[
848 "dnf",
849 "-y",
850 "module",
851 "enable",
852 &format!("nvidia-driver:{}", stream),
853 ]));
854 }
855 }
856 BranchSelection::VersionLock(branch) => {
857 if distro == DriverDistro::Rhel10 {
858 commands.push(quoted_command(&[
860 "dnf",
861 "-y",
862 "install",
863 "python3-dnf-plugin-versionlock",
864 ]));
865 }
866 if !undo.is_empty() {
867 let mut args = vec!["dnf", "versionlock", "delete"];
868 args.extend(undo.iter().map(String::as_str));
869 commands.push(quoted_command(&args));
870 }
871 if let Some(branch) = branch {
872 commands.push(quoted_command(&[
873 "dnf",
874 "versionlock",
875 "add",
876 &format!("*nvidia*{}*", branch),
877 ]));
878 }
879 }
880 BranchSelection::ZypperLock(branch) => {
881 for lock in undo {
882 commands.push(quoted_command(&[
883 "zypper",
884 "--non-interactive",
885 "removelock",
886 lock,
887 ]));
888 }
889 if let Some(branch) = branch {
890 commands.push(quoted_command(&[
891 "zypper",
892 "--non-interactive",
893 "addlock",
894 &format!("{} {}", ZYPPER_LOCK_NAME, zypper_lock_condition(*branch)),
895 ]));
896 }
897 }
898 }
899 commands
900}
901
902const OPEN_PACKAGE: &str = "nvidia-open";
904const PROPRIETARY_PACKAGE: &str = "cuda-drivers";
907
908#[derive(Debug, Clone, PartialEq, Eq)]
910enum InstalledDriver {
911 Expected,
913 Missing,
915 Different(String),
917}
918
919fn apt_driver_meta(name: &str) -> Option<(KernelModuleFlavor, Option<u32>)> {
924 match name {
925 PROPRIETARY_PACKAGE | "nvidia-driver" => Some((KernelModuleFlavor::Proprietary, None)),
926 OPEN_PACKAGE | "nvidia-driver-open" => Some((KernelModuleFlavor::Open, None)),
927 _ => name.strip_prefix("nvidia-driver-").and_then(|rest| {
928 let (branch, flavor) = if let Some(branch) = rest.strip_suffix("-server-open") {
929 (branch, KernelModuleFlavor::Open)
930 } else if let Some(branch) = rest.strip_suffix("-server") {
931 (branch, KernelModuleFlavor::Proprietary)
932 } else if let Some(branch) = rest.strip_suffix("-open") {
933 (branch, KernelModuleFlavor::Open)
934 } else {
935 (rest, KernelModuleFlavor::Proprietary)
936 };
937 branch
938 .parse::<u32>()
939 .ok()
940 .map(|branch| (flavor, Some(branch)))
941 }),
942 }
943}
944
945fn driver_meta_patterns(distro: DriverDistro) -> &'static [&'static str] {
948 match distro {
949 DriverDistro::Ubuntu | DriverDistro::Debian => &[
950 OPEN_PACKAGE,
951 PROPRIETARY_PACKAGE,
952 "nvidia-driver",
953 "nvidia-driver-open",
954 "nvidia-driver-[0-9]*",
955 ],
956 _ => &[OPEN_PACKAGE, PROPRIETARY_PACKAGE],
957 }
958}
959
960fn installed_driver_status(
962 installed: &BTreeMap<String, String>,
963 modules: &KernelModules,
964 distro: DriverDistro,
965) -> InstalledDriver {
966 let branch = modules.branch();
967 if matches!(distro, DriverDistro::Ubuntu | DriverDistro::Debian) {
968 let is_expected = |name: &str| {
972 apt_driver_meta(name).is_some_and(|(flavor, meta_branch)| {
973 flavor == modules.flavor() && meta_branch == branch
974 })
975 };
976 if installed.keys().any(|name| is_expected(name)) {
977 return InstalledDriver::Expected;
978 }
979 return match installed
980 .iter()
981 .find(|(name, _)| apt_driver_meta(name).is_some())
982 {
983 Some((name, version)) => InstalledDriver::Different(format!(
984 "{} {} is installed, expected {} ({} kernel modules)",
985 name,
986 version,
987 modules.install_package(distro),
988 modules.flavor()
989 )),
990 None => InstalledDriver::Missing,
991 };
992 }
993 let other_flavor = match modules.flavor() {
996 KernelModuleFlavor::Open => installed
997 .get(PROPRIETARY_PACKAGE)
998 .filter(|version| {
999 version_branch(version).is_some_and(|branch| branch <= LAST_PROPRIETARY_BRANCH)
1000 })
1001 .map(|version| {
1002 (
1003 PROPRIETARY_PACKAGE,
1004 version,
1005 KernelModuleFlavor::Proprietary,
1006 )
1007 }),
1008 KernelModuleFlavor::Proprietary => installed
1009 .get(OPEN_PACKAGE)
1010 .map(|version| (OPEN_PACKAGE, version, KernelModuleFlavor::Open)),
1011 };
1012 if let Some((package, version, flavor)) = other_flavor {
1013 return InstalledDriver::Different(format!(
1014 "{} {} ({} kernel modules) is installed, expected the {} kernel modules",
1015 package,
1016 version,
1017 flavor,
1018 modules.flavor()
1019 ));
1020 }
1021 match installed.get(modules.package()) {
1022 None => InstalledDriver::Missing,
1023 Some(version) if is_in_branch(version, branch) => InstalledDriver::Expected,
1024 Some(version) => InstalledDriver::Different(format!(
1025 "{} {} is installed, expected branch {}",
1026 modules.package(),
1027 version,
1028 branch.unwrap_or_default()
1029 )),
1030 }
1031}
1032
1033fn driver_package_patterns(distro: DriverDistro) -> &'static [&'static str] {
1035 match distro {
1036 DriverDistro::Ubuntu => &[
1037 "cuda-compat*",
1038 "cuda-drivers*",
1039 "libnvidia-cfg1*",
1040 "libnvidia-compute*",
1041 "libnvidia-decode*",
1042 "libnvidia-encode*",
1043 "libnvidia-extra*",
1044 "libnvidia-fbc1*",
1045 "libnvidia-gl*",
1046 "libnvidia-gpucomp*",
1047 "libnvidia-nscq*",
1048 "libnvsdm*",
1049 "libxnvctrl*",
1050 "nvidia-dkms*",
1051 "nvidia-driver*",
1052 "nvidia-fabricmanager*",
1053 "nvidia-firmware*",
1054 "nvidia-headless*",
1055 "nvidia-imex*",
1056 "nvidia-kernel-common*",
1057 "nvidia-kernel-source*",
1058 "nvidia-modprobe*",
1059 "nvidia-open*",
1060 "nvidia-persistenced*",
1061 "nvidia-settings*",
1062 "nvidia-utils*",
1063 "nvidia-compute-utils*",
1064 "nvidia-xconfig*",
1065 "xserver-xorg-video-nvidia*",
1066 ],
1067 DriverDistro::Debian => &[
1068 "cuda-compat*",
1069 "cuda-drivers*",
1070 "cuda-mps*",
1071 "firmware-nvidia-gsp*",
1072 "libcuda1*",
1073 "libcudadebugger1*",
1074 "libegl-nvidia0*",
1075 "libgles-nvidia1*",
1076 "libgles-nvidia2*",
1077 "libglx-nvidia0*",
1078 "libnvcuvid1*",
1079 "libnvidia-*",
1080 "libnvoptix1*",
1081 "libnvsdm*",
1082 "libxnvctrl*",
1083 "nvidia-alternative*",
1084 "nvidia-detect*",
1085 "nvidia-driver*",
1086 "nvidia-egl-common*",
1087 "nvidia-egl-icd*",
1088 "nvidia-fabricmanager*",
1089 "nvidia-imex*",
1090 "nvidia-kernel-*",
1091 "nvidia-legacy*",
1092 "nvidia-modprobe*",
1093 "nvidia-open*",
1094 "nvidia-opencl*",
1095 "nvidia-persistenced*",
1096 "nvidia-powerd*",
1097 "nvidia-settings*",
1098 "nvidia-smi*",
1099 "nvidia-support*",
1100 "nvidia-suspend-common*",
1101 "nvidia-vdpau-driver*",
1102 "nvidia-vulkan*",
1103 "nvidia-xconfig*",
1104 "xserver-xorg-video-nvidia*",
1105 ],
1106 DriverDistro::RhelModular
1107 | DriverDistro::AmazonLinux
1108 | DriverDistro::Rhel10
1109 | DriverDistro::Fedora => &[
1110 "cuda-drivers*",
1111 "kmod-nvidia*",
1112 "libnvidia*",
1113 "nvidia-driver*",
1114 "nvidia-kmod-common",
1115 "nvidia-libXNVCtrl*",
1116 "nvidia-modprobe",
1117 "nvidia-open*",
1118 "nvidia-persistenced",
1119 "nvidia-settings",
1120 "nvidia-xconfig",
1121 "xorg-x11-nvidia*",
1122 ],
1123 DriverDistro::Suse => &["cuda-compat*", "cuda-drivers*", "libnvidia-*", "nvidia-*"],
1124 }
1125}
1126
1127fn driver_packages_to_remove<'a>(installed: impl Iterator<Item = &'a String>) -> Vec<String> {
1129 installed
1130 .filter(|name| !is_container_toolkit_package(name) && !name.starts_with("cuda-toolkit"))
1131 .cloned()
1132 .collect()
1133}
1134
1135#[derive(Debug, Clone, PartialEq, Eq)]
1137enum SmiStatus {
1138 Working(String),
1140 VersionMismatch,
1142 Failing(String),
1144}
1145
1146fn smi_command() -> String {
1147 quoted_command(&[
1148 "nvidia-smi",
1149 "--query-gpu=driver_version",
1150 "--format=csv,noheader",
1151 ])
1152}
1153
1154fn parse_smi(return_code: i64, output: &str) -> SmiStatus {
1155 if output.contains("Driver/library version mismatch") {
1156 return SmiStatus::VersionMismatch;
1157 }
1158 let version = output
1159 .lines()
1160 .map(str::trim)
1161 .find(|line| version_branch(line).is_some() && line.contains('.'));
1162 match (return_code, version) {
1163 (0, Some(version)) => SmiStatus::Working(version.to_string()),
1164 _ => SmiStatus::Failing(output.trim().to_string()),
1165 }
1166}
1167
1168fn runtime_issue(
1170 loaded: Option<&LoadedDriver>,
1171 smi: &SmiStatus,
1172 flavor: KernelModuleFlavor,
1173 branch: Option<u32>,
1174) -> Option<String> {
1175 let Some(loaded) = loaded else {
1176 return Some("the NVIDIA kernel module is not loaded".to_string());
1177 };
1178 if loaded.flavor != flavor {
1179 return Some(format!(
1180 "the {} kernel module is loaded, expected the {} one",
1181 loaded.flavor, flavor
1182 ));
1183 }
1184 if !is_in_branch(&loaded.version, branch) {
1185 return Some(format!(
1186 "driver {} is loaded, expected branch {}",
1187 loaded.version,
1188 branch.unwrap_or_default()
1189 ));
1190 }
1191 match smi {
1192 SmiStatus::Working(_) => None,
1193 SmiStatus::VersionMismatch => Some(format!(
1194 "driver {} is loaded but the installed libraries are another version (Driver/library version mismatch)",
1195 loaded.version
1196 )),
1197 SmiStatus::Failing(output) => Some(format!("nvidia-smi fails: {}", output)),
1198 }
1199}
1200
1201const PCI_DEVICES_COMMAND: &str =
1204 "grep -H . /sys/bus/pci/devices/*/vendor /sys/bus/pci/devices/*/class";
1205
1206fn has_nvidia_gpu(pci_devices: &str) -> bool {
1209 let mut devices: BTreeMap<&str, (Option<&str>, Option<&str>)> = BTreeMap::new();
1210 for line in pci_devices.lines() {
1211 let Some((path, value)) = line.trim().rsplit_once(':') else {
1212 continue;
1213 };
1214 let Some((device, file)) = path.rsplit_once('/') else {
1215 continue;
1216 };
1217 let entry = devices.entry(device).or_default();
1218 match file {
1219 "vendor" => entry.0 = Some(value),
1220 "class" => entry.1 = Some(value),
1221 _ => {}
1222 }
1223 }
1224 devices.values().any(|(vendor, class)| {
1225 *vendor == Some("0x10de") && class.is_some_and(|class| class.starts_with("0x03"))
1226 })
1227}
1228
1229fn parse_secure_boot(output: &str) -> Option<bool> {
1231 if output.contains("SecureBoot enabled") {
1232 Some(true)
1233 } else if output.contains("SecureBoot disabled") {
1234 Some(false)
1235 } else {
1236 None
1237 }
1238}
1239
1240fn is_persistence_mode_set(state: &UnitState, persistence_mode: bool) -> bool {
1243 if persistence_mode {
1244 state.active
1245 && matches!(
1246 state.enablement,
1247 UnitEnablement::Enabled | UnitEnablement::Static
1248 )
1249 } else {
1250 !state.active && state.enablement != UnitEnablement::Enabled
1251 }
1252}
1253
1254fn persistence_mode_commands(state: &UnitState, persistence_mode: bool) -> Vec<String> {
1256 let mut commands = Vec::new();
1257 if persistence_mode {
1258 if state.enablement == UnitEnablement::Masked {
1259 commands.push(quoted_command(&["systemctl", "unmask", PERSISTENCE_DAEMON]));
1260 }
1261 if matches!(
1262 state.enablement,
1263 UnitEnablement::Disabled | UnitEnablement::Masked
1264 ) {
1265 commands.push(quoted_command(&["systemctl", "enable", PERSISTENCE_DAEMON]));
1266 }
1267 if !state.active {
1268 commands.push(quoted_command(&["systemctl", "start", PERSISTENCE_DAEMON]));
1269 }
1270 } else {
1271 if state.enablement == UnitEnablement::Enabled {
1272 commands.push(quoted_command(&[
1273 "systemctl",
1274 "disable",
1275 PERSISTENCE_DAEMON,
1276 ]));
1277 }
1278 if state.active {
1279 commands.push(quoted_command(&["systemctl", "stop", PERSISTENCE_DAEMON]));
1280 }
1281 }
1282 commands
1283}
1284
1285const NVIDIA_MODULES: [&str; 5] = [
1287 "nvidia_uvm",
1288 "nvidia_drm",
1289 "nvidia_modeset",
1290 "nvidia_peermem",
1291 "nvidia",
1292];
1293
1294struct DriverHost {
1296 repository: CudaRepository,
1297 kernel_release: String,
1299}
1300
1301async fn detect_host<Handler: HostHandler>(
1303 host_handler: &mut Handler,
1304 privilege: &Privilege,
1305) -> Result<Result<DriverHost, String>, RegentError> {
1306 let os_release =
1307 parse_os_release(&query_output(host_handler, "cat /etc/os-release", privilege).await?);
1308 let machine = query_output(host_handler, "uname -m", privilege).await?;
1309 let kernel_release = query_output(host_handler, "uname -r", privilege)
1310 .await?
1311 .trim()
1312 .to_string();
1313 if kernel_release.is_empty()
1314 || !kernel_release
1315 .chars()
1316 .all(|c| c.is_ascii_alphanumeric() || "._+~-".contains(c))
1317 {
1318 return Err(RegentError::FailedDryRunEvaluation(format!(
1319 "Unexpected kernel release (uname -r): {:?}",
1320 kernel_release
1321 )));
1322 }
1323 Ok(
1324 cuda_repository(&os_release, &machine).map(|repository| DriverHost {
1325 repository,
1326 kernel_release,
1327 }),
1328 )
1329}
1330
1331async fn current_selection<Handler: HostHandler>(
1332 host_handler: &mut Handler,
1333 selection: &BranchSelection,
1334 privilege: &Privilege,
1335) -> Result<CurrentSelection, RegentError> {
1336 match selection {
1337 BranchSelection::ModuleStream(_) => {
1338 let result = query(
1340 host_handler,
1341 "ed_command(&["dnf", "-q", "module", "list", "--enabled", "nvidia-driver"]),
1342 privilege,
1343 )
1344 .await?;
1345 Ok(CurrentSelection::ModuleStream(parse_enabled_module_stream(
1346 &result.stdout,
1347 )))
1348 }
1349 BranchSelection::VersionLock(_) => {
1350 let result = query(
1352 host_handler,
1353 "ed_command(&["dnf", "versionlock", "list"]),
1354 privilege,
1355 )
1356 .await?;
1357 let locks = if result.return_code == 0 {
1358 parse_versionlock_list(&result.stdout)
1359 } else {
1360 Vec::new()
1361 };
1362 Ok(CurrentSelection::VersionLocks(locks))
1363 }
1364 BranchSelection::ZypperLock(_) => {
1365 let result = query(
1366 host_handler,
1367 "ed_command(&["cat", "/etc/zypp/locks"]),
1368 privilege,
1369 )
1370 .await?;
1371 let locks = if result.return_code == 0 {
1372 parse_zypper_locks(&result.stdout)
1373 } else {
1374 Vec::new()
1375 };
1376 Ok(CurrentSelection::ZypperLocks(locks))
1377 }
1378 }
1379}
1380
1381impl Timeout for NvidiaDriverExpectedState {
1382 fn default_timeout(&self) -> Duration {
1383 Duration::from_secs(1800)
1385 }
1386}
1387
1388impl Check for NvidiaDriverExpectedState {
1389 fn check(&self) -> Result<(), RegentError> {
1390 Ok(())
1392 }
1393
1394 fn check_host_compatibility(
1395 &self,
1396 host_properties: &HostProperties,
1397 ) -> Result<(), RegentError> {
1398 check_linux_host(host_properties, ATTRIBUTE)
1399 }
1400}
1401
1402impl<Handler: HostHandler> AssessCompliance<Handler> for NvidiaDriverExpectedState {
1403 async fn assess_compliance(
1404 &self,
1405 host_handler: &mut Handler,
1406 host_properties: &Option<HostProperties>,
1407 privilege: &Privilege,
1408 _optional_secret_provider: &Option<SecretProvidersPool>,
1409 ) -> Result<AttributeComplianceAssessment, RegentError> {
1410 if let Some(props) = host_properties {
1411 self.check_host_compatibility(props)?;
1412 }
1413
1414 let host = match detect_host(host_handler, privilege).await? {
1415 Ok(host) => host,
1416 Err(reason) => return Ok(AttributeComplianceAssessment::NonCompliantFatal(reason)),
1417 };
1418 let distro = host.repository.distro;
1419 let family = distro.package_family();
1420
1421 let mut remediations: Vec<Remediation> = Vec::new();
1422 let mut push = |api_call: NvidiaDriverInternalApiCall| {
1423 remediations.push(Remediation::NvidiaDriver(NvidiaDriverApiCall::from(
1424 api_call,
1425 privilege.clone(),
1426 )));
1427 };
1428
1429 match &self.state {
1430 NvidiaDriverState::Absent => {
1431 let installed = installed_packages(
1432 host_handler,
1433 family,
1434 driver_package_patterns(distro),
1435 privilege,
1436 )
1437 .await?;
1438 let packages = driver_packages_to_remove(installed.keys());
1439 if !packages.is_empty() {
1440 push(NvidiaDriverInternalApiCall::RemoveDriver {
1441 distro,
1442 packages,
1443 reason: "the driver must be absent".to_string(),
1444 });
1445 }
1446 if let Some(selection) = unpinned_selection(distro) {
1447 let current = current_selection(host_handler, &selection, privilege).await?;
1448 if !current.matches(&selection) {
1449 push(NvidiaDriverInternalApiCall::SelectBranch {
1450 distro,
1451 selection,
1452 undo: current.to_undo(),
1453 });
1454 }
1455 }
1456 }
1457 NvidiaDriverState::Present(config) => {
1458 let modules = &config.kernel_modules;
1459 let branch = modules.branch();
1460
1461 if !is_repository_configured(
1462 host_handler,
1463 family,
1464 &host.repository.url_fragment(),
1465 privilege,
1466 )
1467 .await?
1468 {
1469 push(NvidiaDriverInternalApiCall::AddRepository(
1470 host.repository.clone(),
1471 ));
1472 }
1473
1474 let installed = installed_packages(
1475 host_handler,
1476 family,
1477 driver_meta_patterns(distro),
1478 privilege,
1479 )
1480 .await?;
1481 let installed_driver = installed_driver_status(&installed, modules, distro);
1482 let needs_install = installed_driver != InstalledDriver::Expected;
1483
1484 if let InstalledDriver::Different(reason) = installed_driver {
1486 let installed = installed_packages(
1487 host_handler,
1488 family,
1489 driver_package_patterns(distro),
1490 privilege,
1491 )
1492 .await?;
1493 push(NvidiaDriverInternalApiCall::RemoveDriver {
1494 distro,
1495 packages: driver_packages_to_remove(installed.keys()),
1496 reason,
1497 });
1498 }
1499
1500 if let Some(selection) = modules.branch_selection(distro) {
1502 let current = current_selection(host_handler, &selection, privilege).await?;
1503 if !current.matches(&selection) {
1504 push(NvidiaDriverInternalApiCall::SelectBranch {
1505 distro,
1506 selection,
1507 undo: current.to_undo(),
1508 });
1509 }
1510 }
1511
1512 if needs_install {
1513 let mut packages = vec![modules.install_package(distro)];
1514 packages.extend(distro.kernel_headers_packages(&host.kernel_release));
1515 push(NvidiaDriverInternalApiCall::InstallDriver { family, packages });
1516 }
1517
1518 if let Some(cuda_toolkit) = &config.cuda_toolkit {
1519 let package = cuda_toolkit.package();
1520 let installed =
1521 installed_packages(host_handler, family, &[&package], privilege).await?;
1522 if !installed.contains_key(&package) {
1523 push(NvidiaDriverInternalApiCall::InstallCudaToolkit {
1524 family,
1525 version: *cuda_toolkit,
1526 });
1527 }
1528 }
1529
1530 if needs_install {
1533 push(NvidiaDriverInternalApiCall::LoadKernelModule {
1534 flavor: modules.flavor(),
1535 branch,
1536 reason: format!("{} is being installed", modules.install_package(distro)),
1537 });
1538 } else {
1539 let loaded = loaded_driver(host_handler, privilege).await?;
1540 let smi = query(host_handler, &smi_command(), privilege).await?;
1541 let smi = parse_smi(smi.return_code, &smi.stdout);
1542 if let Some(reason) =
1543 runtime_issue(loaded.as_ref(), &smi, modules.flavor(), branch)
1544 {
1545 if loaded.is_none() {
1546 let pci_devices =
1547 query(host_handler, PCI_DEVICES_COMMAND, privilege).await?;
1548 if !has_nvidia_gpu(&pci_devices.stdout) {
1549 return Ok(AttributeComplianceAssessment::NonCompliantFatal(
1550 "the NVIDIA driver is installed but no NVIDIA GPU was found on the PCI bus (vendor 0x10de): the kernel module can't be loaded".to_string(),
1551 ));
1552 }
1553 }
1554 push(NvidiaDriverInternalApiCall::LoadKernelModule {
1555 flavor: modules.flavor(),
1556 branch,
1557 reason,
1558 });
1559 }
1560 }
1561
1562 if let Some(persistence_mode) = config.persistence_mode {
1563 let state = unit_state(host_handler, PERSISTENCE_DAEMON, privilege).await?;
1564 let is_compliant = is_persistence_mode_set(&state, persistence_mode);
1565 if !is_compliant {
1566 push(NvidiaDriverInternalApiCall::SetPersistenceMode(
1567 persistence_mode,
1568 ));
1569 }
1570 }
1571 }
1572 }
1573
1574 if remediations.is_empty() {
1575 Ok(AttributeComplianceAssessment::Compliant)
1576 } else {
1577 Ok(AttributeComplianceAssessment::NonCompliant(
1578 RemediationsList::from(remediations)?,
1579 ))
1580 }
1581 }
1582}
1583
1584#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1585#[serde(rename_all = "PascalCase", rename_all_fields = "PascalCase")]
1586pub enum NvidiaDriverInternalApiCall {
1587 AddRepository(CudaRepository),
1589 SelectBranch {
1591 distro: DriverDistro,
1592 selection: BranchSelection,
1593 undo: Vec<String>,
1594 },
1595 RemoveDriver {
1596 distro: DriverDistro,
1597 packages: Vec<String>,
1598 reason: String,
1599 },
1600 InstallDriver {
1602 family: PackageFamily,
1603 packages: Vec<String>,
1604 },
1605 InstallCudaToolkit {
1607 family: PackageFamily,
1608 version: CudaVersion,
1609 },
1610 LoadKernelModule {
1612 flavor: KernelModuleFlavor,
1613 branch: Option<u32>,
1614 reason: String,
1615 },
1616 SetPersistenceMode(bool),
1618}
1619
1620#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1621pub struct NvidiaDriverApiCall {
1622 pub api_call: NvidiaDriverInternalApiCall,
1623 privilege: Privilege,
1624}
1625
1626impl NvidiaDriverApiCall {
1627 pub fn display(&self) -> String {
1628 match &self.api_call {
1629 NvidiaDriverInternalApiCall::AddRepository(repository) => {
1630 format!("Add the NVIDIA CUDA repository {}", repository.base_url())
1631 }
1632 NvidiaDriverInternalApiCall::SelectBranch { selection, .. } => {
1633 format!("Select the NVIDIA driver branch: {}", selection)
1634 }
1635 NvidiaDriverInternalApiCall::RemoveDriver {
1636 packages, reason, ..
1637 } => {
1638 format!(
1639 "Remove the NVIDIA driver packages {} ({}); reboot to unload the kernel module",
1640 packages.join(", "),
1641 reason
1642 )
1643 }
1644 NvidiaDriverInternalApiCall::InstallDriver { packages, .. } => {
1645 format!("Install the NVIDIA driver: {}", packages.join(", "))
1646 }
1647 NvidiaDriverInternalApiCall::InstallCudaToolkit { version, .. } => {
1648 format!(
1649 "Install the CUDA toolkit {} ({})",
1650 version,
1651 version.package()
1652 )
1653 }
1654 NvidiaDriverInternalApiCall::LoadKernelModule { flavor, reason, .. } => format!(
1655 "Load the {} NVIDIA kernel module ({}); a reboot is required if it can't be loaded",
1656 flavor, reason
1657 ),
1658 NvidiaDriverInternalApiCall::SetPersistenceMode(true) => {
1659 format!("Enable and start {}", PERSISTENCE_DAEMON)
1660 }
1661 NvidiaDriverInternalApiCall::SetPersistenceMode(false) => {
1662 format!("Disable and stop {}", PERSISTENCE_DAEMON)
1663 }
1664 }
1665 }
1666
1667 fn from(api_call: NvidiaDriverInternalApiCall, privilege: Privilege) -> NvidiaDriverApiCall {
1668 NvidiaDriverApiCall {
1669 api_call,
1670 privilege,
1671 }
1672 }
1673}
1674
1675impl Timeout for NvidiaDriverApiCall {
1676 fn default_timeout(&self) -> Duration {
1677 match &self.api_call {
1678 NvidiaDriverInternalApiCall::InstallDriver { .. }
1679 | NvidiaDriverInternalApiCall::InstallCudaToolkit { .. } => Duration::from_secs(1800),
1680 NvidiaDriverInternalApiCall::RemoveDriver { .. } => Duration::from_secs(900),
1681 _ => Duration::from_secs(300),
1682 }
1683 }
1684}
1685
1686impl Check for NvidiaDriverApiCall {
1687 fn check(&self) -> Result<(), RegentError> {
1688 Ok(())
1689 }
1690
1691 fn check_host_compatibility(
1692 &self,
1693 host_properties: &HostProperties,
1694 ) -> Result<(), RegentError> {
1695 check_linux_host(host_properties, ATTRIBUTE)
1696 }
1697}
1698
1699impl<Handler: HostHandler> ReachCompliance<Handler> for NvidiaDriverApiCall {
1700 async fn call(
1701 &self,
1702 host_handler: &mut Handler,
1703 host_properties: &Option<HostProperties>,
1704 _optional_secret_provider: &Option<SecretProvidersPool>,
1705 ) -> Result<InternalApiCallOutcome, RegentError> {
1706 if let Some(props) = host_properties {
1707 self.check_host_compatibility(props)?;
1708 }
1709
1710 let privilege = &self.privilege;
1711 match &self.api_call {
1712 NvidiaDriverInternalApiCall::AddRepository(repository) => {
1713 repository.setup(host_handler, privilege).await
1714 }
1715 NvidiaDriverInternalApiCall::SelectBranch {
1716 distro,
1717 selection,
1718 undo,
1719 } => Ok(run_steps(
1720 host_handler,
1721 &branch_selection_commands(selection, *distro, undo),
1722 privilege,
1723 )
1724 .await),
1725 NvidiaDriverInternalApiCall::RemoveDriver {
1726 distro, packages, ..
1727 } => Ok(run_step(
1728 host_handler,
1729 &distro.package_family().remove_command(packages),
1730 privilege,
1731 )
1732 .await),
1733 NvidiaDriverInternalApiCall::InstallDriver { family, packages } => Ok(run_steps(
1734 host_handler,
1735 &family.install_commands(packages, false),
1736 privilege,
1737 )
1738 .await),
1739 NvidiaDriverInternalApiCall::InstallCudaToolkit { family, version } => Ok(run_steps(
1740 host_handler,
1741 &family.install_commands(&[version.package()], false),
1742 privilege,
1743 )
1744 .await),
1745 NvidiaDriverInternalApiCall::LoadKernelModule { flavor, branch, .. } => {
1746 load_kernel_module(host_handler, *flavor, *branch, privilege).await
1747 }
1748 NvidiaDriverInternalApiCall::SetPersistenceMode(persistence_mode) => {
1749 let state = unit_state(host_handler, PERSISTENCE_DAEMON, privilege).await?;
1750 let outcome = run_steps(
1751 host_handler,
1752 &persistence_mode_commands(&state, *persistence_mode),
1753 privilege,
1754 )
1755 .await;
1756 if !is_success(&outcome) {
1757 return Ok(outcome);
1758 }
1759 let state = unit_state(host_handler, PERSISTENCE_DAEMON, privilege).await?;
1760 if is_persistence_mode_set(&state, *persistence_mode) {
1761 Ok(outcome)
1762 } else {
1763 Ok(InternalApiCallOutcome::Failure(format!(
1764 "{} is still {:?} and {} after the change",
1765 PERSISTENCE_DAEMON,
1766 state.enablement,
1767 if state.active { "active" } else { "inactive" }
1768 )))
1769 }
1770 }
1771 }
1772 }
1773}
1774
1775async fn load_kernel_module<Handler: HostHandler>(
1777 host_handler: &mut Handler,
1778 flavor: KernelModuleFlavor,
1779 branch: Option<u32>,
1780 privilege: &Privilege,
1781) -> Result<InternalApiCallOutcome, RegentError> {
1782 let pci_devices = query(host_handler, PCI_DEVICES_COMMAND, privilege).await?;
1783 if !has_nvidia_gpu(&pci_devices.stdout) {
1784 return Ok(InternalApiCallOutcome::Failure(
1785 "No NVIDIA GPU found on the PCI bus (vendor 0x10de): the NVIDIA kernel module can't be loaded".to_string(),
1786 ));
1787 }
1788
1789 let persistenced = unit_state(host_handler, PERSISTENCE_DAEMON, privilege).await?;
1791 if persistenced.active {
1792 run_step(
1793 host_handler,
1794 "ed_command(&["systemctl", "stop", PERSISTENCE_DAEMON]),
1795 privilege,
1796 )
1797 .await;
1798 }
1799 let (mut problem, modprobe_output) = reload_kernel_module(host_handler, privilege).await?;
1800 if persistenced.active {
1801 run_step(
1802 host_handler,
1803 "ed_command(&["systemctl", "start", PERSISTENCE_DAEMON]),
1804 privilege,
1805 )
1806 .await;
1807 }
1808
1809 if problem.is_none() {
1810 let loaded = loaded_driver(host_handler, privilege).await?;
1811 let smi = query(host_handler, &smi_command(), privilege).await?;
1812 let smi = parse_smi(smi.return_code, &smi.stdout);
1813 problem = runtime_issue(loaded.as_ref(), &smi, flavor, branch);
1814 if problem.is_none() {
1815 let version = loaded.map(|loaded| loaded.version).unwrap_or_default();
1816 return Ok(InternalApiCallOutcome::Success(Some(format!(
1817 "NVIDIA driver {} loaded",
1818 version
1819 ))));
1820 }
1821 }
1822
1823 let mut message = format!(
1824 "Reboot required: {}. The host is not rebooted automatically.",
1825 problem.unwrap_or_default()
1826 );
1827 let is_signature_issue = modprobe_output.contains("Key was rejected")
1828 || modprobe_output.contains("Required key not available");
1829 let secure_boot = if host_handler
1830 .is_this_command_available("mokutil", privilege)
1831 .await
1832 .unwrap_or(false)
1833 {
1834 let output = query(
1835 host_handler,
1836 "ed_command(&["mokutil", "--sb-state"]),
1837 privilege,
1838 )
1839 .await?;
1840 parse_secure_boot(&format!("{}{}", output.stdout, output.stderr))
1841 } else {
1842 None
1843 };
1844 if is_signature_issue || secure_boot == Some(true) {
1845 message.push_str(
1846 " Secure Boot is enabled: the DKMS modules must be signed with a key enrolled in the MOK database (mokutil --import, then confirm on the console at boot).",
1847 );
1848 }
1849 Ok(InternalApiCallOutcome::Failure(message))
1850}
1851
1852async fn reload_kernel_module<Handler: HostHandler>(
1855 host_handler: &mut Handler,
1856 privilege: &Privilege,
1857) -> Result<(Option<String>, String), RegentError> {
1858 let loaded_modules = loaded_kernel_modules(host_handler, privilege).await?;
1859 let is_loaded = |module: &str| loaded_modules.iter().any(|loaded| loaded == module);
1860 for module in NVIDIA_MODULES
1861 .into_iter()
1862 .filter(|module| is_loaded(module))
1863 {
1864 let result = query(
1865 host_handler,
1866 "ed_command(&["modprobe", "-r", module]),
1867 privilege,
1868 )
1869 .await?;
1870 if result.return_code != 0 {
1871 return Ok((
1872 Some(format!(
1873 "the loaded NVIDIA kernel module {} is in use and can't be replaced",
1874 module
1875 )),
1876 format!("{}{}", result.stdout, result.stderr),
1877 ));
1878 }
1879 }
1880 if is_loaded("nouveau") {
1881 let result = query(
1882 host_handler,
1883 "ed_command(&["modprobe", "-r", "nouveau"]),
1884 privilege,
1885 )
1886 .await?;
1887 if result.return_code != 0 {
1888 return Ok((
1889 Some("the nouveau driver is in use and can't be unloaded".to_string()),
1890 format!("{}{}", result.stdout, result.stderr),
1891 ));
1892 }
1893 }
1894 for module in ["nvidia", "nvidia_uvm"] {
1895 let result = query(
1896 host_handler,
1897 "ed_command(&["modprobe", module]),
1898 privilege,
1899 )
1900 .await?;
1901 let output = format!("{}{}", result.stdout, result.stderr);
1902 if result.return_code != 0 {
1903 return Ok((
1904 Some(format!("modprobe {} failed: {}", module, output.trim())),
1905 output,
1906 ));
1907 }
1908 }
1909 Ok((None, String::new()))
1910}
1911
1912#[cfg(test)]
1913mod tests {
1914 use super::*;
1915
1916 fn full_yaml() -> &'static str {
1917 "---
1918State: !Present
1919 KernelModules: !Open 580
1920 CudaToolkit: '12.8'
1921 PersistenceMode: true
1922"
1923 }
1924
1925 fn full_config() -> NvidiaDriverConfig {
1926 let state: NvidiaDriverExpectedState = yaml_serde::from_str(full_yaml()).unwrap();
1927 match state.state {
1928 NvidiaDriverState::Present(config) => *config,
1929 other => panic!("unexpected state {:?}", other),
1930 }
1931 }
1932
1933 fn rejection(raw: &str) -> String {
1934 match yaml_serde::from_str::<NvidiaDriverExpectedState>(raw) {
1935 Ok(state) => panic!("should be rejected: {}\nparsed as {:?}", raw, state),
1936 Err(error) => error.to_string(),
1937 }
1938 }
1939
1940 #[test]
1941 fn parsing_nvidia_driver_module_block_from_yaml_str() {
1942 let config = full_config();
1943 assert_eq!(
1944 config.kernel_modules,
1945 KernelModules::Open(DriverBranch::Branch("580".parse().unwrap()))
1946 );
1947 assert_eq!(config.cuda_toolkit.unwrap().package(), "cuda-toolkit-12-8");
1948 assert_eq!(config.persistence_mode, Some(true));
1949
1950 let raw = "---
1951- State: Absent
1952- State: !Present {}
1953- State: !Present
1954 KernelModules: !Open Latest
1955- State: !Present
1956 KernelModules: !Proprietary 580
1957 CudaToolkit: 13.0
1958- State: !Present
1959 CudaToolkit: 12.10
1960";
1961 let states: Vec<NvidiaDriverExpectedState> = yaml_serde::from_str(raw).unwrap();
1962 assert_eq!(states[0], NvidiaDriverExpectedState::absent());
1963 assert_eq!(
1964 states[1],
1965 NvidiaDriverExpectedState::present(NvidiaDriverConfig::new())
1966 );
1967 assert_eq!(states[1], states[2]);
1968 match &states[3].state {
1969 NvidiaDriverState::Present(config) => {
1970 assert_eq!(
1971 config.kernel_modules.flavor(),
1972 KernelModuleFlavor::Proprietary
1973 );
1974 assert_eq!(config.kernel_modules.branch(), Some(580));
1975 assert_eq!(config.kernel_modules.package(), "cuda-drivers");
1976 assert_eq!(config.cuda_toolkit.unwrap().to_string(), "13.0");
1977 }
1978 other => panic!("unexpected state {:?}", other),
1979 }
1980 match &states[4].state {
1982 NvidiaDriverState::Present(config) => {
1983 assert_eq!(config.cuda_toolkit.unwrap().package(), "cuda-toolkit-12-10")
1984 }
1985 other => panic!("unexpected state {:?}", other),
1986 }
1987 }
1988
1989 #[test]
1990 fn serialization_round_trip_preserves_state() {
1991 for raw in [
1993 full_yaml(),
1994 "State: Absent\n",
1995 "State: !Present\n KernelModules: !Proprietary 570\n",
1996 "State: !Present {}\n",
1997 ] {
1998 let state: NvidiaDriverExpectedState = yaml_serde::from_str(raw).unwrap();
1999 let json = serde_json::to_string(&state).unwrap();
2000 assert_eq!(
2001 serde_json::from_str::<NvidiaDriverExpectedState>(&json).unwrap(),
2002 state,
2003 "{}",
2004 json
2005 );
2006 }
2007 }
2008
2009 #[test]
2010 fn invalid_states_are_rejected_while_parsing() {
2011 let rejected = [
2012 ("State: !Absent\n PersistenceMode: true\n", "expected unit"),
2014 (
2016 "State: !Present\n KernelModules: !Proprietary Latest\n",
2017 "need a pinned branch",
2018 ),
2019 (
2020 "State: !Present\n KernelModules: !Proprietary 615\n",
2021 "removed after branch 610",
2022 ),
2023 (
2025 "State: !Present\n KernelModules: !Open 550\n",
2026 "from 570 on",
2027 ),
2028 (
2029 "State: !Present\n KernelModules: !Open newest\n",
2030 "expected Latest or a branch number",
2031 ),
2032 ("State: !Present\n KernelModules: 580\n", "KernelModules"),
2034 ("State: !Present\n CudaToolkit: '12'\n", "<major>.<minor>"),
2035 (
2036 "State: !Present\n CudaToolkit: 12.8.1\n",
2037 "<major>.<minor>",
2038 ),
2039 ("State: !Present\n CudaToolkit: 9.2\n", "<major>.<minor>"),
2040 ("State: !Present\n Branch: 580\n", "unknown field"),
2041 ];
2042 for (raw, reason) in rejected {
2043 let error = rejection(raw);
2044 assert!(
2045 error.contains(reason),
2046 "{:?} rejected for another reason: {}",
2047 raw,
2048 error
2049 );
2050 }
2051 }
2052
2053 #[test]
2054 fn proprietary_branch_is_built_from_an_integer() {
2055 let branch = ProprietaryBranch::new(580);
2056 assert_eq!(branch.number(), 580);
2057 assert_eq!(u32::from(branch), 580);
2058 assert_eq!(branch.to_string(), "580");
2059 assert_eq!(KernelModules::Proprietary(branch).branch(), Some(580));
2060 assert_eq!(ProprietaryBranch::new(570).number(), 570);
2061 assert_eq!(ProprietaryBranch::new(610).number(), 610);
2062 }
2063
2064 #[test]
2065 fn kernel_modules_shortcuts_match_the_explicit_form() {
2066 let with = |kernel_modules| NvidiaDriverConfig::new().with_kernel_modules(kernel_modules);
2067 assert_eq!(
2068 NvidiaDriverConfig::new()
2069 .with_proprietary_kernel_modules(580)
2070 .unwrap()
2071 .with_latest_open_kernel_modules(),
2072 with(KernelModules::Open(DriverBranch::Latest))
2073 );
2074 assert_eq!(
2075 NvidiaDriverConfig::new().with_latest_open_kernel_modules(),
2076 NvidiaDriverConfig::new()
2077 );
2078 assert_eq!(
2079 NvidiaDriverConfig::new()
2080 .with_open_kernel_modules(580)
2081 .unwrap(),
2082 with(KernelModules::Open(DriverBranch::Branch(
2083 "580".parse().unwrap()
2084 )))
2085 );
2086 assert_eq!(
2087 NvidiaDriverConfig::new()
2088 .with_proprietary_kernel_modules(580)
2089 .unwrap(),
2090 with(KernelModules::Proprietary(ProprietaryBranch::new(580)))
2091 );
2092 assert!(
2094 NvidiaDriverConfig::new()
2095 .with_open_kernel_modules(615)
2096 .is_ok()
2097 );
2098 }
2099
2100 #[test]
2101 fn kernel_modules_shortcuts_reject_invalid_branches() {
2102 let rejected = [
2103 (
2104 NvidiaDriverConfig::new().with_open_kernel_modules(550),
2105 "from 570 on",
2106 ),
2107 (
2108 NvidiaDriverConfig::new().with_proprietary_kernel_modules(550),
2109 "from 570 on",
2110 ),
2111 (
2112 NvidiaDriverConfig::new().with_proprietary_kernel_modules(615),
2113 "removed after branch 610",
2114 ),
2115 ];
2116 for (result, reason) in rejected {
2117 let error = result.unwrap_err().to_string();
2118 assert!(
2119 error.contains(reason),
2120 "rejected for another reason: {}",
2121 error
2122 );
2123 }
2124 }
2125
2126 #[test]
2127 #[should_panic(expected = "proprietary kernel modules branch")]
2128 fn proprietary_branch_new_panics_on_a_branch_without_proprietary_modules() {
2129 ProprietaryBranch::new(615);
2130 }
2131
2132 #[test]
2133 fn branch_selection_follows_the_distribution() {
2134 let open_580 = KernelModules::Open(DriverBranch::Branch("580".parse().unwrap()));
2135 let proprietary = KernelModules::Proprietary(ProprietaryBranch::new(575));
2136 let latest = KernelModules::default();
2137 assert_eq!(open_580.branch_selection(DriverDistro::Ubuntu), None);
2139 assert_eq!(proprietary.branch_selection(DriverDistro::Debian), None);
2140 assert_eq!(
2141 open_580.branch_selection(DriverDistro::RhelModular),
2142 Some(BranchSelection::ModuleStream(Some("580-open".to_string())))
2143 );
2144 assert_eq!(
2145 latest.branch_selection(DriverDistro::AmazonLinux),
2146 Some(BranchSelection::ModuleStream(Some("open-dkms".to_string())))
2147 );
2148 assert_eq!(
2149 proprietary.branch_selection(DriverDistro::RhelModular),
2150 Some(BranchSelection::ModuleStream(Some("575-dkms".to_string())))
2151 );
2152 assert_eq!(unpinned_selection(DriverDistro::Ubuntu), None);
2153 assert_eq!(
2154 unpinned_selection(DriverDistro::RhelModular),
2155 Some(BranchSelection::ModuleStream(None))
2156 );
2157 assert_eq!(
2158 latest.branch_selection(DriverDistro::Fedora),
2159 Some(BranchSelection::VersionLock(None))
2160 );
2161 }
2162
2163 #[test]
2164 fn install_package_pins_the_branch_on_apt() {
2165 let open_580 = KernelModules::Open(DriverBranch::Branch("580".parse().unwrap()));
2166 let proprietary_580 = KernelModules::Proprietary(ProprietaryBranch::new(580));
2167 let latest = KernelModules::default();
2168 assert_eq!(
2169 proprietary_580.install_package(DriverDistro::Ubuntu),
2170 "nvidia-driver-580"
2171 );
2172 assert_eq!(
2173 open_580.install_package(DriverDistro::Debian),
2174 "nvidia-driver-580-open"
2175 );
2176 assert_eq!(latest.install_package(DriverDistro::Ubuntu), "nvidia-open");
2177 assert_eq!(latest.install_package(DriverDistro::Fedora), "nvidia-open");
2178 assert_eq!(
2179 proprietary_580.install_package(DriverDistro::Fedora),
2180 "cuda-drivers"
2181 );
2182 }
2183
2184 #[test]
2185 fn current_branch_selection_is_compared() {
2186 let stream = CurrentSelection::ModuleStream(Some("580-open".to_string()));
2187 assert!(stream.matches(&BranchSelection::ModuleStream(Some("580-open".to_string()))));
2188 assert!(!stream.matches(&BranchSelection::ModuleStream(Some(
2189 "open-dkms".to_string()
2190 ))));
2191 assert!(!stream.matches(&BranchSelection::ModuleStream(None)));
2192 assert!(
2193 !CurrentSelection::ModuleStream(None).matches(&BranchSelection::ModuleStream(Some(
2194 "open-dkms".to_string()
2195 )))
2196 );
2197 assert!(CurrentSelection::ModuleStream(None).matches(&BranchSelection::ModuleStream(None)));
2198
2199 let locks = CurrentSelection::ZypperLocks(vec![ZypperLock {
2200 name: "*nvidia*".to_string(),
2201 version: ">= 581".to_string(),
2202 }]);
2203 assert!(locks.matches(&BranchSelection::ZypperLock(Some(580))));
2204 assert!(!locks.matches(&BranchSelection::ZypperLock(None)));
2205 assert_eq!(locks.to_undo(), vec!["*nvidia* >= 581"]);
2206 }
2207
2208 #[test]
2209 fn dnf_versionlock_lists_are_parsed() {
2210 let dnf4 = "Last metadata expiration check: 0:01:02 ago.\nnvidia-open-3:580.105.08-1.el10.*\nlibnvidia-ml-3:580.105.08-1.el10.*\n!kernel-0:6.12.0-55.el10.*\nbash-0:5.2.26-6.el10.*\nnvidia-container-toolkit-0:1.17.8-1.*\n";
2211 let locks = parse_versionlock_list(dnf4);
2212 assert_eq!(
2213 locks,
2214 vec![
2215 LockedPackage {
2216 name: "nvidia-open".to_string(),
2217 version: "580.105.08-1.el10".to_string()
2218 },
2219 LockedPackage {
2220 name: "libnvidia-ml".to_string(),
2221 version: "580.105.08-1.el10".to_string()
2222 },
2223 ]
2224 );
2225 let current = CurrentSelection::VersionLocks(locks);
2226 assert!(current.matches(&BranchSelection::VersionLock(Some(580))));
2227 assert!(!current.matches(&BranchSelection::VersionLock(Some(590))));
2228
2229 let dnf5 = "# Added by 'versionlock add' command on 2026-05-04 10:12:31\nPackage name: nvidia-open\nevr = 3:590.48.01-1.fc44\n\nPackage name: htop\nevr = 3.3.0-1.fc44\n";
2230 assert_eq!(
2231 parse_versionlock_list(dnf5),
2232 vec![LockedPackage {
2233 name: "nvidia-open".to_string(),
2234 version: "3:590.48.01-1.fc44".to_string()
2235 }]
2236 );
2237 assert!(
2238 CurrentSelection::VersionLocks(parse_versionlock_list(dnf5))
2239 .matches(&BranchSelection::VersionLock(Some(590)))
2240 );
2241 }
2242
2243 #[test]
2244 fn zypper_locks_and_module_streams_are_parsed() {
2245 let locks = "type: package\nmatch_type: glob\ncase_sensitive: on\nsolvable_name: *nvidia*\nversion: >= 581\n\ntype: package\nsolvable_name: kernel-default\n";
2246 assert_eq!(
2247 parse_zypper_locks(locks),
2248 vec![ZypperLock {
2249 name: "*nvidia*".to_string(),
2250 version: ">= 581".to_string()
2251 }]
2252 );
2253
2254 let modules = "CUDA and drivers from NVIDIA\nName Stream Profiles Summary\nnvidia-driver 580-open [e] default [d], fm, ks, src Nvidia driver for 580-open branch\n\nHint: [d]efault, [e]nabled, [x]disabled, [i]nstalled\n";
2255 assert_eq!(
2256 parse_enabled_module_stream(modules),
2257 Some("580-open".to_string())
2258 );
2259 assert_eq!(
2260 parse_enabled_module_stream("nvidia-driver latest-dkms [d][e] default [d]"),
2261 Some("latest-dkms".to_string())
2262 );
2263 assert_eq!(
2264 parse_enabled_module_stream("Error: No matching Modules to list"),
2265 None
2266 );
2267 }
2268
2269 #[test]
2270 fn branch_selection_commands_are_quoted() {
2271 assert_eq!(
2272 branch_selection_commands(
2273 &BranchSelection::ModuleStream(Some("580-open".to_string())),
2274 DriverDistro::RhelModular,
2275 &[]
2276 ),
2277 vec![
2278 "'dnf' '-y' 'module' 'reset' 'nvidia-driver'",
2279 "'dnf' '-y' 'module' 'enable' 'nvidia-driver:580-open'"
2280 ]
2281 );
2282 assert_eq!(
2283 branch_selection_commands(
2284 &BranchSelection::ModuleStream(None),
2285 DriverDistro::AmazonLinux,
2286 &[]
2287 ),
2288 vec!["'dnf' '-y' 'module' 'reset' 'nvidia-driver'"]
2289 );
2290 assert_eq!(
2291 branch_selection_commands(
2292 &BranchSelection::VersionLock(Some(580)),
2293 DriverDistro::Rhel10,
2294 &["nvidia-open".to_string()]
2295 ),
2296 vec![
2297 "'dnf' '-y' 'install' 'python3-dnf-plugin-versionlock'",
2298 "'dnf' 'versionlock' 'delete' 'nvidia-open'",
2299 "'dnf' 'versionlock' 'add' '*nvidia*580*'"
2300 ]
2301 );
2302 assert_eq!(
2303 branch_selection_commands(
2304 &BranchSelection::ZypperLock(Some(580)),
2305 DriverDistro::Suse,
2306 &["*nvidia* >= 591".to_string()]
2307 ),
2308 vec![
2309 "'zypper' '--non-interactive' 'removelock' '*nvidia* >= 591'",
2310 "'zypper' '--non-interactive' 'addlock' '*nvidia* >= 581'"
2311 ]
2312 );
2313 }
2314
2315 #[test]
2316 fn installed_driver_is_compared_with_the_expected_one() {
2317 let installed = |packages: &[(&str, &str)]| -> BTreeMap<String, String> {
2318 packages
2319 .iter()
2320 .map(|(name, version)| (name.to_string(), version.to_string()))
2321 .collect()
2322 };
2323 let open_580 = KernelModules::Open(DriverBranch::Branch("580".parse().unwrap()));
2324 let proprietary_580 = KernelModules::Proprietary(ProprietaryBranch::new(580));
2325
2326 assert_eq!(
2328 installed_driver_status(&installed(&[]), &open_580, DriverDistro::Fedora),
2329 InstalledDriver::Missing
2330 );
2331 assert_eq!(
2332 installed_driver_status(
2333 &installed(&[("nvidia-open", "580.105.08-1ubuntu1")]),
2334 &open_580,
2335 DriverDistro::Fedora
2336 ),
2337 InstalledDriver::Expected
2338 );
2339 assert_eq!(
2340 installed_driver_status(
2341 &installed(&[("nvidia-open", "590.48.01-1ubuntu1")]),
2342 &KernelModules::default(),
2343 DriverDistro::Fedora
2344 ),
2345 InstalledDriver::Expected
2346 );
2347 assert!(matches!(
2348 installed_driver_status(
2349 &installed(&[("nvidia-open", "590.48.01-1")]),
2350 &open_580,
2351 DriverDistro::Fedora
2352 ),
2353 InstalledDriver::Different(reason) if reason.contains("expected branch 580")
2354 ));
2355 assert!(matches!(
2356 installed_driver_status(
2357 &installed(&[("cuda-drivers", "580.105.08-1")]),
2358 &open_580,
2359 DriverDistro::Fedora
2360 ),
2361 InstalledDriver::Different(reason) if reason.contains("proprietary kernel modules) is installed")
2362 ));
2363 assert!(matches!(
2364 installed_driver_status(
2365 &installed(&[("nvidia-open", "580.105.08-1")]),
2366 &proprietary_580,
2367 DriverDistro::Fedora
2368 ),
2369 InstalledDriver::Different(_)
2370 ));
2371 assert_eq!(
2373 installed_driver_status(
2374 &installed(&[
2375 ("nvidia-open", "615.71.09-1"),
2376 ("cuda-drivers", "615.71.09-2")
2377 ]),
2378 &KernelModules::default(),
2379 DriverDistro::Fedora
2380 ),
2381 InstalledDriver::Expected
2382 );
2383
2384 assert_eq!(
2386 installed_driver_status(
2387 &installed(&[("nvidia-driver-580", "580.178.04-0ubuntu0.26.04.1")]),
2388 &proprietary_580,
2389 DriverDistro::Ubuntu
2390 ),
2391 InstalledDriver::Expected
2392 );
2393 assert_eq!(
2394 installed_driver_status(
2395 &installed(&[("nvidia-driver-580-server", "580.178.04-0ubuntu0.26.04.1")]),
2396 &proprietary_580,
2397 DriverDistro::Ubuntu
2398 ),
2399 InstalledDriver::Expected
2400 );
2401 assert_eq!(
2402 installed_driver_status(
2403 &installed(&[("nvidia-driver-580-open", "580.178.04-1")]),
2404 &open_580,
2405 DriverDistro::Ubuntu
2406 ),
2407 InstalledDriver::Expected
2408 );
2409 assert!(matches!(
2411 installed_driver_status(
2412 &installed(&[("cuda-drivers", "580.105.08-1")]),
2413 &proprietary_580,
2414 DriverDistro::Ubuntu
2415 ),
2416 InstalledDriver::Different(reason) if reason.contains("expected nvidia-driver-580")
2417 ));
2418 assert!(matches!(
2420 installed_driver_status(
2421 &installed(&[("nvidia-driver-575", "575.143.08-1")]),
2422 &proprietary_580,
2423 DriverDistro::Ubuntu
2424 ),
2425 InstalledDriver::Different(reason) if reason.contains("nvidia-driver-575")
2426 ));
2427 assert!(matches!(
2428 installed_driver_status(
2429 &installed(&[("nvidia-driver-580-open", "580.178.04-1")]),
2430 &proprietary_580,
2431 DriverDistro::Ubuntu
2432 ),
2433 InstalledDriver::Different(reason) if reason.contains("expected nvidia-driver-580")
2434 ));
2435 assert_eq!(
2437 installed_driver_status(
2438 &installed(&[("nvidia-open", "615.71.09-1")]),
2439 &KernelModules::default(),
2440 DriverDistro::Ubuntu
2441 ),
2442 InstalledDriver::Expected
2443 );
2444 assert!(matches!(
2445 installed_driver_status(
2446 &installed(&[("nvidia-driver-580", "580.178.04-1")]),
2447 &KernelModules::default(),
2448 DriverDistro::Ubuntu
2449 ),
2450 InstalledDriver::Different(reason) if reason.contains("expected nvidia-open")
2451 ));
2452 assert_eq!(
2454 installed_driver_status(
2455 &installed(&[("nvidia-driver-pinning-580", "580.105.08")]),
2456 &proprietary_580,
2457 DriverDistro::Ubuntu
2458 ),
2459 InstalledDriver::Missing
2460 );
2461 }
2462
2463 #[test]
2464 fn driver_removal_keeps_the_container_toolkit() {
2465 let installed = [
2466 "nvidia-open",
2467 "nvidia-driver-580-open",
2468 "nvidia-driver-pinning-580",
2469 "nvidia-container-toolkit",
2470 "libnvidia-container1",
2471 "libnvidia-compute-580",
2472 ]
2473 .map(String::from);
2474 assert_eq!(
2475 driver_packages_to_remove(installed.iter()),
2476 vec![
2477 "nvidia-open",
2478 "nvidia-driver-580-open",
2479 "nvidia-driver-pinning-580",
2480 "libnvidia-compute-580"
2481 ]
2482 );
2483 }
2484
2485 #[test]
2486 fn runtime_issues_tell_when_a_reload_or_reboot_is_needed() {
2487 let open_580 = LoadedDriver {
2488 version: "580.105.08".to_string(),
2489 flavor: KernelModuleFlavor::Open,
2490 };
2491 let working = parse_smi(0, "580.105.08\n580.105.08\n");
2492 assert_eq!(working, SmiStatus::Working("580.105.08".to_string()));
2493 assert_eq!(
2494 runtime_issue(
2495 Some(&open_580),
2496 &working,
2497 KernelModuleFlavor::Open,
2498 Some(580)
2499 ),
2500 None
2501 );
2502 assert_eq!(
2503 runtime_issue(Some(&open_580), &working, KernelModuleFlavor::Open, None),
2504 None
2505 );
2506 assert!(
2507 runtime_issue(None, &working, KernelModuleFlavor::Open, None)
2508 .unwrap()
2509 .contains("not loaded")
2510 );
2511 assert!(
2512 runtime_issue(
2513 Some(&open_580),
2514 &working,
2515 KernelModuleFlavor::Proprietary,
2516 Some(580)
2517 )
2518 .unwrap()
2519 .contains("the open kernel module is loaded")
2520 );
2521 assert!(
2522 runtime_issue(
2523 Some(&open_580),
2524 &working,
2525 KernelModuleFlavor::Open,
2526 Some(590)
2527 )
2528 .unwrap()
2529 .contains("expected branch 590")
2530 );
2531 let mismatch = parse_smi(
2532 18,
2533 "Failed to initialize NVML: Driver/library version mismatch\nNVML library version: 580.126\n",
2534 );
2535 assert_eq!(mismatch, SmiStatus::VersionMismatch);
2536 assert!(
2537 runtime_issue(Some(&open_580), &mismatch, KernelModuleFlavor::Open, None)
2538 .unwrap()
2539 .contains("version mismatch")
2540 );
2541 assert!(matches!(
2542 parse_smi(
2543 9,
2544 "NVIDIA-SMI has failed because it couldn't communicate with the NVIDIA driver."
2545 ),
2546 SmiStatus::Failing(_)
2547 ));
2548 }
2549
2550 #[test]
2551 fn gpus_and_secure_boot_are_detected() {
2552 let devices = |nvidia_class: &str| {
2553 format!(
2554 "/sys/bus/pci/devices/0000:00:00.0/vendor:0x8086\n/sys/bus/pci/devices/0000:01:00.0/vendor:0x10de\n/sys/bus/pci/devices/0000:00:00.0/class:0x060000\n/sys/bus/pci/devices/0000:01:00.0/class:{}\n",
2555 nvidia_class
2556 )
2557 };
2558 assert!(has_nvidia_gpu(&devices("0x030200")));
2559 assert!(!has_nvidia_gpu(&devices("0x040300")));
2560 assert_eq!(parse_secure_boot("SecureBoot enabled\n"), Some(true));
2561 assert_eq!(
2562 parse_secure_boot("SecureBoot disabled\nPlatform is in Setup Mode"),
2563 Some(false)
2564 );
2565 assert_eq!(
2566 parse_secure_boot("EFI variables are not supported on this system"),
2567 None
2568 );
2569 }
2570
2571 #[test]
2572 fn persistence_mode_handles_static_and_masked_units() {
2573 let state = |enablement, active| UnitState { enablement, active };
2574 let static_stopped = state(UnitEnablement::Static, false);
2575 assert!(is_persistence_mode_set(&static_stopped, false));
2576 assert!(!is_persistence_mode_set(&static_stopped, true));
2577 assert_eq!(
2578 persistence_mode_commands(&static_stopped, true),
2579 vec!["'systemctl' 'start' 'nvidia-persistenced'"]
2580 );
2581 assert_eq!(
2582 persistence_mode_commands(&state(UnitEnablement::Static, true), false),
2583 vec!["'systemctl' 'stop' 'nvidia-persistenced'"]
2584 );
2585 assert_eq!(
2586 persistence_mode_commands(&state(UnitEnablement::Masked, false), true),
2587 vec![
2588 "'systemctl' 'unmask' 'nvidia-persistenced'",
2589 "'systemctl' 'enable' 'nvidia-persistenced'",
2590 "'systemctl' 'start' 'nvidia-persistenced'"
2591 ]
2592 );
2593 assert_eq!(
2594 persistence_mode_commands(&state(UnitEnablement::Enabled, true), false),
2595 vec![
2596 "'systemctl' 'disable' 'nvidia-persistenced'",
2597 "'systemctl' 'stop' 'nvidia-persistenced'"
2598 ]
2599 );
2600 assert!(is_persistence_mode_set(&UnitState::default(), false));
2601 }
2602
2603 #[test]
2604 fn display_explains_remediations() {
2605 let api_call = NvidiaDriverApiCall::from(
2606 NvidiaDriverInternalApiCall::LoadKernelModule {
2607 flavor: KernelModuleFlavor::Open,
2608 branch: None,
2609 reason: "nvidia-open is being installed".to_string(),
2610 },
2611 Privilege::WithSudo,
2612 );
2613 assert!(api_call.display().contains("reboot is required"));
2614 assert_eq!(api_call.default_timeout(), Duration::from_secs(300));
2615 }
2616}