summaryrefslogtreecommitdiff
path: root/cloudinit/config/cc_ubuntu_drivers.py
blob: 59347e2582f9c5d9539b7d9f3a5bce76207c2ac0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# This file is part of cloud-init. See LICENSE file for license information.

"""Ubuntu Drivers: Interact with third party drivers in Ubuntu."""

import os
from textwrap import dedent

from cloudinit.cloud import Cloud
from cloudinit.distros import Distro

try:
    import debconf

    HAS_DEBCONF = True
except ImportError:
    debconf = None
    HAS_DEBCONF = False

from logging import Logger

from cloudinit import log as logging
from cloudinit import subp, temp_utils, type_utils, util
from cloudinit.config import Config
from cloudinit.config.schema import MetaSchema, get_meta_doc
from cloudinit.settings import PER_INSTANCE

LOG = logging.getLogger(__name__)

distros = ["ubuntu"]

meta: MetaSchema = {
    "id": "cc_ubuntu_drivers",
    "name": "Ubuntu Drivers",
    "title": "Interact with third party drivers in Ubuntu.",
    "description": dedent(
        """\
        This module interacts with the 'ubuntu-drivers' command to install
        third party driver packages."""
    ),
    "distros": distros,
    "examples": [
        dedent(
            """\
        drivers:
          nvidia:
            license-accepted: true
        """
        )
    ],
    "frequency": PER_INSTANCE,
    "activate_by_schema_keys": ["drivers"],
}

__doc__ = get_meta_doc(meta)

OLD_UBUNTU_DRIVERS_STDERR_NEEDLE = (
    "ubuntu-drivers: error: argument <command>: invalid choice: 'install'"
)


# Use a debconf template to configure a global debconf variable
# (linux/nvidia/latelink) setting this to "true" allows the
# 'linux-restricted-modules' deb to accept the NVIDIA EULA and the package
# will automatically link the drivers to the running kernel.

NVIDIA_DEBCONF_CONTENT = """\
Template: linux/nvidia/latelink
Type: boolean
Default: true
Description: Late-link NVIDIA kernel modules?
 Enable this to link the NVIDIA kernel modules in cloud-init and
 make them available for use.
"""


X_LOADTEMPLATEFILE = "X_LOADTEMPLATEFILE"


def install_drivers(cfg, pkg_install_func, distro: Distro):
    if not isinstance(cfg, dict):
        raise TypeError(
            "'drivers' config expected dict, found '%s': %s"
            % (type_utils.obj_name(cfg), cfg)
        )

    cfgpath = "nvidia/license-accepted"
    # Call translate_bool to ensure that we treat string values like "yes" as
    # acceptance and _don't_ treat string values like "nah" as acceptance
    # because they're True-ish
    nv_acc = util.translate_bool(util.get_cfg_by_path(cfg, cfgpath))
    if not nv_acc:
        LOG.debug("Not installing NVIDIA drivers. %s=%s", cfgpath, nv_acc)
        return

    if not subp.which("ubuntu-drivers"):
        LOG.debug(
            "'ubuntu-drivers' command not available.  "
            "Installing ubuntu-drivers-common"
        )
        pkg_install_func(["ubuntu-drivers-common"])

    driver_arg = "nvidia"
    version_cfg = util.get_cfg_by_path(cfg, "nvidia/version")
    if version_cfg:
        driver_arg += ":{}".format(version_cfg)

    LOG.debug(
        "Installing and activating NVIDIA drivers (%s=%s, version=%s)",
        cfgpath,
        nv_acc,
        version_cfg if version_cfg else "latest",
    )

    # Register and set debconf selection linux/nvidia/latelink = true
    tdir = temp_utils.mkdtemp(dir=distro.get_tmp_exec_path(), needs_exe=True)
    debconf_file = os.path.join(tdir, "nvidia.template")
    try:
        util.write_file(debconf_file, NVIDIA_DEBCONF_CONTENT)
        with debconf.DebconfCommunicator("cloud-init") as dc:
            dc.command(X_LOADTEMPLATEFILE, debconf_file)
    except Exception as e:
        util.logexc(
            LOG, "Failed to register NVIDIA debconf template: %s", str(e)
        )
        raise
    finally:
        if os.path.isdir(tdir):
            util.del_dir(tdir)

    try:
        subp.subp(["ubuntu-drivers", "install", "--gpgpu", driver_arg])
    except subp.ProcessExecutionError as exc:
        if OLD_UBUNTU_DRIVERS_STDERR_NEEDLE in exc.stderr:
            LOG.warning(
                "the available version of ubuntu-drivers is"
                " too old to perform requested driver installation"
            )
        elif "No drivers found for installation." in exc.stdout:
            LOG.warning("ubuntu-drivers found no drivers for installation")
        raise


def handle(
    name: str, cfg: Config, cloud: Cloud, log: Logger, args: list
) -> None:
    if "drivers" not in cfg:
        log.debug("Skipping module named %s, no 'drivers' key in config", name)
        return
    if not HAS_DEBCONF:
        log.warning(
            "Skipping module named %s, 'python3-debconf' is not installed",
            name,
        )
        return

    install_drivers(
        cfg["drivers"], cloud.distro.install_packages, cloud.distro
    )