Merge pull request #1702 from ogayot/install-oem-kernel-selection

Override requested kernel flavor on certified hardware
This commit is contained in:
Olivier Gayot 2023-06-30 20:52:05 +02:00 committed by GitHub
commit 30dff3f8d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 213 additions and 28 deletions

View File

@ -13,14 +13,29 @@
# 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/>.
from typing import Optional
class KernelModel:
metapkg_name = None
# The name of the kernel metapackage that we intend to install.
metapkg_name: Optional[str] = None
# During the installation, if we detect that a different kernel version is
# needed (OEM being a common use-case), we can override the metapackage
# name.
metapkg_name_override: Optional[str] = None
# If we explicitly request a kernel through autoinstall, this attribute
# should be True.
explicitly_requested: bool = False
def render(self):
if self.metapkg_name_override is not None:
metapkg = self.metapkg_name_override
else:
metapkg = self.metapkg_name
return {
'kernel': {
'package': self.metapkg_name,
'package': metapkg,
},
}

View File

@ -16,11 +16,19 @@
import logging
from typing import List, Optional
import attr
log = logging.getLogger('subiquity.models.oem')
@attr.s(auto_attribs=True)
class OEMMetaPkg:
name: str
wants_oem_kernel: bool
class OEMModel:
def __init__(self):
# List of OEM metapackages relevant to the current hardware.
# When the list is None, it has not yet been retrieved.
self.metapkgs: Optional[List[str]] = None
self.metapkgs: Optional[List[OEMMetaPkg]] = None

View File

