Skip to content

Commit

Permalink
Merge pull request #2129 from Chris-Peterson444/add-core-variant-support
Browse files Browse the repository at this point in the history
 Introduce support for "core" variant in TUI
  • Loading branch information
Chris-Peterson444 authored Jan 21, 2025
2 parents 782dbb6 + e859f80 commit 88e8fc7
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 28 deletions.
6 changes: 0 additions & 6 deletions examples/answers/core-desktop.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,12 @@ Refresh:
update: no
Keyboard:
layout: us
Zdev:
accept-default: yes
Network:
accept-default: yes
Proxy:
proxy: ""
Filesystem:
guided: yes
guided-index: 0
UbuntuPro:
token: ""
InstallProgress:
reboot: yes
Drivers:
install: yes
56 changes: 35 additions & 21 deletions subiquity/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,27 +95,41 @@ def make_model(self):
def make_ui(self):
return SubiquityUI(self, self.help_menu)

controllers = [
"Serial",
"Welcome",
"Refresh",
"Keyboard",
"Source",
"Zdev",
"Network",
"Proxy",
"Mirror",
"Refresh",
"Filesystem",
"Identity",
"UbuntuPro",
"SSH",
"Drivers",
"SnapList",
"Progress",
]

variant_to_controllers: Dict[str, List[str]] = {}
variant_to_controllers: Dict[str, List[str]] = {
"server": [
"Serial",
"Welcome",
"Refresh",
"Keyboard",
"Source",
"Zdev",
"Network",
"Proxy",
"Mirror",
"Refresh",
"Filesystem",
"Identity",
"UbuntuPro",
"SSH",
"Drivers",
"SnapList",
"Progress",
],
"core": [
"Serial",
"Welcome",
"Refresh",
"Keyboard",
"Network",
"Refresh",
"Source",
"Filesystem",
"Progress",
],
}

# Set default controllerset
controllers = variant_to_controllers["server"]

def __init__(self, opts, about_msg=None):
if is_linux_tty():
Expand Down
12 changes: 12 additions & 0 deletions subiquity/client/controllers/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from subiquity.client.controller import SubiquityTuiController
from subiquity.ui.views.source import SourceView
from subiquitycore.tuicontroller import Skip

log = logging.getLogger("subiquity.client.controllers.source")

Expand All @@ -26,6 +27,17 @@ class SourceController(SubiquityTuiController):

async def make_ui(self):
sources = await self.endpoint.GET()

# When we have a source screen with only one source and don't need to
# ask about drivers, let's skip showing the screen and mark it
# configured.

# The "core" variant doesn't care about drivers
if self.app.variant == "core":
if len(sources.sources) == 1:
await self.endpoint.POST(source_id=sources.current_id)
raise Skip

return SourceView(
self, sources.sources, sources.current_id, sources.search_drivers
)
Expand Down
59 changes: 59 additions & 0 deletions subiquity/client/controllers/tests/test_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright 2025 Canonical, Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3.
#
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from unittest.mock import AsyncMock, Mock, patch

from subiquity.client.controllers.source import SourceController
from subiquity.common.types import SourceSelectionAndSetting
from subiquitycore.tests import SubiTestCase
from subiquitycore.tests.mocks import make_app
from subiquitycore.tests.parameterized import parameterized
from subiquitycore.tuicontroller import Skip


class TestSourceController(SubiTestCase):
def setUp(self):
self.app = make_app()
self.app.client = AsyncMock()
self.app.show_error_report = Mock()
self.app.show_nonreportable_error = Mock()

self.controller = SourceController(self.app)

@parameterized.expand(
(
("core", 1, True), # core is the only driverless variant
("core", 2, False), # don't skip if core has multiple sources
("server", 1, False), # Any other variant shouldn't skip
)
)
async def test_make_ui__skip_simple_sources(self, variant, sources, skip):
"""Test source screen is skipped for single-source, driverless media."""

self.app.variant = variant
resp = SourceSelectionAndSetting(
sources=[Mock() for i in range(sources)],
current_id=0,
search_drivers=None,
)

with (
patch("subiquity.client.controllers.source.SourceView"),
patch.object(self.controller.endpoint, "GET", return_value=resp),
):
if not skip:
await self.controller.make_ui()
else:
with self.assertRaises(Skip):
await self.controller.make_ui()
Empty file.
44 changes: 44 additions & 0 deletions subiquity/client/tests/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright 2025 Canonical, Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3.
#
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from unittest.mock import Mock

from subiquity.client.client import SubiquityClient
from subiquitycore.tests import SubiTestCase


class TestClientVariantSupport(SubiTestCase):
async def asyncSetUp(self):
opts = Mock()
opts.dry_run = True
opts.output_base = self.tmp_dir()
opts.machine_config = "examples/machines/simple.json"
opts.answers = None
self.client = SubiquityClient(opts, None)
self.client.make_apport_report = Mock()

def test_default_variant(self):
expected = SubiquityClient.variant_to_controllers["server"]
self.assertEqual(
# The controllers attribute is a list of names before init
SubiquityClient.controllers,
expected,
"default controller names do not match names for 'server' variant",
)
self.assertEqual(
# The controllers attribute is a ControllerSet after init
self.client.controllers.controller_names,
expected,
"controllers changed unexpectedly during init",
)
4 changes: 3 additions & 1 deletion subiquity/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ async def mark_configured_POST(self, endpoint_names: List[str]) -> None:
if controller.endpoint in endpoints:
await controller.configured()

# TODO: Make post to /meta/client_variant a RecoverableError (it doesn't
# have to be fatal and it's currently only pseudo-fatal).
async def client_variant_POST(self, variant: str) -> None:
if variant not in self.app.supported_variants:
raise ValueError(f"unrecognized client variant {variant}")
Expand Down Expand Up @@ -283,7 +285,7 @@ class SubiquityServer(Application):
"Shutdown",
]

supported_variants = ["server", "desktop"]
supported_variants = ["server", "desktop", "core"]

def make_model(self):
root = "/"
Expand Down
26 changes: 26 additions & 0 deletions subiquity/tests/api/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from aiohttp.client_exceptions import ClientResponseError

from subiquitycore.tests import SubiTestCase
from subiquitycore.tests.parameterized import parameterized
from subiquitycore.utils import astart_command, matching_dicts

default_timeout = 10
Expand Down Expand Up @@ -2304,3 +2305,28 @@ async def test_disable_dead_NICS_on_view(self):

ethernets = conf_data["network"]["ethernets"]
self.assertIn("ens4", ethernets)


class TestServerVariantSupport(TestAPI):
@parameterized.expand(
(
("server", True),
("desktop", True),
("core", True),
("foo-bar", False),
)
)
async def test_supported_variants(self, variant, is_supported):
async with start_server("examples/machines/simple.json") as inst:
if is_supported:
await inst.post("/meta/client_variant", variant=variant)
else:
with self.assertRaises(ClientResponseError) as ctx:
await inst.post("/meta/client_variant", variant=variant)
cre = ctx.exception
self.assertEqual(500, cre.status)
self.assertIn("x-error-report", cre.headers)
self.assertEqual(
"unrecognized client variant foo-bar",
json.loads(cre.headers["x-error-msg"]),
)

0 comments on commit 88e8fc7

Please sign in to comment.