diff --git a/documentation/autoinstall-reference.md b/documentation/autoinstall-reference.md index bb8449a8..42959641 100644 --- a/documentation/autoinstall-reference.md +++ b/documentation/autoinstall-reference.md @@ -139,9 +139,15 @@ The version of Subiquity released with 20.04 GA does not accept `null` for this #### search_drivers **type:** boolean -**default:** `true` +**default:** `true` (mostly, see below) -Whether the installer should search for available third-party drivers. When set to `false`, it disables the drivers screen and [section](#drivers). +Whether the installer should search for available third-party +drivers. When set to `false`, it disables the drivers screen and +[section](#drivers). + +The default is true for most installs but false when a "core boot" or +"enhanced secure boot" method is selected (where third-party drivers +cannot currently be installed). #### id **type:** string diff --git a/subiquity/common/types.py b/subiquity/common/types.py index 69b5f1cb..712f2699 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -387,6 +387,8 @@ class GuidedCapability(enum.Enum): class GuidedDisallowedCapabilityReason(enum.Enum): TOO_SMALL = enum.auto() CORE_BOOT_ENCRYPTION_UNAVAILABLE = enum.auto() + NOT_UEFI = enum.auto() + THIRD_PARTY_DRIVERS = enum.auto() @attr.s(auto_attribs=True) diff --git a/subiquity/server/controllers/drivers.py b/subiquity/server/controllers/drivers.py index a0f24431..333102ba 100644 --- a/subiquity/server/controllers/drivers.py +++ b/subiquity/server/controllers/drivers.py @@ -21,6 +21,7 @@ from subiquity.common.apidef import API from subiquity.common.types import DriversPayload, DriversResponse from subiquity.server.apt import OverlayCleanupError from subiquity.server.controller import SubiquityController +from subiquity.server.controllers.source import SEARCH_DRIVERS_AUTOINSTALL_DEFAULT from subiquity.server.types import InstallerChannels from subiquity.server.ubuntu_drivers import ( CommandNotFoundError, @@ -123,6 +124,8 @@ class DriversController(SubiquityController): await self.list_drivers_done_event.wait() search_drivers = self.app.controllers.Source.model.search_drivers + if search_drivers is SEARCH_DRIVERS_AUTOINSTALL_DEFAULT: + search_drivers = True return DriversResponse( install=self.model.do_install, diff --git a/subiquity/server/controllers/filesystem.py b/subiquity/server/controllers/filesystem.py index 5af288a3..bda5f03a 100644 --- a/subiquity/server/controllers/filesystem.py +++ b/subiquity/server/controllers/filesystem.py @@ -22,7 +22,7 @@ import os import pathlib import select import time -from typing import Any, Dict, List, Optional, Union +from typing import Any, Callable, Dict, List, Optional, Union import attr import pyudev @@ -74,6 +74,7 @@ from subiquity.models.filesystem import ( ) from subiquity.server import snapdapi from subiquity.server.controller import SubiquityController +from subiquity.server.controllers.source import SEARCH_DRIVERS_AUTOINSTALL_DEFAULT from subiquity.server.mounter import Mounter from subiquity.server.snapdapi import ( StorageEncryptionSupport, @@ -123,7 +124,7 @@ class CapabilityInfo: allowed: List[GuidedCapability] = attr.Factory(list) disallowed: List[GuidedDisallowedCapability] = attr.Factory(list) - def combine(self, other: "CapabilityInfo"): + def combine(self, other: "CapabilityInfo") -> None: for allowed_cap in other.allowed: if allowed_cap not in self.allowed: self.allowed.append(allowed_cap) @@ -140,6 +141,38 @@ class CapabilityInfo: self.allowed.sort() self.disallowed.sort() + def copy(self) -> "CapabilityInfo": + return CapabilityInfo( + allowed=list(self.allowed), disallowed=list(self.disallowed) + ) + + def disallow_if( + self, + filter: Callable[[GuidedCapability], bool], + reason: GuidedDisallowedCapabilityReason, + message: Optional[str] = None, + ) -> None: + new_allowed = [] + for cap in self.allowed: + if filter(cap): + self.disallowed.append( + GuidedDisallowedCapability( + capability=cap, + reason=reason, + message=message, + ) + ) + else: + new_allowed.append(cap) + self.allowed = new_allowed + + def disallow_all( + self, + reason: GuidedDisallowedCapabilityReason, + message: Optional[str] = None, + ) -> None: + self.disallow_if(lambda cap: True, reason, message) + @attr.s(auto_attribs=True) class VariationInfo: @@ -160,22 +193,15 @@ class VariationInfo: gap: gaps.Gap, install_min: int, ) -> CapabilityInfo: - r = CapabilityInfo() - r.disallowed = list(self.capability_info.disallowed) if gap is None: gap_size = 0 else: gap_size = gap.size - if self.capability_info.allowed and gap_size < install_min: - for capability in self.capability_info.allowed: - r.disallowed.append( - GuidedDisallowedCapability( - capability=capability, - reason=GuidedDisallowedCapabilityReason.TOO_SMALL, - ) - ) - else: - r.allowed = list(self.capability_info.allowed) + r = self.capability_info.copy() + if gap_size < install_min: + r.disallow_all( + reason=GuidedDisallowedCapabilityReason.TOO_SMALL, + ) return r @classmethod @@ -267,6 +293,11 @@ class FilesystemController(SubiquityController, FilesystemManipulator): self._configured = True if self._info is None: self.set_info_for_capability(GuidedCapability.DIRECT) + if ( + self.app.base_model.source.search_drivers + is SEARCH_DRIVERS_AUTOINSTALL_DEFAULT + ): + self.app.base_model.source.search_drivers = not self.is_core_boot_classic() await super().configured() self.stop_listening_udev() @@ -383,12 +414,40 @@ class FilesystemController(SubiquityController, FilesystemManipulator): if not self.app.opts.enhanced_secureboot: log.debug("Not offering enhanced_secureboot: commandline disabled") continue - if self.model.bootloader != Bootloader.UEFI: - log.debug("Not offering core boot based install: not a UEFI system") - continue info = self.info_for_system(name, label, system) - if info is not None: - self._variation_info[name] = info + if info is None: + continue + if self.model.bootloader != Bootloader.UEFI: + log.debug( + "Disabling core boot based install options on non-UEFI " + "system" + ) + info.capability_info.disallow_if( + lambda cap: cap.is_core_boot(), + GuidedDisallowedCapabilityReason.NOT_UEFI, + _( + "Enhanced secure boot options only available on UEFI " + "systems." + ), + ) + search_drivers = self.app.base_model.source.search_drivers + if ( + search_drivers is not SEARCH_DRIVERS_AUTOINSTALL_DEFAULT + and search_drivers + ): + log.debug( + "Disabling core boot based install options as third-party " + "drivers selected" + ) + info.capability_info.disallow_if( + lambda cap: cap.is_core_boot(), + GuidedDisallowedCapabilityReason.THIRD_PARTY_DRIVERS, + _( + "Enhanced secure boot options cannot currently install " + "third party drivers." + ), + ) + self._variation_info[name] = info elif catalog_entry.type.startswith("dd-"): min_size = variation.size self._variation_info[name] = VariationInfo.dd( diff --git a/subiquity/server/controllers/source.py b/subiquity/server/controllers/source.py index 54406458..c2e41f6e 100644 --- a/subiquity/server/controllers/source.py +++ b/subiquity/server/controllers/source.py @@ -14,6 +14,7 @@ # along with this program. If not, see . import contextlib +import logging import os from typing import Any, Optional @@ -29,6 +30,8 @@ from subiquity.common.types import SourceSelection, SourceSelectionAndSetting from subiquity.server.controller import SubiquityController from subiquity.server.types import InstallerChannels +log = logging.getLogger("subiquity.server.controllers.source") + def _translate(d, lang): if lang: @@ -50,6 +53,9 @@ def convert_source(source, lang): ) +SEARCH_DRIVERS_AUTOINSTALL_DEFAULT = object() + + class SourceController(SubiquityController): model_name = "source" @@ -67,10 +73,6 @@ class SourceController(SubiquityController): }, }, } - # Defaults to true for backward compatibility with existing autoinstall - # configurations. Back then, then users were able to install third-party - # drivers without this field. - autoinstall_default = {"search_drivers": True} def __init__(self, app): super().__init__(app) @@ -86,13 +88,15 @@ class SourceController(SubiquityController): def load_autoinstall_data(self, data: Any) -> None: if data is None: - # NOTE: The JSON schema does not allow data to be null in this - # context. However, Subiquity bypasses the schema validation when - # a section is set to null. So in practice, we can have data = None - # here. - data = {**self.autoinstall_default, "id": None} + data = {} - self.model.search_drivers = data.get("search_drivers", True) + # Defaults to almost-true for backward compatibility with existing autoinstall + # configurations. Back then, then users were able to install third-party drivers + # without this field. The "almost-true" part is that search_drivers defaults to + # False for core boot classic installs. + self.model.search_drivers = data.get( + "search_drivers", SEARCH_DRIVERS_AUTOINSTALL_DEFAULT + ) # At this point, the model has not yet loaded the sources from the # catalog. So we store the ID and lean on self.start to select the @@ -122,10 +126,14 @@ class SourceController(SubiquityController): cur_lang = self.app.base_model.locale.selected_language cur_lang = cur_lang.rsplit(".", 1)[0] + search_drivers = self.model.search_drivers + if search_drivers is SEARCH_DRIVERS_AUTOINSTALL_DEFAULT: + search_drivers = True + return SourceSelectionAndSetting( [convert_source(source, cur_lang) for source in self.model.sources], self.model.current.id, - search_drivers=self.model.search_drivers, + search_drivers=search_drivers, ) def get_handler( diff --git a/subiquity/server/controllers/tests/test_filesystem.py b/subiquity/server/controllers/tests/test_filesystem.py index f891d789..36fd47cb 100644 --- a/subiquity/server/controllers/tests/test_filesystem.py +++ b/subiquity/server/controllers/tests/test_filesystem.py @@ -1144,6 +1144,7 @@ class TestCoreBootInstallMethods(IsolatedAsyncioTestCase): self.app.dr_cfg = DRConfig() self.app.dr_cfg.systems_dir_exists = True self.app.controllers.Source.get_handler.return_value = TrivialSourceHandler("") + self.app.base_model.source.search_drivers = False self.fsc = FilesystemController(app=self.app) self.fsc._configured = True self.fsc.model = make_model(Bootloader.UEFI) diff --git a/subiquity/ui/views/filesystem/guided.py b/subiquity/ui/views/filesystem/guided.py index 00fae251..e720c8b4 100644 --- a/subiquity/ui/views/filesystem/guided.py +++ b/subiquity/ui/views/filesystem/guided.py @@ -235,6 +235,15 @@ class GuidedChoiceForm(SubForm): self.use_tpm.help = self.tpm_choice.help self.use_tpm.help = self.tpm_choice.help.format(reason=reason) else: + self.use_tpm.enabled = False + core_boot_disallowed = [ + d for d in val.disallowed if d.capability.is_core_boot() + ] + if core_boot_disallowed: + self.use_tpm.help = core_boot_disallowed[0].message + else: + self.use_tpm.help = "" + self.tpm_choice = None def _toggle_lvm(self, sender, val):