Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OEM: ensure only one kernel gets installed #1703

Merged
merged 4 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions apt-deps.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
build-essential
cloud-init
curl
dctrl-tools
fuseiso
gettext
gir1.2-umockdev-1.0
Expand Down
19 changes: 19 additions & 0 deletions autoinstall-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,25 @@
}
}
},
"oem": {
"type": "object",
"properties": {
"install": {
"oneOf": [
{
"type": "boolean"
},
{
"type": "string",
"const": "auto"
}
]
}
},
"required": [
"install"
]
},
"timezone": {
"type": "string"
},
Expand Down
16 changes: 16 additions & 0 deletions documentation/autoinstall-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,22 @@ Whether to install the ubuntu-restricted-addons package.

Whether to install the available third-party drivers.

<a name="oem"></a>

### oem

**type:** mapping, see below
**default:** see below
**can be interactive:** no

#### install

**type:** boolean or string (special value `auto`)
**default:**: `auto`

Whether to install the available OEM meta-packages. The special value `auto` - which is the default - enables the installation on ubuntu-desktop but not on ubuntu-server.
This option has no effect on core boot classic.

<a name="snaps"></a>

### snaps
Expand Down
19 changes: 19 additions & 0 deletions documentation/autoinstall-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,25 @@ The [JSON schema](https://json-schema.org/) for autoinstall data is as follows:
}
}
},
"oem": {
"type": "object",
"properties": {
"install": {
"oneOf": [
{
"type": "boolean"
},
{
"type": "string",
"const": "auto"
}
]
}
},
"required": [
"install"
]
},
"timezone": {
"type": "string"
},
Expand Down
1 change: 1 addition & 0 deletions snapcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ parts:
# This list includes the dependencies for curtin and probert as well,
# there doesn't seem to be any real benefit to listing them separately.
- cloud-init
- dctrl-tools
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could just stage /usr/bin/grep-dctrl but the whole package is fairly tiny.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - and no Depends so I guess I'll keep the whole package.

- iso-codes
- libpython3-stdlib
- libpython3.10-minimal
Expand Down
20 changes: 15 additions & 5 deletions subiquity/models/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,23 @@ class KernelModel:
# should be True.
explicitly_requested: bool = False

def render(self):
# If set to True, we won't request curthooks to install the kernel.
# We can use this option if the kernel is already part of the source image
# of if a kernel got installed using ubuntu-drivers.
curthooks_no_install: bool = False

@property
def needed_kernel(self) -> Optional[str]:
if self.metapkg_name_override is not None:
metapkg = self.metapkg_name_override
else:
metapkg = self.metapkg_name
return self.metapkg_name_override
return self.metapkg_name

def render(self):
if self.curthooks_no_install:
return {'kernel': None}

return {
'kernel': {
'package': metapkg,
'package': self.needed_kernel,
},
}
33 changes: 32 additions & 1 deletion subiquity/models/oem.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import logging
from typing import List, Optional
from typing import Any, Dict, List, Optional, Union

import attr

Expand All @@ -32,3 +32,34 @@ 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[OEMMetaPkg]] = None

# By default, 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).
self.install_on = {
"server": False,
"desktop": True,
}

def make_autoinstall(self) -> Dict[str, Union[str, bool]]:
server = self.install_on["server"]
desktop = self.install_on["desktop"]

if server and desktop:
return {"install": True}
if not server and not desktop:
return {"install": False}

# Having server = True and desktop = False is not supported.
assert desktop and not server

return {"install": "auto"}

def load_autoinstall_data(self, data: Dict[str, Any]) -> None:
if data["install"] == "auto":
self.install_on["server"] = False
self.install_on["desktop"] = True
return

self.install_on["server"] = data["install"]
self.install_on["desktop"] = data["install"]
22 changes: 21 additions & 1 deletion subiquity/server/controllers/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
)
from subiquitycore.context import with_context
from subiquitycore.file_util import write_file, generate_config_yaml
from subiquitycore.utils import log_process_streams
from subiquitycore.utils import arun_command, log_process_streams

from subiquity.common.errorreport import ErrorReportKind
from subiquity.common.types import (
Expand All @@ -51,6 +51,9 @@
run_curtin_command,
start_curtin_command,
)
from subiquity.server.kernel import (
list_installed_kernels,
)
from subiquity.server.mounter import (
Mounter,
)
Expand Down Expand Up @@ -312,6 +315,17 @@ async def run_curtin_step(name, stages, step_config, source=None):
step_config=self.generic_config(),
source=source,
)
if self.app.opts.dry_run:
# In dry-run, extract does not do anything. Let's create what's
# needed manually. Ideally, we would not hardcode
# var/lib/dpkg/status because it is an implementation detail.
status = "var/lib/dpkg/status"
(root / status).parent.mkdir(parents=True, exist_ok=True)
await arun_command([
"cp", "-aT", "--",
str(Path("/") / status),
str(root / status),
])
await self.setup_target(context=context)

# For OEM, we basically mimic what ubuntu-drivers does:
Expand Down Expand Up @@ -343,6 +357,12 @@ async def run_curtin_step(name, stages, step_config, source=None):
for pkg in self.model.oem.metapkgs:
await self.install_package(package=pkg.name)

# If we already have a kernel installed, don't bother requesting
# curthooks to install it again or we might end up with two
# kernels.
if await list_installed_kernels(Path(self.tpath())):
self.model.kernel.curthooks_no_install = True

