Skip to main content

regent_sdk/state/attribute/gpu/
nvidia_driver.rs

1//! NVIDIA driver attribute
2//!
3//! This module provides the `NvidiaDriverExpectedState` type for installing the NVIDIA GPU
4//! driver (and optionally the CUDA toolkit) from NVIDIA's CUDA network repository,
5//! `https://developer.download.nvidia.com/compute/cuda/repos/<distro>/<arch>/`, as described in
6//! the [NVIDIA Driver Installation Guide](https://docs.nvidia.com/datacenter/tesla/driver-installation-guide/).
7//!
8//! **Compatible OS:** Linux, on the distributions served by the CUDA network repository:
9//! Ubuntu 22.04/24.04/26.04, Debian 12/13, RHEL/Rocky/AlmaLinux/Oracle Linux/CentOS Stream
10//! 8/9/10, Fedora 42/43/44, Amazon Linux 2023, SLES 15/16 and openSUSE Leap 15, on `x86_64` or
11//! `aarch64` servers (`sbsa` repositories; Jetson boards use JetPack instead). The distribution
12//! and architecture are detected at assessment time (`/etc/os-release`, `uname -m`): an
13//! unsupported host is reported as a fatal non-compliance.
14//!
15//! **Privilege:** installing packages needs root, use `!WithSudo`.
16//!
17//! # Invalid states are unrepresentable
18//!
19//! - The driver configuration only exists in the `Present` state: an `Absent` driver has nothing
20//!   to configure.
21//! - The kernel module flavor carries the driver branch. The open kernel modules (default, the
22//!   only flavor supporting Blackwell GPUs, recommended from Turing on) accept `Latest` or a
23//!   branch number. The proprietary modules (needed by Maxwell, Pascal and Volta GPUs) were
24//!   removed from the packages after branch 610, so they require an explicit branch between 570
25//!   and 610: `!Proprietary Latest` or `!Proprietary 615` can't be built.
26//! - Branches before 570 don't follow the current packaging conventions and are rejected.
27//! - The CUDA toolkit version is a `<major>.<minor>` version (`cuda-toolkit-<major>-<minor>`).
28//!
29//! # How compliance is assessed
30//!
31//! 1. The CUDA network repository of the host must be configured (cuda-keyring package for apt,
32//!    `cuda-<distro>.repo` for dnf and zypper). It is never removed by this attribute.
33//! 2. The driver branch is selected the way NVIDIA documents it for the distribution. On
34//!    Ubuntu and Debian, the branch is pinned by installing its meta package
35//!    `nvidia-driver-<branch>` (proprietary kernel modules) or `nvidia-driver-<branch>-open`
36//!    (open kernel modules), shipped by the CUDA repository or by the distribution archive
37//!    once NVIDIA prunes the branch from the CUDA repository: the branch-suffixed packages
38//!    keep the driver in the branch. Elsewhere, the `nvidia-driver` DNF module stream is
39//!    enabled (`open-dkms`, `<branch>-open`, `<branch>-dkms`; RHEL 8/9, Amazon Linux 2023),
40//!    `dnf versionlock` on `*nvidia*<branch>*` is set (RHEL 10, Fedora) or a
41//!    `*nvidia* >= <branch + 1>` zypper lock is added (SUSE). `Latest` removes these pins
42//!    and locks.
43//! 3. The driver meta package must be installed: on Ubuntu and Debian, the branch meta
44//!    package (`nvidia-driver-<branch>` / `nvidia-driver-<branch>-open`) when a branch is
45//!    pinned, `nvidia-open` (open kernel modules) or `cuda-drivers` (proprietary kernel
46//!    modules) for the latest branch; on the other distributions, `nvidia-open` or
47//!    `cuda-drivers`, together with the kernel headers DKMS needs to build the modules. Its
48//!    version must belong to the expected branch: if it doesn't, the driver is removed then
49//!    installed again. `Latest` installs the newest driver once and doesn't upgrade it
50//!    afterwards (upgrades are left to the system updates).
51//! 4. The kernel module must be loaded with the expected flavor (`/proc/driver/nvidia/version`
52//!    tells `Open Kernel Module` apart) and `nvidia-smi` must work (no
53//!    `Driver/library version mismatch`). If not, a remediation tries to (re)load the module
54//!    with `modprobe` and **fails with an explicit "reboot required" message** when it can't:
55//!    the host is never rebooted automatically. A reboot is typically needed after a first
56//!    installation when `nouveau` is loaded, or after an upgrade while the GPU is in use.
57//! 5. `CudaToolkit`, if set, requires the `cuda-toolkit-<major>-<minor>` package (not needed to
58//!    run CUDA containers, which ship their own CUDA runtime).
59//! 6. `PersistenceMode`, if set, requires the `nvidia-persistenced` service to be enabled and
60//!    running (`true`) or disabled and stopped (`false`). It keeps the GPUs initialized, which
61//!    avoids a delay at each start of a GPU application.
62//!
63//! `Absent` removes the driver packages (as listed in the "Removing the Driver" section of the
64//! guide) and the branch pins and locks (module stream reset, version locks, zypper lock), but
65//! keeps the repository, the CUDA toolkit and the NVIDIA Container Toolkit. A reboot is needed to
66//! unload a module still in memory.
67//!
68//! Installing the driver builds the kernel modules with DKMS, which can take a while: the
69//! default timeout is 30 minutes.
70//!
71//! **Prerequisites** (not managed by this attribute, see the guide's "Pre-installation Actions"):
72//! the `contrib` component on Debian, EPEL and CodeReady Builder / CRB on RHEL-compatible
73//! distributions (for DKMS), the PackageHub module on SLES.
74//!
75//! **Secure Boot:** with Secure Boot enabled, the modules built by DKMS must be signed with a key
76//! enrolled in the MOK database (`mokutil --import`, then a confirmation on the console at the
77//! next boot), otherwise loading them fails with "Key was rejected by service". When the module
78//! can't be loaded, the remediation reports the Secure Boot state given by `mokutil --sb-state`.
79//!
80//! # Examples
81//!
82//! ## Rust API
83//!
84//! ```no_run
85//! use regent_sdk::state::attribute::gpu::nvidia_driver::{NvidiaDriverConfig, NvidiaDriverExpectedState};
86//! use regent_sdk::{Attribute, ExpectedState, Privilege, RegentError};
87//!
88//! fn gpu_node() -> Result<ExpectedState, RegentError> {
89//!     // with_latest_open_kernel_modules() (default) | with_open_kernel_modules(<branch>)
90//!     // | with_proprietary_kernel_modules(<branch 570-610>)
91//!     let config = NvidiaDriverConfig::new()
92//!         .with_open_kernel_modules(580)?
93//!         .with_cuda_toolkit("12.8".parse()?)
94//!         .with_persistence_mode(true);
95//!
96//!     Ok(ExpectedState::new()
97//!         .with_attribute(Attribute::nvidia_driver(
98//!             NvidiaDriverExpectedState::present(config),
99//!             Privilege::WithSudo,
100//!             None,
101//!         ))
102//!         .build())
103//! }
104//! ```
105//!
106//! ## YAML API
107//!
108//! ```yaml
109//! Attributes:
110//!   - Name: NVIDIA driver must be installed
111//!     Privilege: !WithSudo
112//!     Detail: !NvidiaDriver
113//!       State: !Present
114//!         KernelModules: !Open 580   # !Open Latest (default) | !Open <branch> | !Proprietary <branch 570-610>
115//!         CudaToolkit: "12.8"        # optional, cuda-toolkit-12-8
116//!         PersistenceMode: true      # optional, nvidia-persistenced service
117//! ```
118//!
119//! With the defaults (newest driver, open kernel modules):
120//!
121//! ```yaml
122//! Attributes:
123//!   - Name: NVIDIA driver must be installed
124//!     Privilege: !WithSudo
125//!     Detail: !NvidiaDriver
126//!       State: !Present {}
127//! ```
128//!
129//! To remove the driver:
130//!
131//! ```yaml
132//! Attributes:
133//!   - Name: NVIDIA driver must be removed
134//!     Privilege: !WithSudo
135//!     Detail: !NvidiaDriver
136//!       State: Absent
137//! ```
138
139use 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
166/// First branch following the current packaging conventions (pinning packages, module streams)
167const FIRST_SUPPORTED_BRANCH: u32 = 570;
168
169/// Last branch shipping the proprietary kernel modules
170const LAST_PROPRIETARY_BRANCH: u32 = 610;
171
172/// Service keeping the GPUs initialized
173const 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    /// Driver installed and loaded with this configuration
186    Present(Box<NvidiaDriverConfig>),
187    Absent,
188}
189
190impl NvidiaDriverExpectedState {
191    /// Driver must be installed and loaded with this configuration
192    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/// Optional fields left out are not managed.
206#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
207#[serde(deny_unknown_fields)]
208#[serde(rename_all = "PascalCase")]
209pub struct NvidiaDriverConfig {
210    /// Kernel module flavor and driver branch (open modules, latest branch by default)
211    #[serde(default)]
212    kernel_modules: KernelModules,
213    /// CUDA toolkit to install (`cuda-toolkit-<major>-<minor>`)
214    cuda_toolkit: Option<CudaVersion>,
215    /// `nvidia-persistenced` service enabled and running (`true`) or disabled and stopped (`false`)
216    persistence_mode: Option<bool>,
217}
218
219impl NvidiaDriverConfig {
220    /// Open kernel modules, latest branch
221    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    /// Open kernel modules, newest driver branch (the default)
231    pub fn with_latest_open_kernel_modules(self) -> Self {
232        self.with_kernel_modules(KernelModules::Open(DriverBranch::Latest))
233    }
234
235    /// Open kernel modules, driver pinned to this branch (from 570 on)
236    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    /// Proprietary kernel modules, driver pinned to this branch (570 to 610)
242    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/// Kernel module flavor, carrying the driver branch
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
260#[serde(rename_all = "PascalCase")]
261pub enum KernelModules {
262    /// Open GPU kernel modules (`nvidia-open`): required on Blackwell, recommended from Turing on
263    Open(DriverBranch),
264    /// Proprietary kernel modules (`cuda-drivers`): needed by Maxwell, Pascal and Volta GPUs,
265    /// only available up to branch 610
266    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    /// Pinned branch, `None` for the latest one
284    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    /// Floating meta package installing the whole driver (latest branch)
293    pub fn package(&self) -> &'static str {
294        match self {
295            KernelModules::Open(_) => OPEN_PACKAGE,
296            KernelModules::Proprietary(_) => PROPRIETARY_PACKAGE,
297        }
298    }
299
300    /// Meta package installing the driver with the expected branch: on apt, the branch
301    /// meta package `nvidia-driver-<branch>[-open]` (pinning the branch by construction,
302    /// shipped by the CUDA repository or the distribution archive); the floating meta
303    /// package otherwise
304    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    /// How the branch is selected on a distribution, `None` when the driver meta package
315    /// itself pins the branch (apt)
316    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
337/// Selection leaving no branch pinned nor locked, `None` on apt (pinned by the meta package)
338fn 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/// Driver branch of the open kernel modules: `Latest` or a branch number (e.g. `580`)
350#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
351#[serde(try_from = "RawBranch", into = "RawBranch")]
352pub enum DriverBranch {
353    /// Newest driver available when installing
354    Latest,
355    /// Driver branch, pinned
356    Branch(BranchNumber),
357}
358
359/// Branch as written in YAML or JSON: a number, `Latest`, or a number as a string
360#[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/// Driver branch number (major version of the driver, e.g. `580`), from 570 on
409#[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/// Branch of the proprietary kernel modules: from 570 to 610 (the last branch shipping them)
466#[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    /// Branch number of the proprietary kernel modules (e.g. `580`), from 570 to 610
533    ///
534    /// # Panics
535    ///
536    /// When the branch does not ship the proprietary kernel modules — use
537    /// `ProprietaryBranch::try_from` to handle an invalid branch.
538    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/// CUDA toolkit version `<major>.<minor>` (e.g. `12.8`, `13.0`)
548#[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    /// `cuda-toolkit-<major>-<minor>`
604    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/// How a driver branch is selected (`None`: latest branch, no pin or lock)
614#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
615#[serde(rename_all = "PascalCase")]
616pub enum BranchSelection {
617    /// Enabled stream of the `nvidia-driver` DNF module, `None` for a reset module
618    ModuleStream(Option<String>),
619    /// `dnf versionlock` on `*nvidia*<branch>*`
620    VersionLock(Option<u32>),
621    /// zypper lock `*nvidia* >= <branch + 1>`
622    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/// Branch selection currently in place, with what must be undone to change it
651#[derive(Debug, Clone, PartialEq, Eq)]
652enum CurrentSelection {
653    /// Enabled `nvidia-driver` module stream
654    ModuleStream(Option<String>),
655    /// Version locks of NVIDIA packages
656    VersionLocks(Vec<LockedPackage>),
657    /// zypper locks on `*nvidia*`
658    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    /// Pins and locks to remove before applying a new selection
691    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
704/// Branch (major version) of a driver version: `3:580.105.08-1.el10` -> 580
705fn 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/// Package locked by `dnf versionlock`
717#[derive(Debug, Clone, PartialEq, Eq)]
718struct LockedPackage {
719    name: String,
720    /// `[epoch:]version-release`
721    version: String,
722}
723
724/// Parse `dnf versionlock list`, keeping the NVIDIA driver packages. DNF 4 prints one
725/// `name-[epoch:]version-release.*` entry per line, DNF 5 a `Package name: <name>` line followed
726/// by `evr = <version>` conditions. Exclusions (`!name...`) are ignored.
727fn 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        // DNF 4: nvidia-open-3:580.105.08-1.el10.*
749        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/// Lock of the `/etc/zypp/locks` file on `*nvidia*`
765#[derive(Debug, Clone, PartialEq, Eq)]
766struct ZypperLock {
767    name: String,
768    /// Version condition, whitespace normalized (`>= 581`)
769    version: String,
770}
771
772impl ZypperLock {
773    /// Lock as given to `zypper addlock` / `zypper removelock`
774    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
785/// Version condition excluding the branches after `branch`
786fn zypper_lock_condition(branch: u32) -> String {
787    format!(">= {}", branch + 1)
788}
789
790/// Parse `/etc/zypp/locks` (blocks of `key: value` lines), keeping the `*nvidia*` locks
791fn 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
814/// Parse `dnf module list --enabled nvidia-driver`: enabled stream, if any
815fn 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
830/// Commands applying a branch selection, `undo` being what [`CurrentSelection::to_undo`] returned
831fn 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                // DNF 4 needs the versionlock plugin (built into DNF 5)
859                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
902/// Meta package of the driver with the open kernel modules
903const OPEN_PACKAGE: &str = "nvidia-open";
904/// Meta package of the driver with the proprietary kernel modules (up to branch 610, then a
905/// transitional package depending on `nvidia-open`)
906const PROPRIETARY_PACKAGE: &str = "cuda-drivers";
907
908/// Installed driver compared with the expected one
909#[derive(Debug, Clone, PartialEq, Eq)]
910enum InstalledDriver {
911    /// Expected flavor, in the expected branch
912    Expected,
913    /// No driver meta package installed
914    Missing,
915    /// Another flavor or branch is installed, with the reason why it must be replaced
916    Different(String),
917}
918
919/// Driver meta package installed on an apt host: kernel module flavor, and branch for
920/// the branch meta packages (`nvidia-driver-580`, `nvidia-driver-580-open`,
921/// `nvidia-driver-580-server`, ...). `None` for the floating meta packages
922/// (`cuda-drivers`, `nvidia-driver`, `nvidia-open`, `nvidia-driver-open`).
923fn 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
945/// Meta packages telling which driver is installed: the floating ones, and on apt the
946/// branch ones (`nvidia-driver-<branch>[-open]`, pinning the branch by construction)
947fn 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
960/// Compare the installed driver meta packages (name -> version) with the expected driver
961fn 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        // On apt, the branch is pinned by its meta package: the expected driver is the
969        // branch meta package `nvidia-driver-<branch>[-open]` (or the floating meta
970        // package for the latest branch), any other driver meta package is another driver
971        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    // dnf/zypper: the floating meta package, kept in the expected branch by the module
994    // stream or the version locks
995    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
1033/// Packages of the driver, as listed in the "Removing the Driver" section of the guide
1034fn 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
1127/// Installed driver packages to remove: the container toolkit is kept
1128fn 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/// Outcome of `nvidia-smi --query-gpu=driver_version --format=csv,noheader`
1136#[derive(Debug, Clone, PartialEq, Eq)]
1137enum SmiStatus {
1138    /// Driver version reported for each GPU
1139    Working(String),
1140    /// User space libraries and kernel module versions differ
1141    VersionMismatch,
1142    /// Any other failure (driver not loaded, no device, nvidia-smi missing)
1143    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
1168/// Why the loaded driver is not the expected one, `None` if it is
1169fn 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
1201/// Command printing `<path>:<value>` for the vendor and class of each PCI device (the glob is
1202/// expanded by the shell)
1203const PCI_DEVICES_COMMAND: &str =
1204    "grep -H . /sys/bus/pci/devices/*/vendor /sys/bus/pci/devices/*/class";
1205
1206/// Whether an NVIDIA display or 3D controller (vendor 0x10de, class 0x03xxxx) is present, from
1207/// the output of [`PCI_DEVICES_COMMAND`]
1208fn 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
1229/// Secure Boot state from `mokutil --sb-state`
1230fn 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
1240/// Whether `nvidia-persistenced` is in the expected state. A static unit can't be enabled nor
1241/// disabled: only whether it runs matters.
1242fn 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
1254/// Commands bringing `nvidia-persistenced` from `state` to the expected one
1255fn 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
1285/// NVIDIA kernel modules, unloaded in this order before loading the expected one
1286const NVIDIA_MODULES: [&str; 5] = [
1287    "nvidia_uvm",
1288    "nvidia_drm",
1289    "nvidia_modeset",
1290    "nvidia_peermem",
1291    "nvidia",
1292];
1293
1294/// Host characteristics detected at assessment time
1295struct DriverHost {
1296    repository: CudaRepository,
1297    /// `uname -r`
1298    kernel_release: String,
1299}
1300
1301/// Detect the CUDA repository of the host. `Ok(Err(..))` for unsupported hosts.
1302async 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            // Fails when no stream is enabled: the output is what matters
1339            let result = query(
1340                host_handler,
1341                &quoted_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            // Fails when the versionlock plugin is not installed: no lock then
1351            let result = query(
1352                host_handler,
1353                &quoted_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                &quoted_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        // Installing the driver builds the kernel modules with DKMS
1384        Duration::from_secs(1800)
1385    }
1386}
1387
1388impl Check for NvidiaDriverExpectedState {
1389    fn check(&self) -> Result<(), RegentError> {
1390        // Every value is validated when its type is built: an existing NvidiaDriverExpectedState is valid
1391        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                // Another branch or flavor: remove the current driver, then install the expected one
1485                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                // On apt, the branch meta package pins the branch: nothing to select
1501                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                // Loading fails with "reboot required" when the module can't be replaced, which
1531                // stops the remediations after it: only the persistence mode, which needs the module, comes after
1532                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    /// Configure the NVIDIA CUDA network repository
1588    AddRepository(CudaRepository),
1589    /// Pin or lock the driver branch, after removing the pins and locks in `undo`
1590    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    /// Install the driver meta package and the kernel headers
1601    InstallDriver {
1602        family: PackageFamily,
1603        packages: Vec<String>,
1604    },
1605    /// Install `cuda-toolkit-<major>-<minor>`
1606    InstallCudaToolkit {
1607        family: PackageFamily,
1608        version: CudaVersion,
1609    },
1610    /// (Re)load the NVIDIA kernel module, failing with "reboot required" when it can't
1611    LoadKernelModule {
1612        flavor: KernelModuleFlavor,
1613        branch: Option<u32>,
1614        reason: String,
1615    },
1616    /// Enable and start (`true`) or disable and stop (`false`) nvidia-persistenced
1617    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
1775/// Load the NVIDIA kernel module and verify the driver works, never rebooting the host
1776async 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    // The daemon holds the module: stop it while reloading, and start it again whatever happens
1790    let persistenced = unit_state(host_handler, PERSISTENCE_DAEMON, privilege).await?;
1791    if persistenced.active {
1792        run_step(
1793            host_handler,
1794            &quoted_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            &quoted_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            &quoted_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
1852/// Unload the loaded NVIDIA modules and nouveau, then load `nvidia` and `nvidia_uvm`: the problem
1853/// preventing it, if any, with the output of the failed modprobe
1854async 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            &quoted_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            &quoted_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            &quoted_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        // Unquoted, YAML would read a float: the version is kept as written
1981        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        // Templating (Attribute::consider_context) relies on a JSON round trip
1992        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            // Configuration makes no sense for an absent driver
2013            ("State: !Absent\n  PersistenceMode: true\n", "expected unit"),
2014            // Proprietary modules need a pinned branch up to 610
2015            (
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            // Branches before 570 use other package names
2024            (
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            // The flavor is mandatory when a branch is given
2033            ("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        // Open modules exist on every branch from 570 on
2093        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        // On apt, the branch meta package pins the branch: no separate selection
2138        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        // dnf/zypper: the floating meta package, kept in the expected branch
2327        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        // From branch 615, cuda-drivers is a transitional package depending on nvidia-open
2372        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        // apt: the branch meta package pins the branch
2385        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        // The floating meta package is another driver, even in the expected branch
2410        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        // Another branch, another flavor
2419        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        // Latest branch: the floating meta package, not a branch meta package
2436        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        // Pinning packages and other nvidia-driver-* packages are not driver meta packages
2453        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}