@ -324,11 +324,11 @@ class InstallController(SubiquityController):
# NOTE In ubuntu-drivers, this is done in a single call to apt-get
# install.
for pkg in self.model.oem.metapkgs:
await self.install_package(package=pkg)
await self.install_package(package=pkg.name)
if self.model.network.has_network:
for pkg in self.model.oem.metapkgs:
source_list = f"/etc/apt/sources.list.d/{pkg}.list"
source_list = f"/etc/apt/sources.list.d/{pkg.name}.list"
await run_curtin_command(
self.app, context,
"in-target", "-t", self.tpath(), "--",
@ -341,7 +341,7 @@ class InstallController(SubiquityController):
# NOTE In ubuntu-drivers, this is done in a single call to
# apt-get install.
for pkg in self.model.oem.metapkgs:
await self.install_package(package=pkg)
await self.install_package(package=pkg.name)
await run_curtin_step(
name="curthooks", stages=["curthooks"],

View File

@ -16,9 +16,8 @@
import logging
import os
from subiquitycore.lsb_release import lsb_release
from subiquity.server.controller import NonInteractiveController
from subiquity.server.kernel import flavor_to_pkgname
log = logging.getLogger("subiquity.server.controllers.kernel")
@ -75,20 +74,13 @@ class KernelController(NonInteractiveController):
package = data.get('package')
flavor = data.get('flavor')
if package is None:
if flavor is None or flavor == 'generic':
package = 'linux-generic'
else:
if flavor == 'hwe':
flavor = 'generic-hwe'
# Should check this package exists really but
# that's a bit tricky until we get cleverer about
# the apt config in general.
dry_run: bool = self.app.opts.dry_run
package = 'linux-{flavor}-{release}'.format(
flavor=flavor,
release=lsb_release(dry_run=dry_run)['release'])
dry_run: bool = self.app.opts.dry_run
if flavor is None:
flavor = 'generic'
package = flavor_to_pkgname(flavor, dry_run=dry_run)
log.debug(f'Using kernel {package} due to autoinstall')
self.model.metapkg_name = package
self.model.explicitly_requested = True
def make_autoinstall(self):
return {'package': self.model.metapkg_name}

View File

@ -15,14 +15,17 @@
import asyncio
import logging
from typing import Optional
from typing import List, Optional
from subiquitycore.context import with_context
from subiquity.common.apidef import API
from subiquity.common.types import OEMResponse
from subiquity.models.oem import OEMMetaPkg
from subiquity.server.apt import OverlayCleanupError
from subiquity.server.controller import SubiquityController
from subiquity.server.curtin import run_curtin_command
from subiquity.server.kernel import flavor_to_pkgname
from subiquity.server.types import InstallerChannels
from subiquity.server.ubuntu_drivers import (
CommandNotFoundError,
@ -45,24 +48,67 @@ class OEMController(SubiquityController):
self.ubuntu_drivers = get_ubuntu_drivers_interface(self.app)
self.load_metapkgs_task: Optional[asyncio.Task] = None
self.kernel_configured_event = asyncio.Event()
def start(self) -> None:
self._wait_apt = asyncio.Event()
self.app.hub.subscribe(
InstallerChannels.APT_CONFIGURED,
self._wait_apt.set)
self.app.hub.subscribe(
(InstallerChannels.CONFIGURED, "kernel"),
self.kernel_configured_event.set)
async def list_and_mark_configured() -> None:
await self.load_metapackages_list()
await self.ensure_no_kernel_conflict()
await self.configured()
self.load_metapkgs_task = asyncio.create_task(
list_and_mark_configured())
async def wants_oem_kernel(self, pkgname: str,
*, context, overlay) -> bool:
""" For a given package, tell whether it wants the OEM or the default
kernel flavor. We look for the Ubuntu-Oem-Kernel-Flavour attribute in
the package meta-data. If the attribute is present and has the value
"default", then return False. Otherwise, return True. """
result = await run_curtin_command(
self.app, context,
"in-target", "-t", overlay.mountpoint, "--",
"apt-cache", "show", pkgname,
capture=True, private_mounts=True)
for line in result.stdout.decode("utf-8").splitlines():
if not line.startswith("Ubuntu-Oem-Kernel-Flavour:"):
continue
flavor = line.split("=", maxsplit=1)[1].strip()
if flavor == "default":
return False
elif flavor == "oem":
return True
else:
log.warning("%s wants unexpected kernel flavor: %s",
pkgname, flavor)
return True
log.warning("%s has no Ubuntu-Oem-Kernel-Flavour", pkgname)
return True
@with_context()
async def load_metapackages_list(self, context) -> None:
with context.child("wait_apt"):
await self._wait_apt.wait()
# Skip looking for OEM meta-packages if we are running ubuntu-server.
# OEM meta-packages expect the default kernel flavor to be HWE (which
# is only true for ubuntu-desktop).
if self.app.base_model.source.current.variant == "server":
log.debug("not listing OEM meta-packages since we are installing"
" ubuntu-server")
self.model.metapkgs = []
return
apt = self.app.controllers.Mirror.final_apt_configurer
try:
async with apt.overlay() as d:
@ -72,14 +118,60 @@ class OEMController(SubiquityController):
except CommandNotFoundError:
self.model.metapkgs = []
else:
self.model.metapkgs = await self.ubuntu_drivers.list_oem(
metapkgs: List[str] = await self.ubuntu_drivers.list_oem(
root_dir=d.mountpoint,
context=context)
self.model.metapkgs = [
OEMMetaPkg(
name=name,
wants_oem_kernel=await self.wants_oem_kernel(
name, context=context, overlay=d),
) for name in metapkgs]
except OverlayCleanupError:
log.exception("Failed to cleanup overlay. Continuing anyway.")
for pkg in self.model.metapkgs:
if pkg.wants_oem_kernel:
kernel_model = self.app.base_model.kernel
kernel_model.metapkg_name_override = flavor_to_pkgname(
pkg.name, dry_run=self.app.opts.dry_run)
log.debug("overriding kernel flavor because of OEM")
log.debug("OEM meta-packages to install: %s", self.model.metapkgs)
async def ensure_no_kernel_conflict(self) -> None:
kernel_model = self.app.base_model.kernel
await self.kernel_configured_event.wait()
if self.model.metapkgs and kernel_model.explicitly_requested:
# TODO
# This should be a dialog or something rather than the content of
# an exception, really. But this is a simple way to print out
# something in autoinstall.
msg = _("""\
A specific kernel flavor was requested but it cannot be satistified when \
installing on certified hardware.
You should either disable the installation of OEM meta-packages using the \
following autoinstall snippet or let the installer decide which kernel to
install.
oem:
install: false
""")
raise RuntimeError(msg)
@with_context()
async def apply_autoinstall_config(self, context) -> None:
await self.load_metapkgs_task
await self.ensure_no_kernel_conflict()
async def GET(self, wait: bool = False) -> OEMResponse:
if wait:
await asyncio.shield(self.load_metapkgs_task)
return OEMResponse(metapackages=self.model.metapkgs)
if self.model.metapkgs is None:
metapkgs = None
else:
metapkgs = [pkg.name for pkg in self.model.metapkgs]
return OEMResponse(metapackages=metapkgs)

View File

@ -0,0 +1,29 @@
# Copyright 2023 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/>.
from subiquitycore.lsb_release import lsb_release
def flavor_to_pkgname(flavor: str, *, dry_run: bool) -> str:
if flavor == 'generic':
return 'linux-generic'
if flavor == 'hwe':
flavor = 'generic-hwe'
release = lsb_release(dry_run=dry_run)['release']
# Should check this package exists really but
# that's a bit tricky until we get cleverer about
# the apt config in general.
return f'linux-{flavor}-{release}'

View File

@ -0,0 +1,35 @@
# Copyright 2023 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/>.
import unittest
from subiquity.server.kernel import flavor_to_pkgname
class TestFlavorToPkgname(unittest.TestCase):
def test_flavor_generic(self):
self.assertEqual('linux-generic',
flavor_to_pkgname('generic', dry_run=True))
def test_flavor_oem(self):
self.assertEqual('linux-oem-20.04',
flavor_to_pkgname('oem', dry_run=True))
def test_flavor_hwe(self):
self.assertEqual('linux-generic-hwe-20.04',
flavor_to_pkgname('hwe', dry_run=True))
self.assertEqual('linux-generic-hwe-20.04',
flavor_to_pkgname('generic-hwe', dry_run=True))

View File

@ -1707,11 +1707,13 @@ class TestOEM(TestAPI):
resp = await inst.get('/oem', wait=True)
self.assertEqual(expected_pkgs, resp['metapackages'])
async def test_listing_certified(self):
expected_pkgs = ['oem-somerville-tentacool-meta']
async def _test_listing_certified(self, source_id: str,
expected: List[str]):
with patch.dict(os.environ, {'SUBIQUITY_DEBUG': 'has-drivers'}):
async with start_server('examples/simple.json') as inst:
await inst.post('/source', source_id='ubuntu-server')
args = ['--source-catalog', 'examples/mixed-sources.yaml']
config = 'examples/simple.json'
async with start_server(config, extra_args=args) as inst:
await inst.post('/source', source_id=source_id)
names = ['locale', 'keyboard', 'source', 'network', 'proxy',
'mirror', 'storage']
await inst.post('/meta/mark_configured', endpoint_names=names)
@ -1720,7 +1722,19 @@ class TestOEM(TestAPI):
await inst.get('/meta/status', cur='NEEDS_CONFIRMATION')
resp = await inst.get('/oem', wait=True)
self.assertEqual(expected_pkgs, resp['metapackages'])
self.assertEqual(expected, resp['metapackages'])
async def test_listing_certified_ubuntu_server(self):
# Listing of OEM meta-packages is intentionally disabled on
# ubuntu-server.
await self._test_listing_certified(
source_id='ubuntu-server',
expected=[])
async def test_listing_certified_ubuntu_desktop(self):
await self._test_listing_certified(
source_id='ubuntu-desktop',
expected=['oem-somerville-tentacool-meta'])
class TestSource(TestAPI):