Move the calls to ubuntu-drivers out of the drivers controller

We now provide implementation for different interfaces to list and
install drivers using ubuntu-drivers. The implementations are outside
the drivers controller.

We have:
 * the normal interface that calls ubuntu-drivers in the root directory
   specified (i.e., in /target or an overlay).
 * a dry-run interface that calls the system's ubuntu-drivers for
   listing the drivers available (but does not install anything).
   The listing of drivers will fail if --recommended is not an available
   option in the system's ubuntu-drivers implementation.
 * a dry-run interface that returns an hard-coded list of drivers and
   does nothing on installation.
 * a dry-run interface that returns an empty list of drivers.

Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
This commit is contained in:
Olivier Gayot 2022-02-22 18:07:12 +01:00
parent b0eb063288
commit de148289aa
4 changed files with 183 additions and 54 deletions

View File

@ -15,7 +15,6 @@
import asyncio
import logging
import subprocess
from typing import List, Optional
from subiquitycore.context import with_context
@ -23,8 +22,12 @@ from subiquitycore.context import with_context
from subiquity.common.apidef import API
from subiquity.common.types import DriversResponse
from subiquity.server.controller import SubiquityController
from subiquity.server.curtin import run_curtin_command
from subiquity.server.types import InstallerChannels
from subiquity.server.ubuntu_drivers import (
CommandNotFoundError,
UbuntuDriversInterface,
get_ubuntu_drivers_interface,
)
log = logging.getLogger('subiquity.server.controllers.drivers')
@ -46,6 +49,11 @@ class DriversController(SubiquityController):
drivers: Optional[List[str]] = None
def __init__(self, app) -> None:
super().__init__(app)
self.ubuntu_drivers: UbuntuDriversInterface = \
get_ubuntu_drivers_interface(app)
def make_autoinstall(self):
return {
"install": self.model.do_install,
@ -67,49 +75,17 @@ class DriversController(SubiquityController):
with context.child("wait_apt"):
await self._wait_apt.wait()
apt = self.app.controllers.Mirror.apt_configurer
# TODO make sure --recommended is a supported option
cmd = ['ubuntu-drivers', 'list', '--recommended']
server: bool = self.app.base_model.source.current.variant == "server"
if server:
cmd.append('--gpgpu')
if self.app.opts.dry_run:
if 'has-drivers' in self.app.debug_flags:
if server:
self.drivers = ["nvidia-driver-470-server"]
else:
self.drivers = ["nvidia-driver-510"]
return
elif 'run-drivers' in self.app.debug_flags:
pass
else:
self.drivers = []
await self.configured()
return
async with apt.overlay() as d:
try:
await self.app.command_runner.run(
['chroot', d.mountpoint,
'sh', '-c',
"command -v ubuntu-drivers"])
except subprocess.CalledProcessError:
# Make sure ubuntu-drivers is available.
self.ubuntu_drivers.ensure_cmd_exists(d.mountpoint)
except CommandNotFoundError:
self.drivers = []
await self.configured()
return
result = await run_curtin_command(
self.app, context, "in-target", "-t", d.mountpoint,
"--", *cmd, capture=True)
# Drivers are listed one per line, but each is followed by a
# linux-modules-* package (which we are not interested in) ; e.g.:
# $ ubuntu-drivers list --recommended
# nvidia-driver-470 linux-modules-nvidia-470-generic-hwe-20.04
self.drivers = []
# Currently we have no way to specify universal_newlines=True or
# encoding="utf-8" to run_curtin_command.
stdout = result.stdout.decode("utf-8")
for line in [x.strip() for x in stdout.split("\n")]:
if not line:
continue
self.drivers.append(line.split(" ", maxsplit=1)[0])
else:
self.drivers = await self.ubuntu_drivers.list_drivers(
root_dir=d.mountpoint,
context=context)
log.debug("Available drivers to install: %s", self.drivers)
if not self.drivers:
await self.configured()

View File

@ -200,12 +200,9 @@ class InstallController(SubiquityController):
with context.child(
"ubuntu-drivers-install",
"installing third-party drivers") as child:
cmd = ["ubuntu-drivers", "install"]
if self.model.source.current.variant == 'server':
cmd.append('--gpgpu')
await run_curtin_command(
self.app, child, "in-target", "-t", self.tpath(),
"--", *cmd)
ubuntu_drivers = self.app.controllers.Drivers.ubuntu_drivers
await ubuntu_drivers.install_drivers(root_dir=self.tpath(),
context=child)
if self.model.network.has_network:
self.app.update_state(ApplicationState.UU_RUNNING)

View File

@ -57,12 +57,6 @@ class DryRunCommandRunner(LoggedCommandRunner):
async def start(self, cmd, *, capture=False):
if 'scripts/replay-curtin-log.py' in cmd:
delay = 0
elif cmd[-4:] == ['ubuntu-drivers', 'list', '--recommended', '--gpgpu']:
cmd = cmd[-4:]
delay = 0
elif cmd[-3:] == ['ubuntu-drivers', 'list', '--recommended']:
cmd = cmd[-3:]
delay = 0
else:
cmd = ['echo', 'not running:'] + cmd
if 'unattended-upgrades' in cmd:

View File

@ -0,0 +1,162 @@
# Copyright 2022 Canonical, Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Module that defines helpers to use the ubuntu-drivers command. """
from abc import ABC, abstractmethod
import logging
import subprocess
from typing import List, Type
from subiquity.server.curtin import run_curtin_command
from subiquitycore.utils import arun_command
log = logging.getLogger("subiquity.server.ubuntu_drivers")
class CommandNotFoundError(Exception):
""" Exception to be raised when the ubuntu-drivers command is not
available.
"""
class UbuntuDriversInterface(ABC):
def __init__(self, app, gpgpu: bool) -> None:
self.app = app
self.list_drivers_cmd = [
"ubuntu-drivers", "list",
"--recommended",
]
self.install_drivers_cmd = [
"ubuntu-drivers", "install",
]
if gpgpu:
self.list_drivers_cmd.append("--gpgpu")
self.install_drivers_cmd.append("--gpgpu")
@abstractmethod
async def ensure_cmd_exists(self, root_dir: str) -> None:
pass
@abstractmethod
async def list_drivers(self, root_dir: str, context) -> List[str]:
pass
async def install_drivers(self, root_dir: str, context) -> None:
await run_curtin_command(
self.app, context,
"in-target", "-t", root_dir, "--", *self.install_drivers_cmd)
def _drivers_from_output(self, output: str) -> List[str]:
""" Parse the output of ubuntu-drivers list --recommended and return a
list of drivers. """
drivers: List[str] = []
# Drivers are listed one per line, but each driver is followed by a
# linux-modules-* package (which we are not interested in showing).
# e.g.,:
# $ ubuntu-drivers list --recommended
# nvidia-driver-470 linux-modules-nvidia-470-generic-hwe-20.04
for line in [x.strip() for x in output.split("\n")]:
if not line:
continue
drivers.append(line.split(" ", maxsplit=1)[0])
return drivers
class UbuntuDriversClientInterface(UbuntuDriversInterface):
""" UbuntuDrivers interface that uses the ubuntu-drivers command from the
specified root directory. """
async def ensure_cmd_exists(self, root_dir: str) -> None:
# TODO This does not tell us if the "--recommended" option is
# available.
try:
await self.app.command_runner.run(
['chroot', root_dir,
'sh', '-c',
"command -v ubuntu-drivers"])
except subprocess.CalledProcessError:
raise CommandNotFoundError(
f"Command ubuntu-drivers is not available in {root_dir}")
async def list_drivers(self, root_dir: str, context) -> List[str]:
result = await run_curtin_command(
self.app, context,
"in-target", "-t", root_dir, "--", *self.list_drivers_cmd,
capture=True)
# Currently we have no way to specify universal_newlines=True or
# encoding="utf-8" to run_curtin_command so we need to decode the
# output.
return self._drivers_from_output(result.stdout.decode("utf-8"))
class UbuntuDriversHasDriversInterface(UbuntuDriversInterface):
""" A dry-run implementation of ubuntu-drivers that returns a hard-coded
list of drivers. """
gpgpu_drivers: List[str] = ["nvidia-driver-470-server"]
not_gpgpu_drivers: List[str] = ["nvidia-driver-510"]
def __init__(self, app, gpgpu: bool) -> None:
super().__init__(app, gpgpu)
self.drivers = self.gpgpu_drivers if gpgpu else self.not_gpgpu_drivers
async def ensure_cmd_exists(self, root_dir: str) -> None:
pass
async def list_drivers(self, root_dir: str, context) -> List[str]:
return self.drivers
class UbuntuDriversNoDriversInterface(UbuntuDriversHasDriversInterface):
""" A dry-run implementation of ubuntu-drivers that returns a hard-coded
empty list of drivers. """
gpgpu_drivers: List[str] = []
not_gpgpu_drivers: List[str] = []
class UbuntuDriversRunDriversInterface(UbuntuDriversInterface):
""" A dry-run implementation of ubuntu-drivers that actually runs the
ubuntu-drivers command but locally. """
async def ensure_cmd_exists(self, root_dir: str) -> None:
# TODO This does not tell us if the "--recommended" option is
# available.
try:
await arun_command(["command", "-v", "ubuntu-drivers"])
except subprocess.CalledProcessError:
raise CommandNotFoundError(
"Command ubuntu-drivers is not available in this system")
async def list_drivers(self, root_dir: str, context) -> List[str]:
# We run the command locally - ignoring the root_dir.
result = await arun_command(self.list_drivers_cmd)
return self._drivers_from_output(result.stdout)
def get_ubuntu_drivers_interface(app) -> UbuntuDriversInterface:
is_server = app.base_model.source.current.variant == "server"
cls: Type[UbuntuDriversInterface] = UbuntuDriversClientInterface
if app.opts.dry_run:
if 'has-drivers' in app.debug_flags:
cls = UbuntuDriversHasDriversInterface
elif 'run-drivers' in app.debug_flags:
cls = UbuntuDriversRunDriversInterface
else:
cls = UbuntuDriversNoDriversInterface
return cls(app, gpgpu=is_server)