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

network: fix Wi-Fi interfaces not listed in dry-run #1785

Merged
merged 2 commits into from
Sep 7, 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
10 changes: 1 addition & 9 deletions subiquity/server/controllers/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,7 @@ def wlan_support_install_state(self):
return self.app.package_installer.state_for_pkg("wpasupplicant")

async def _install_wpasupplicant(self):
if self.opts.dry_run:
await asyncio.sleep(10 / self.app.scale_factor)
a = "DONE"
for k in self.app.debug_flags:
if k.startswith("wlan_install="):
a = k.split("=", 2)[1]
r = getattr(PackageInstallState, a)
else:
r = await self.app.package_installer.install_pkg("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:
Expand Down
54 changes: 46 additions & 8 deletions subiquity/server/pkghelper.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import asyncio
import logging
import os
from typing import Dict, Optional
from typing import Dict, List, Optional

import apt

Expand All @@ -34,10 +34,9 @@ class PackageInstaller:
by the server installer.
"""

def __init__(self, app):
def __init__(self):
self.pkgs: Dict[str, asyncio.Task] = {}
self._cache: Optional[apt.Cache] = None
self.app = app

@property
def cache(self):
Expand All @@ -58,7 +57,7 @@ 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:
async def install_pkg(self, pkgname: str) -> PackageInstallState:
self.start_installing_pkg(pkgname)
return await self.pkgs[pkgname]

Expand All @@ -71,10 +70,6 @@ async def _install_pkg(self, pkgname: str) -> PackageInstallState:
if binpkg.installed:
log.debug("%s already installed", pkgname)
return PackageInstallState.DONE
if self.app.opts.dry_run:
log.debug("dry-run apt-get install %s", pkgname)
await asyncio.sleep(2 / self.app.scale_factor)
return PackageInstallState.DONE
if not binpkg.candidate.uri.startswith("cdrom:"):
log.debug(
"%s not available from cdrom (rather %s)", pkgname, binpkg.candidate.uri
Expand All @@ -94,3 +89,46 @@ async def _install_pkg(self, pkgname: str) -> PackageInstallState:
return PackageInstallState.DONE
else:
return PackageInstallState.FAILED


class DryRunPackageInstaller(PackageInstaller):
def __init__(self, app) -> None:
super().__init__()
self.scale_factor: float = app.scale_factor
self.debug_flags: List[str] = app.debug_flags
self.package_specific_impl = {
"wpasupplicant": self._install_wpa_supplicant,
}

async def _install_wpa_supplicant(self) -> PackageInstallState:
"""Special implementation for wpasupplicant (used by code related to
Wi-Fi interfaces)."""
await asyncio.sleep(10 / self.scale_factor)
status = "DONE"
for flag in self.debug_flags:
if flag.startswith("wlan_install="):
status = flag.split("=", 2)[1]
return getattr(PackageInstallState, status)

async def _install_pkg(self, pkgname: str) -> PackageInstallState:
if pkgname in self.package_specific_impl:
return await self.package_specific_impl[pkgname]()

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
log.debug("dry-run apt-get install %s", pkgname)
await asyncio.sleep(2 / self.scale_factor)
return PackageInstallState.DONE


def get_package_installer(app):
if app.opts.dry_run:
return DryRunPackageInstaller(app)
else:
return PackageInstaller()
4 changes: 2 additions & 2 deletions subiquity/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
from subiquity.server.dryrun import DRConfig
from subiquity.server.errors import ErrorController
from subiquity.server.geoip import DryRunGeoIPStrategy, GeoIP, HTTPGeoIPStrategy
from subiquity.server.pkghelper import PackageInstaller
from subiquity.server.pkghelper import get_package_installer
from subiquity.server.runner import get_command_runner
from subiquity.server.snapdapi import make_api_client
from subiquity.server.types import InstallerChannels
Expand Down Expand Up @@ -291,7 +291,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)
self.package_installer = get_package_installer(self)

self.error_reporter = ErrorReporter(
self.context.child("ErrorReporter"), self.opts.dry_run, self.root
Expand Down
130 changes: 130 additions & 0 deletions subiquity/server/tests/test_pkghelper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# 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 typing import Optional
from unittest.mock import Mock, patch

import attr

from subiquity.server.pkghelper import (
DryRunPackageInstaller,
PackageInstaller,
PackageInstallState,
)
from subiquity.server.pkghelper import log as PkgHelperLogger
from subiquitycore.tests import SubiTestCase
from subiquitycore.tests.mocks import make_app


class MockPackage:
@attr.s(auto_attribs=True)
class Candidate:
uri: str

def __init__(
self, installed: bool, name: str, candidate_uri: Optional[str] = None
) -> None:
self.installed = installed
self.name = name
if candidate_uri is None:
self.candidate = None
else:
self.candidate = self.Candidate(uri=candidate_uri)


@patch("apt.Cache", Mock(return_value={}))
class TestPackageInstaller(SubiTestCase):
def setUp(self):
self.pkginstaller = PackageInstaller()

async def test_install_pkg_not_found(self):
self.assertEqual(
await self.pkginstaller.install_pkg("sysvinit-core"),
PackageInstallState.NOT_AVAILABLE,
)

async def test_install_pkg_already_installed(self):
with patch.dict(
self.pkginstaller.cache,
{"util-linux": MockPackage(installed=True, name="util-linux")},
):
self.assertEqual(
await self.pkginstaller.install_pkg("util-linux"),
PackageInstallState.DONE,
)

async def test_install_pkg_not_from_cdrom(self):
with patch.dict(
self.pkginstaller.cache,
{
"python3-attr": MockPackage(
installed=False,
name="python3-attr",
candidate_uri="http://archive.ubuntu.com",
)
},
):
self.assertEqual(
await self.pkginstaller.install_pkg("python3-attr"),
PackageInstallState.NOT_AVAILABLE,
)


@patch("apt.Cache", Mock(return_value={}))
@patch("subiquity.server.pkghelper.asyncio.sleep")
class TestDryRunPackageInstaller(SubiTestCase):
def setUp(self):
app = make_app()
app.debug_flags = []
self.pkginstaller = DryRunPackageInstaller(app)

async def test_install_pkg(self, sleep):
with patch.dict(
self.pkginstaller.cache,
{"python3-attr": MockPackage(installed=False, name="python3-attr")},
):
with self.assertLogs(PkgHelperLogger, "DEBUG") as debug:
self.assertEqual(
await self.pkginstaller.install_pkg("python3-attr"),
PackageInstallState.DONE,
)
sleep.assert_called_once()
self.assertIn(
"dry-run apt-get install %s", [record.msg for record in debug.records]
)

async def test_install_pkg_wpasupplicant_default_impl(self, sleep):
with patch.object(self.pkginstaller, "debug_flags", []):
self.assertEqual(
await self.pkginstaller.install_pkg("wpasupplicant"),
PackageInstallState.DONE,
)
sleep.assert_called_once()

async def test_install_pkg_wpasupplicant_done(self, sleep):
with patch.object(self.pkginstaller, "debug_flags", ["wlan_install=DONE"]):
self.assertEqual(
await self.pkginstaller.install_pkg("wpasupplicant"),
PackageInstallState.DONE,
)
sleep.assert_called_once()

async def test_install_pkg_wpasupplicant_failed(self, sleep):
with patch.object(self.pkginstaller, "debug_flags", ["wlan_install=FAILED"]):
self.assertEqual(
await self.pkginstaller.install_pkg("wpasupplicant"),
PackageInstallState.FAILED,
)
sleep.assert_called_once()