Merge pull request #1702 from ogayot/install-oem-kernel-selection
Override requested kernel flavor on certified hardware
This commit is contained in:
commit
30dff3f8d9
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}'
|
|
@ -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))
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue