From 02e5bc3c1753b704ee4f90090fe20efe02fefb61 Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Wed, 12 Jul 2023 13:03:29 +1200 Subject: [PATCH 1/3] rename WLANSupportInstallState to PackageInstallState --- subiquity/client/controllers/network.py | 4 ++-- subiquity/common/apidef.py | 4 ++-- subiquity/common/types.py | 4 ++-- subiquity/server/controllers/network.py | 32 ++++++++++++------------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/subiquity/client/controllers/network.py b/subiquity/client/controllers/network.py index d81a67fc5..852f9f502 100644 --- a/subiquity/client/controllers/network.py +++ b/subiquity/client/controllers/network.py @@ -36,7 +36,7 @@ from subiquity.common.apidef import LinkAction, NetEventAPI from subiquity.common.types import ( ErrorReportKind, - WLANSupportInstallState, + PackageInstallState, ) log = logging.getLogger('subiquity.client.controllers.network') @@ -91,7 +91,7 @@ async def apply_error_POST(self, stage: str) -> None: self.view.show_network_error(stage) async def wlan_support_install_finished_POST( - self, state: WLANSupportInstallState) -> None: + self, state: PackageInstallState) -> None: if self.view: self.view.update_for_wlan_support_install_state(state.name) diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py index eda5e21bd..88e457d6b 100644 --- a/subiquity/common/apidef.py +++ b/subiquity/common/apidef.py @@ -78,7 +78,7 @@ UPCSInitiateResponse, UPCSWaitResponse, UsernameValidation, - WLANSupportInstallState, + PackageInstallState, ZdevInfo, WSLConfigurationBase, WSLConfigurationAdvanced, @@ -484,7 +484,7 @@ class LinkAction(enum.Enum): @api class NetEventAPI: class wlan_support_install_finished: - def POST(state: WLANSupportInstallState) -> None: ... + def POST(state: PackageInstallState) -> None: ... class update_link: def POST(act: LinkAction, info: Payload[NetDevInfo]) -> None: ... diff --git a/subiquity/common/types.py b/subiquity/common/types.py index 88bf940b0..76b251b55 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -233,7 +233,7 @@ def typeclass(self): return self.type -class WLANSupportInstallState(enum.Enum): +class PackageInstallState(enum.Enum): NOT_NEEDED = enum.auto() NOT_AVAILABLE = enum.auto() INSTALLING = enum.auto() @@ -244,7 +244,7 @@ class WLANSupportInstallState(enum.Enum): @attr.s(auto_attribs=True) class NetworkStatus: devices: List[NetDevInfo] - wlan_support_install_state: WLANSupportInstallState + wlan_support_install_state: PackageInstallState class ProbeStatus(enum.Enum): diff --git a/subiquity/server/controllers/network.py b/subiquity/server/controllers/network.py index ad03df773..f6578f912 100644 --- a/subiquity/server/controllers/network.py +++ b/subiquity/server/controllers/network.py @@ -44,7 +44,7 @@ from subiquity.common.errorreport import ErrorReportKind from subiquity.common.types import ( NetworkStatus, - WLANSupportInstallState, + PackageInstallState, ) from subiquity.server.controller import SubiquityController @@ -126,11 +126,11 @@ def maybe_start_install_wpasupplicant(self): def wlan_support_install_state(self): if self.install_wpasupplicant_task is None: - return WLANSupportInstallState.NOT_NEEDED + return PackageInstallState.NOT_NEEDED elif self.install_wpasupplicant_task.done(): return self.install_wpasupplicant_task.result() else: - return WLANSupportInstallState.INSTALLING + return PackageInstallState.INSTALLING async def _install_wpasupplicant(self): if self.opts.dry_run: @@ -139,12 +139,12 @@ async def _install_wpasupplicant(self): for k in self.app.debug_flags: if k.startswith('wlan_install='): a = k.split('=', 2)[1] - r = getattr(WLANSupportInstallState, a) + r = getattr(PackageInstallState, a) else: r = await self._really_install_wpasupplicant() log.debug("wlan_support_install_finished %s", r) self._call_clients("wlan_support_install_finished", r) - if r == WLANSupportInstallState.DONE: + if r == PackageInstallState.DONE: for dev in self.pending_wlan_devices: self._send_update(LinkAction.NEW, dev) self.pending_wlan_devices = set() @@ -156,15 +156,15 @@ async def _really_install_wpasupplicant(self): binpkg = cache.get('wpasupplicant') if not binpkg: log.debug('wpasupplicant not found') - return WLANSupportInstallState.NOT_AVAILABLE + return PackageInstallState.NOT_AVAILABLE if binpkg.installed: log.debug('wpasupplicant already installed') - return WLANSupportInstallState.DONE + return PackageInstallState.DONE if not binpkg.candidate.uri.startswith('cdrom:'): log.debug( 'wpasupplicant not available from cdrom (rather %s)', binpkg.candidate.uri) - return WLANSupportInstallState.NOT_AVAILABLE + return PackageInstallState.NOT_AVAILABLE env = os.environ.copy() env['DEBIAN_FRONTEND'] = 'noninteractive' apt_opts = [ @@ -176,9 +176,9 @@ async def _really_install_wpasupplicant(self): ['apt-get', 'install'] + apt_opts + ['wpasupplicant'], env=env) log.debug('apt-get install wpasupplicant returned %s', cp) if cp.returncode == 0: - return WLANSupportInstallState.DONE + return PackageInstallState.DONE else: - return WLANSupportInstallState.FAILED + return PackageInstallState.FAILED def load_autoinstall_data(self, data): if data is not None: @@ -284,7 +284,7 @@ async def GET(self) -> NetworkStatus: self.apply_config(silent=True) self.view_shown = True if self.wlan_support_install_state() == \ - WLANSupportInstallState.DONE: + PackageInstallState.DONE: devices = self.model.get_all_netdevs() else: devices = [ @@ -298,7 +298,7 @@ async def GET(self) -> NetworkStatus: async def configured(self): self.model.has_network = self.network_event_receiver.has_default_route self.model.needs_wpasupplicant = ( - self.wlan_support_install_state() == WLANSupportInstallState.DONE) + self.wlan_support_install_state() == PackageInstallState.DONE) await super().configured() async def POST(self) -> None: @@ -374,13 +374,13 @@ def new_link(self, dev): if dev.type == 'wlan': self.maybe_start_install_wpasupplicant() state = self.wlan_support_install_state() - if state == WLANSupportInstallState.INSTALLING: + if state == PackageInstallState.INSTALLING: self.pending_wlan_devices.add(dev) return - elif state in [WLANSupportInstallState.FAILED, - WLANSupportInstallState.NOT_AVAILABLE]: + elif state in [PackageInstallState.FAILED, + PackageInstallState.NOT_AVAILABLE]: return - # WLANSupportInstallState.DONE falls through + # PackageInstallState.DONE falls through self._send_update(LinkAction.NEW, dev) def update_link(self, dev): From 743c350b1f912e0887c06b1854d15ddf7f72ee40 Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Wed, 12 Jul 2023 13:21:46 +1200 Subject: [PATCH 2/3] factor out code to install a package from the pool in the live session --- subiquity/server/controllers/network.py | 43 +----------- subiquity/server/pkghelper.py | 87 +++++++++++++++++++++++++ subiquity/server/server.py | 2 + 3 files changed, 91 insertions(+), 41 deletions(-) create mode 100644 subiquity/server/pkghelper.py diff --git a/subiquity/server/controllers/network.py b/subiquity/server/controllers/network.py index f6578f912..ccee3bdd8 100644 --- a/subiquity/server/controllers/network.py +++ b/subiquity/server/controllers/network.py @@ -15,13 +15,10 @@ import asyncio import logging -import os from typing import List, Optional import aiohttp -import apt - from subiquitycore.async_helpers import ( run_bg_task, schedule_task, @@ -33,7 +30,6 @@ StaticConfig, WLANConfig, ) -from subiquitycore.utils import arun_command from subiquity.common.api.client import make_client_for_conn from subiquity.common.apidef import ( @@ -125,12 +121,7 @@ def maybe_start_install_wpasupplicant(self): self._install_wpasupplicant()) def wlan_support_install_state(self): - if self.install_wpasupplicant_task is None: - return PackageInstallState.NOT_NEEDED - elif self.install_wpasupplicant_task.done(): - return self.install_wpasupplicant_task.result() - else: - return PackageInstallState.INSTALLING + return self.app.package_installer.state_for_pkg('wpasupplicant') async def _install_wpasupplicant(self): if self.opts.dry_run: @@ -141,7 +132,7 @@ async def _install_wpasupplicant(self): a = k.split('=', 2)[1] r = getattr(PackageInstallState, a) else: - r = await self._really_install_wpasupplicant() + r = await self.app.package_installer.install_pkg('wpasupplicant') log.debug("wlan_support_install_finished %s", r) self._call_clients("wlan_support_install_finished", r) if r == PackageInstallState.DONE: @@ -150,36 +141,6 @@ async def _install_wpasupplicant(self): self.pending_wlan_devices = set() return r - async def _really_install_wpasupplicant(self): - log.debug('checking if wpasupplicant is available') - cache = apt.Cache() - binpkg = cache.get('wpasupplicant') - if not binpkg: - log.debug('wpasupplicant not found') - return PackageInstallState.NOT_AVAILABLE - if binpkg.installed: - log.debug('wpasupplicant already installed') - return PackageInstallState.DONE - if not binpkg.candidate.uri.startswith('cdrom:'): - log.debug( - 'wpasupplicant not available from cdrom (rather %s)', - binpkg.candidate.uri) - return PackageInstallState.NOT_AVAILABLE - env = os.environ.copy() - env['DEBIAN_FRONTEND'] = 'noninteractive' - apt_opts = [ - '--quiet', '--assume-yes', - '--option=Dpkg::Options::=--force-unsafe-io', - '--option=Dpkg::Options::=--force-confold', - ] - cp = await arun_command( - ['apt-get', 'install'] + apt_opts + ['wpasupplicant'], env=env) - log.debug('apt-get install wpasupplicant returned %s', cp) - if cp.returncode == 0: - return PackageInstallState.DONE - else: - return PackageInstallState.FAILED - def load_autoinstall_data(self, data): if data is not None: self.ai_data = data diff --git a/subiquity/server/pkghelper.py b/subiquity/server/pkghelper.py new file mode 100644 index 000000000..f2fa55f59 --- /dev/null +++ b/subiquity/server/pkghelper.py @@ -0,0 +1,87 @@ +# 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 . + +import asyncio +import logging +import os +from typing import Dict, Optional + +import apt + +from subiquitycore.utils import arun_command + +from subiquity.common.types import PackageInstallState + +log = logging.getLogger('subiquity.server.pkghelper') + + +class PackageInstaller: + + def __init__(self): + self.pkgs: Dict[str, asyncio.Task] = {} + self._cache: Optional[apt.Cache] = None + + @property + def cache(self): + if self._cache is None: + self._cache = apt.Cache() + return self._cache + + def state_for_pkg(self, pkgname: str) -> PackageInstallState: + t = self.pkgs.get(pkgname) + if t is None: + return PackageInstallState.NOT_NEEDED + if t.done(): + return t.result() + else: + return PackageInstallState.INSTALLING + + def start_installing_pkg(self, pkgname: str) -> None: + if pkgname not in self.pkgs: + self.pkgs[pkgname] = asyncio.create_task( + self._install_pkg(pkgname)) + + async def install_pkg(self, pkgname) -> PackageInstallState: + self.start_installing_pkg(pkgname) + return await self.pkgs[pkgname] + + async def _install_pkg(self, pkgname: str) -> PackageInstallState: + log.debug('checking if %s is available', pkgname) + binpkg = self.cache.get(pkgname) + if not binpkg: + log.debug('%s not found', pkgname) + return PackageInstallState.NOT_AVAILABLE + if binpkg.installed: + log.debug('%s already installed', pkgname) + return PackageInstallState.DONE + if not binpkg.candidate.uri.startswith('cdrom:'): + log.debug( + '%s not available from cdrom (rather %s)', + pkgname, binpkg.candidate.uri) + return PackageInstallState.NOT_AVAILABLE + env = os.environ.copy() + env['DEBIAN_FRONTEND'] = 'noninteractive' + apt_opts = [ + '--quiet', '--assume-yes', + '--option=Dpkg::Options::=--force-unsafe-io', + '--option=Dpkg::Options::=--force-confold', + ] + cp = await arun_command( + ['apt-get', 'install'] + apt_opts + [pkgname], env=env) + log.debug('apt-get install %s returned %s', pkgname, cp) + if cp.returncode == 0: + return PackageInstallState.DONE + else: + return PackageInstallState.FAILED diff --git a/subiquity/server/server.py b/subiquity/server/server.py index 0f54d8166..beeef789d 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -79,6 +79,7 @@ HTTPGeoIPStrategy, ) from subiquity.server.errors import ErrorController +from subiquity.server.pkghelper import PackageInstaller from subiquity.server.runner import get_command_runner from subiquity.server.snapdapi import make_api_client from subiquity.server.types import InstallerChannels @@ -315,6 +316,7 @@ def __init__(self, opts, block_log_dir): self.event_syslog_id = 'subiquity_event.{}'.format(os.getpid()) self.log_syslog_id = 'subiquity_log.{}'.format(os.getpid()) self.command_runner = get_command_runner(self) + self.package_installer = PackageInstaller() self.error_reporter = ErrorReporter( self.context.child("ErrorReporter"), self.opts.dry_run, self.root) From cb1e760207f371735002678bc08b2caa3a8af36d Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Thu, 13 Jul 2023 11:37:43 +1200 Subject: [PATCH 3/3] add docstring to PackageInstaller --- subiquity/server/pkghelper.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/subiquity/server/pkghelper.py b/subiquity/server/pkghelper.py index f2fa55f59..7664f1e40 100644 --- a/subiquity/server/pkghelper.py +++ b/subiquity/server/pkghelper.py @@ -28,6 +28,12 @@ class PackageInstaller: + """Install packages from the pool on the ISO in the live session. + + Sometimes we need packages from the pool in the live session, for + example to install wpasupplicant when wlan interfaces are detected + by the server installer. + """ def __init__(self): self.pkgs: Dict[str, asyncio.Task] = {}