await run_curtin_step(
name="curthooks", stages=["curthooks"],
step_config=self.generic_config(),
Expand Down
1 change: 1 addition & 0 deletions subiquity/server/controllers/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def start(self):
with open(mp_file) as fp:
kernel_package = fp.read().strip()
self.model.metapkg_name = kernel_package
self.model.explicitly_requested = True
log.debug(f'Using kernel {kernel_package} due to {mp_file}')
break
else:
Expand Down
43 changes: 36 additions & 7 deletions subiquity/server/controllers/oem.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,25 @@ class OEMController(SubiquityController):

endpoint = API.oem

model_name = "oem"
autoinstall_key = model_name = "oem"
autoinstall_schema = {
"type": "object",
"properties": {
"install": {
"oneOf": [
{
"type": "boolean",
},
{
"type": "string",
"const": "auto",
},
],
},
},
"required": ["install"],
}
autoinstall_default = {"install": "auto"}

def __init__(self, app) -> None:
super().__init__(app)
Expand Down Expand Up @@ -67,6 +85,12 @@ async def list_and_mark_configured() -> None:
self.load_metapkgs_task = asyncio.create_task(
list_and_mark_configured())

def make_autoinstall(self):
return self.model.make_autoinstall()

def load_autoinstall_data(self, *args, **kwargs) -> None:
self.model.load_autoinstall_data(*args, **kwargs)

async def wants_oem_kernel(self, pkgname: str,
*, context, overlay) -> bool:
""" For a given package, tell whether it wants the OEM or the default
Expand Down Expand Up @@ -100,12 +124,17 @@ 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")
# Only look for OEM meta-packages on supported variants and if we are
# not running core boot.
variant: str = self.app.base_model.source.current.variant
fs_controller = self.app.controllers.Filesystem
if fs_controller.is_core_boot_classic():
log.debug("listing of OEM meta-packages disabled on core boot"
" classic")
self.model.metapkgs = []
return
if not self.model.install_on[variant]:
log.debug("listing of OEM meta-packages disabled on %s", variant)
self.model.metapkgs = []
return

Expand Down
30 changes: 30 additions & 0 deletions subiquity/server/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
# 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 pathlib
import subprocess
from typing import List

from subiquitycore.lsb_release import lsb_release
from subiquitycore.utils import arun_command


def flavor_to_pkgname(flavor: str, *, dry_run: bool) -> str:
Expand All @@ -27,3 +32,28 @@ def flavor_to_pkgname(flavor: str, *, dry_run: bool) -> str:
# that's a bit tricky until we get cleverer about
# the apt config in general.
return f'linux-{flavor}-{release}'


async def list_installed_kernels(rootfs: pathlib.Path) -> List[str]:
''' Return the list of linux-image packages installed in rootfs. '''
# TODO use python-apt instead coupled with rootdir.
# Ideally, we should not hardcode var/lib/dpkg/status which is an
# implementation detail.
try:
cp = await arun_command([
'grep-status',
'--whole-pkg',
'-FProvides', 'linux-image', '--and', '-FStatus', 'installed',
'--show-field=Package',
'--no-field-names',
str(rootfs / pathlib.Path('var/lib/dpkg/status')),
], check=True)
except subprocess.CalledProcessError as cpe:
# grep-status exits with status 1 when there is no match.
if cpe.returncode != 1:
raise
stdout = cpe.stdout
else:
stdout = cp.stdout

return [line for line in stdout.splitlines() if line]
40 changes: 39 additions & 1 deletion subiquity/server/tests/test_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@
# 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 subprocess
import unittest
from unittest import mock

from subiquity.server.kernel import flavor_to_pkgname
from subiquity.server.kernel import (
flavor_to_pkgname,
list_installed_kernels,
)


class TestFlavorToPkgname(unittest.TestCase):
Expand All @@ -33,3 +38,36 @@ def test_flavor_hwe(self):

self.assertEqual('linux-generic-hwe-20.04',
flavor_to_pkgname('generic-hwe', dry_run=True))


class TestListInstalledKernels(unittest.IsolatedAsyncioTestCase):
async def test_one_kernel(self):
rv = subprocess.CompletedProcess([], 0)
rv.stdout = 'linux-image-6.1.0-16-generic'
with mock.patch('subiquity.server.kernel.arun_command',
return_value=rv) as mock_arun:
ret = await list_installed_kernels('/')
self.assertEqual(['linux-image-6.1.0-16-generic'], ret)
mock_arun.assert_called_once()

async def test_two_kernels(self):
rv = subprocess.CompletedProcess([], 0)
rv.stdout = '''\
linux-image-6.1.0-16-generic
linux-image-6.2.0-24-generic
'''
with mock.patch('subiquity.server.kernel.arun_command',
return_value=rv) as mock_arun:
ret = await list_installed_kernels('/')
self.assertEqual(['linux-image-6.1.0-16-generic',
'linux-image-6.2.0-24-generic'], ret)
mock_arun.assert_called_once()

async def test_no_kernel(self):
rv = subprocess.CompletedProcess([], 0)
rv.stdout = '\n'
with mock.patch('subiquity.server.kernel.arun_command',
return_value=rv) as mock_arun:
ret = await list_installed_kernels('/')
self.assertEqual([], ret)
mock_arun.assert_called_once()