From c76a8f23e34a2b04e368dff94eced6805a7635ce Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Tue, 29 Mar 2022 15:18:56 +0200 Subject: [PATCH 01/10] source: give the option to search for third-party drivers The source screen now includes a checkbox that makes Subiquity search for third-party drivers in a later stage. Signed-off-by: Olivier Gayot --- subiquity/client/controllers/source.py | 21 ++++++++++---- subiquity/common/apidef.py | 2 +- subiquity/common/types.py | 1 + subiquity/models/source.py | 1 + subiquity/server/controllers/source.py | 6 ++-- subiquity/tests/api/test_api.py | 2 ++ subiquity/ui/views/source.py | 38 ++++++++++++++++++++++---- 7 files changed, 57 insertions(+), 14 deletions(-) diff --git a/subiquity/client/controllers/source.py b/subiquity/client/controllers/source.py index 7c212d10..8d3d17de 100644 --- a/subiquity/client/controllers/source.py +++ b/subiquity/client/controllers/source.py @@ -27,18 +27,27 @@ class SourceController(SubiquityTuiController): async def make_ui(self): sources = await self.endpoint.GET() - return SourceView(self, sources.sources, sources.current_id) + return SourceView(self, + sources.sources, + sources.current_id, + sources.search_drivers) def run_answers(self): + form = self.app.ui.body.form + if "search_drivers" in self.answers: + form.search_drivers.value = self.answers["search_drivers"] if 'source' in self.answers: wanted_id = self.answers['source'] - for bf in self.app.ui.body.form._fields: + for bf in form._fields: + if bf is form.search_drivers: + continue bf.value = bf.field.name == wanted_id - self.app.ui.body.form._click_done(None) + form._click_done(None) def cancel(self): self.app.prev_screen() - def done(self, source_id): - log.debug("SourceController.done source_id=%s", source_id) - self.app.next_screen(self.endpoint.POST(source_id)) + def done(self, source_id, search_drivers: bool): + log.debug("SourceController.done source_id=%s, search_drivers=%s", + source_id, search_drivers) + self.app.next_screen(self.endpoint.POST(source_id, search_drivers)) diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py index d5e5bb49..49bc8618 100644 --- a/subiquity/common/apidef.py +++ b/subiquity/common/apidef.py @@ -145,7 +145,7 @@ class API: class source: def GET() -> SourceSelectionAndSetting: ... - def POST(source_id: str) -> None: ... + def POST(source_id: str, search_drivers: bool) -> None: ... class zdev: def GET() -> List[ZdevInfo]: ... diff --git a/subiquity/common/types.py b/subiquity/common/types.py index 290c98cf..ab6ad68d 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -183,6 +183,7 @@ class SourceSelection: class SourceSelectionAndSetting: sources: List[SourceSelection] current_id: str + search_drivers: bool @attr.s(auto_attribs=True) diff --git a/subiquity/models/source.py b/subiquity/models/source.py index 1d766dbc..e1df107c 100644 --- a/subiquity/models/source.py +++ b/subiquity/models/source.py @@ -68,6 +68,7 @@ class SourceModel: self.current = fake_entries['server'] self.sources = [self.current] self.lang = None + self.search_drivers = False def load_from_file(self, fp): self._dir = os.path.dirname(fp.name) diff --git a/subiquity/server/controllers/source.py b/subiquity/server/controllers/source.py index 385ed3cf..af3681d3 100644 --- a/subiquity/server/controllers/source.py +++ b/subiquity/server/controllers/source.py @@ -86,7 +86,8 @@ class SourceController(SubiquityController): convert_source(source, cur_lang) for source in self.model.sources ], - self.model.current.id) + self.model.current.id, + search_drivers=self.model.search_drivers) async def configured(self): if self._handler is not None: @@ -100,7 +101,8 @@ class SourceController(SubiquityController): await super().configured() self.app.base_model.set_source_variant(self.model.current.variant) - async def POST(self, source_id: str) -> None: + async def POST(self, source_id: str, search_drivers: bool) -> None: + self.model.search_drivers = search_drivers for source in self.model.sources: if source.id == source_id: self.model.current = source diff --git a/subiquity/tests/api/test_api.py b/subiquity/tests/api/test_api.py index 00788e85..94d66cbe 100755 --- a/subiquity/tests/api/test_api.py +++ b/subiquity/tests/api/test_api.py @@ -948,6 +948,8 @@ class TestCancel(TestAPI): async def test_cancel_drivers(self): with patch.dict(os.environ, {'SUBIQUITY_DEBUG': 'has-drivers'}): async with start_server('examples/simple.json') as inst: + await inst.post('/source', source_id="dummy", + search_drivers=True) # /drivers?wait=true is expected to block until APT is # configured. # Let's make sure we cancel it. diff --git a/subiquity/ui/views/source.py b/subiquity/ui/views/source.py index 634c5d08..8687d4e3 100644 --- a/subiquity/ui/views/source.py +++ b/subiquity/ui/views/source.py @@ -14,13 +14,21 @@ # along with this program. If not, see . import logging -from urwid import connect_signal +from typing import List + +from urwid import ( + connect_signal, + Text, +) from subiquitycore.view import BaseView +from subiquitycore.ui.container import ListBox from subiquitycore.ui.form import ( + BooleanField, Form, RadioButtonField, ) +from subiquitycore.ui.utils import screen log = logging.getLogger('subiquity.ui.views.source') @@ -29,10 +37,10 @@ class SourceView(BaseView): title = _("Choose type of install") - def __init__(self, controller, sources, current_id): + def __init__(self, controller, sources, current_id, search_drivers: bool): self.controller = controller - group = [] + group: List[RadioButtonField] = [] ns = { 'cancel_label': _("Back"), @@ -47,6 +55,12 @@ class SourceView(BaseView): group, source.name, '\n' + source.description) initial[source.id] = source.id == current_id + ns["search_drivers"] = BooleanField( + _("Search for third-party drivers"), "\n" + + _("This software is subject to license terms included with its " + "documentation. Some is proprietary.")) + initial["search_drivers"] = search_drivers + SourceForm = type(Form)('SourceForm', (Form,), ns) log.debug('%r %r', ns, current_id) @@ -57,13 +71,27 @@ class SourceView(BaseView): excerpt = _("Choose the base for the installation.") - super().__init__(self.form.as_screen(excerpt=excerpt)) + # NOTE Hack to insert the "Additional options" text between two fields + # of the form. + rows = self.form.as_rows() + rows.insert(-2, Text("")) + rows.insert(-2, Text("Additional options")) + + super().__init__( + screen( + ListBox(rows), + self.form.buttons, + excerpt=excerpt, + focus_buttons=True)) def done(self, result): log.debug("User input: {}".format(result.as_data())) + search_drivers = result.as_data()["search_drivers"] for k, v in result.as_data().items(): + if k == "search_drivers": + continue if v: - self.controller.done(k) + self.controller.done(k, search_drivers=search_drivers) def cancel(self, result=None): self.controller.cancel() From 1ffff94eff1e65fd36b87fc725dec29a58882c76 Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Wed, 16 Mar 2022 16:19:31 +0100 Subject: [PATCH 02/10] drivers: show the drivers screen only if search drivers was checked Signed-off-by: Olivier Gayot --- subiquity/client/controllers/drivers.py | 8 ++++++-- subiquity/server/controllers/drivers.py | 8 ++++++++ subiquity/ui/views/drivers.py | 2 ++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/subiquity/client/controllers/drivers.py b/subiquity/client/controllers/drivers.py index 09d63d75..42d811bf 100644 --- a/subiquity/client/controllers/drivers.py +++ b/subiquity/client/controllers/drivers.py @@ -31,9 +31,13 @@ class DriversController(SubiquityTuiController): endpoint_name = 'drivers' async def make_ui(self) -> DriversView: - response: DriversResponse = await self.endpoint.GET() - if not response.drivers and response.drivers is not None: + source_endpoint = self.app.client.source + source_response = await source_endpoint.GET() + + if not source_response.search_drivers: raise Skip + + response: DriversResponse = await self.endpoint.GET() return DriversView(self, response.drivers, response.install) async def _wait_drivers(self) -> List[str]: diff --git a/subiquity/server/controllers/drivers.py b/subiquity/server/controllers/drivers.py index 96e6285d..4ac3c92b 100644 --- a/subiquity/server/controllers/drivers.py +++ b/subiquity/server/controllers/drivers.py @@ -74,6 +74,14 @@ class DriversController(SubiquityController): async def _list_drivers(self, context): with context.child("wait_apt"): await self._wait_apt.wait() + # The APT_CONFIGURED event (which unblocks _wait_apt.wait) is sent + # after the user confirms the destruction changes. At this point, the + # source is already mounted so the user can't go back all the way to + # the source screen to enable/disable the "search drivers" checkbox. + if not self.app.controllers.Source.model.search_drivers: + self.drivers = [] + await self.configured() + return apt = self.app.controllers.Mirror.apt_configurer async with apt.overlay() as d: try: diff --git a/subiquity/ui/views/drivers.py b/subiquity/ui/views/drivers.py index fa5ec9fa..1f4c430f 100644 --- a/subiquity/ui/views/drivers.py +++ b/subiquity/ui/views/drivers.py @@ -66,6 +66,8 @@ class DriversView(BaseView): if drivers is None: self.make_waiting(install) + elif not drivers: + self.make_no_drivers() else: self.make_main(install, drivers) From 57d5aef575bbaaf1e6a55fc355cb999f2c2c730e Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Wed, 16 Mar 2022 16:20:41 +0100 Subject: [PATCH 03/10] source: prevent to skip the drivers screen but allow to go back Signed-off-by: Olivier Gayot --- subiquity/ui/views/drivers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/subiquity/ui/views/drivers.py b/subiquity/ui/views/drivers.py index 1f4c430f..c9be2e5c 100644 --- a/subiquity/ui/views/drivers.py +++ b/subiquity/ui/views/drivers.py @@ -26,7 +26,7 @@ from urwid import ( Text, ) -from subiquitycore.ui.buttons import ok_btn +from subiquitycore.ui.buttons import back_btn, ok_btn from subiquitycore.ui.form import ( Form, BooleanField, @@ -81,10 +81,10 @@ class DriversView(BaseView): Text(""), self.spinner, ] - self.cont_btn = ok_btn( - _("Continue"), - on_press=lambda sender: self.done(False)) - self._w = screen(rows, [self.cont_btn]) + self.back_btn = back_btn( + _("Back"), + on_press=lambda sender: self.cancel()) + self._w = screen(rows, [self.back_btn]) asyncio.create_task(self._wait(install)) self.status = DriversViewStatus.WAITING From 595254159ec365ceea34e761bdb8984f125e5fd2 Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Wed, 30 Mar 2022 11:00:50 +0200 Subject: [PATCH 04/10] source: use lazy string interpolation Signed-off-by: Olivier Gayot --- subiquity/ui/views/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subiquity/ui/views/source.py b/subiquity/ui/views/source.py index 8687d4e3..1991a336 100644 --- a/subiquity/ui/views/source.py +++ b/subiquity/ui/views/source.py @@ -85,7 +85,7 @@ class SourceView(BaseView): focus_buttons=True)) def done(self, result): - log.debug("User input: {}".format(result.as_data())) + log.debug("User input: %s", result.as_data()) search_drivers = result.as_data()["search_drivers"] for k, v in result.as_data().items(): if k == "search_drivers": From d6048a691495e5f75072298e24652590335240ba Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Wed, 30 Mar 2022 17:52:21 +0200 Subject: [PATCH 05/10] drivers: use radio buttons instead of checkbox to install drivers Signed-off-by: Olivier Gayot --- subiquity/ui/views/drivers.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/subiquity/ui/views/drivers.py b/subiquity/ui/views/drivers.py index c9be2e5c..7f6d3098 100644 --- a/subiquity/ui/views/drivers.py +++ b/subiquity/ui/views/drivers.py @@ -29,7 +29,7 @@ from urwid import ( from subiquitycore.ui.buttons import back_btn, ok_btn from subiquitycore.ui.form import ( Form, - BooleanField, + RadioButtonField, ) from subiquitycore.ui.spinner import Spinner from subiquitycore.ui.utils import screen @@ -44,8 +44,16 @@ class DriversForm(Form): available drivers or not. """ cancel_label = _("Back") + ok_label = _("Continue") - install = BooleanField(_("Install the drivers")) + group: List[RadioButtonField] = [] + + install = RadioButtonField( + group, + _("Install all third-party drivers")) + do_not_install = RadioButtonField( + group, + _("Do not install third-party drivers now")) class DriversViewStatus(Enum): @@ -64,6 +72,13 @@ class DriversView(BaseView): install: bool) -> None: self.controller = controller + self.search_later = [ + Text(_("Note: You can search again later for third-party " + + "drivers using the command:")), + Text(""), + Text(" $ ubuntu-drivers list --recommended --gpgpu"), + ] + if drivers is None: self.make_waiting(install) elif not drivers: @@ -102,7 +117,10 @@ class DriversView(BaseView): """ Change the view into an information page that shows that no third-party drivers are available for installation. """ - rows = [Text(_("No applicable third-party drivers were found."))] + rows = [ + Text(_("No applicable third-party drivers were found.")), + Text(""), + ] + self.search_later self.cont_btn = ok_btn( _("Continue"), on_press=lambda sender: self.done(False)) @@ -111,7 +129,10 @@ class DriversView(BaseView): def make_main(self, install: bool, drivers: List[str]) -> None: """ Change the view to display the drivers form. """ - self.form = DriversForm(initial={'install': install}) + self.form = DriversForm(initial={ + "install": bool(install), + "do_not_install": (not install), + }) excerpt = _( "The following third-party drivers were found. " @@ -128,6 +149,8 @@ class DriversView(BaseView): rows = [Text(f"* {driver}") for driver in drivers] rows.append(Text("")) rows.extend(self.form.as_rows()) + rows.append(Text("")) + rows.extend(self.search_later) self._w = screen(rows, self.form.buttons, excerpt=excerpt) self.status = DriversViewStatus.MAIN From 6a2e8b6a4940994899f823f7f1ae60ff7e18c884 Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Thu, 31 Mar 2022 12:28:37 +0200 Subject: [PATCH 06/10] drivers: show different words when online vs offline Signed-off-by: Olivier Gayot --- subiquity/client/controllers/drivers.py | 3 +- subiquity/common/types.py | 2 ++ subiquity/server/controllers/drivers.py | 5 ++- subiquity/ui/views/drivers.py | 41 +++++++++++++++++++------ 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/subiquity/client/controllers/drivers.py b/subiquity/client/controllers/drivers.py index 42d811bf..82442338 100644 --- a/subiquity/client/controllers/drivers.py +++ b/subiquity/client/controllers/drivers.py @@ -38,7 +38,8 @@ class DriversController(SubiquityTuiController): raise Skip response: DriversResponse = await self.endpoint.GET() - return DriversView(self, response.drivers, response.install) + return DriversView(self, response.drivers, + response.install, response.local_only) async def _wait_drivers(self) -> List[str]: response: DriversResponse = await self.endpoint.GET(wait=True) diff --git a/subiquity/common/types.py b/subiquity/common/types.py index ab6ad68d..cf39d081 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -398,9 +398,11 @@ class DriversResponse: :drivers: tells what third-party drivers will be installed should we decide to do it. It will bet set to None until we figure out what drivers are available. + :local_only: tells if we are looking for drivers only from the ISO. """ install: bool drivers: Optional[List[str]] + local_only: bool @attr.s(auto_attribs=True) diff --git a/subiquity/server/controllers/drivers.py b/subiquity/server/controllers/drivers.py index 4ac3c92b..325b4f26 100644 --- a/subiquity/server/controllers/drivers.py +++ b/subiquity/server/controllers/drivers.py @@ -98,10 +98,13 @@ class DriversController(SubiquityController): await self.configured() async def GET(self, wait: bool = False) -> DriversResponse: + local_only = not self.app.base_model.network.has_network if wait: await asyncio.shield(self._drivers_task) + return DriversResponse(install=self.model.do_install, - drivers=self.drivers) + drivers=self.drivers, + local_only=local_only) async def POST(self, data: DriversPayload) -> None: self.model.do_install = data.install diff --git a/subiquity/ui/views/drivers.py b/subiquity/ui/views/drivers.py index 7f6d3098..d76dcd2b 100644 --- a/subiquity/ui/views/drivers.py +++ b/subiquity/ui/views/drivers.py @@ -69,12 +69,14 @@ class DriversView(BaseView): form = None def __init__(self, controller, drivers: Optional[List[str]], - install: bool) -> None: + install: bool, local_only: bool) -> None: self.controller = controller + self.local_only = local_only self.search_later = [ - Text(_("Note: You can search again later for third-party " + - "drivers using the command:")), + Text(_("Note: Once the installation has finished and you are " + + "connected to a network, you can search again for " + + "third-party drivers using the following command:")), Text(""), Text(" $ ubuntu-drivers list --recommended --gpgpu"), ] @@ -91,8 +93,17 @@ class DriversView(BaseView): asynchronously. """ self.spinner = Spinner(self.controller.app.aio_loop, style='dots') self.spinner.start() + + if self.local_only: + looking_for_drivers = _("Not connected to a network. " + + "Looking for applicable third-party " + + "drivers available locally...") + else: + looking_for_drivers = _("Looking for applicable third-party " + + "drivers available locally or online...") + rows = [ - Text(_("Looking for applicable third-party drivers...")), + Text(looking_for_drivers), Text(""), self.spinner, ] @@ -117,10 +128,18 @@ class DriversView(BaseView): """ Change the view into an information page that shows that no third-party drivers are available for installation. """ - rows = [ - Text(_("No applicable third-party drivers were found.")), - Text(""), - ] + self.search_later + if self.local_only: + no_drivers_found = _("No applicable third-party drivers are " + + "available locally.") + else: + no_drivers_found = _("No applicable third-party drivers are " + + "available locally or online.") + + rows = [Text(no_drivers_found)] + if self.local_only: + rows.append(Text("")) + rows.extend(self.search_later) + self.cont_btn = ok_btn( _("Continue"), on_press=lambda sender: self.done(False)) @@ -149,8 +168,10 @@ class DriversView(BaseView): rows = [Text(f"* {driver}") for driver in drivers] rows.append(Text("")) rows.extend(self.form.as_rows()) - rows.append(Text("")) - rows.extend(self.search_later) + + if self.local_only: + rows.append(Text("")) + rows.extend(self.search_later) self._w = screen(rows, self.form.buttons, excerpt=excerpt) self.status = DriversViewStatus.MAIN From 5a4b66553c5cafc4818d796bdf5ddd7703b1fd0a Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Thu, 31 Mar 2022 12:43:00 +0200 Subject: [PATCH 07/10] drivers: add button to go back when no drivers are available Signed-off-by: Olivier Gayot --- subiquity/ui/views/drivers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/subiquity/ui/views/drivers.py b/subiquity/ui/views/drivers.py index d76dcd2b..a10b707e 100644 --- a/subiquity/ui/views/drivers.py +++ b/subiquity/ui/views/drivers.py @@ -143,7 +143,10 @@ class DriversView(BaseView): self.cont_btn = ok_btn( _("Continue"), on_press=lambda sender: self.done(False)) - self._w = screen(rows, [self.cont_btn]) + self.back_btn = back_btn( + _("Back"), + on_press=lambda sender: self.cancel()) + self._w = screen(rows, [self.cont_btn, self.back_btn]) self.status = DriversViewStatus.NO_DRIVERS def make_main(self, install: bool, drivers: List[str]) -> None: From 79326cf2eb72fee5d62b7e653ab7aebea21e5284 Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Tue, 19 Apr 2022 14:50:52 +0200 Subject: [PATCH 08/10] source: run answers when only one source is configured Before, we would not run answers for the source controller if only one source was specified. The controller would automatically get marked configured. Since we're adding a new "search drivers" option in this screen, we need to make it interactive in integration tests now. Signed-off-by: Olivier Gayot --- examples/answers-bond.yaml | 3 +++ examples/answers-guided-lvm.yaml | 3 +++ examples/answers-imsm.yaml | 2 ++ examples/answers-lvm-dmcrypt.yaml | 2 ++ examples/answers-lvm.yaml | 2 ++ examples/answers-preserve.yaml | 2 ++ examples/answers-raid-lvm.yaml | 2 ++ examples/answers-raid.yaml | 2 ++ examples/answers-serial.yaml | 2 ++ examples/answers-swap.yaml | 2 ++ examples/answers.yaml | 2 ++ scripts/runtests.sh | 3 ++- subiquity/server/controllers/source.py | 5 ----- subiquity/tests/api/test_api.py | 3 ++- 14 files changed, 28 insertions(+), 7 deletions(-) diff --git a/examples/answers-bond.yaml b/examples/answers-bond.yaml index d9431b8a..dd085d4a 100644 --- a/examples/answers-bond.yaml +++ b/examples/answers-bond.yaml @@ -1,3 +1,6 @@ +Source: + source: ubuntu-server-minimal + search_drivers: true Welcome: lang: en_US Refresh: diff --git a/examples/answers-guided-lvm.yaml b/examples/answers-guided-lvm.yaml index cfbff2bf..3ff37944 100644 --- a/examples/answers-guided-lvm.yaml +++ b/examples/answers-guided-lvm.yaml @@ -1,3 +1,6 @@ +Source: + source: ubuntu-server + search_drivers: false Welcome: lang: en_US Refresh: diff --git a/examples/answers-imsm.yaml b/examples/answers-imsm.yaml index 0d0c3aa9..50ba63ca 100644 --- a/examples/answers-imsm.yaml +++ b/examples/answers-imsm.yaml @@ -1,4 +1,6 @@ #machine-config: examples/imsm.json +Source: + source: ubuntu-server Welcome: lang: en_US Refresh: diff --git a/examples/answers-lvm-dmcrypt.yaml b/examples/answers-lvm-dmcrypt.yaml index 5b1137c8..455205fe 100644 --- a/examples/answers-lvm-dmcrypt.yaml +++ b/examples/answers-lvm-dmcrypt.yaml @@ -1,3 +1,5 @@ +Source: + source: ubuntu-server-minimal Welcome: lang: en_US Refresh: diff --git a/examples/answers-lvm.yaml b/examples/answers-lvm.yaml index 858aec3c..601dbe55 100644 --- a/examples/answers-lvm.yaml +++ b/examples/answers-lvm.yaml @@ -1,3 +1,5 @@ +Source: + source: ubuntu-server Welcome: lang: en_US Refresh: diff --git a/examples/answers-preserve.yaml b/examples/answers-preserve.yaml index 48ea9a96..46dbb88a 100644 --- a/examples/answers-preserve.yaml +++ b/examples/answers-preserve.yaml @@ -1,4 +1,6 @@ #machine-config: examples/existing-partitions.json +Source: + source: ubuntu-server Welcome: lang: en_US Refresh: diff --git a/examples/answers-raid-lvm.yaml b/examples/answers-raid-lvm.yaml index bb970879..56ad06fa 100644 --- a/examples/answers-raid-lvm.yaml +++ b/examples/answers-raid-lvm.yaml @@ -1,3 +1,5 @@ +Source: + source: ubuntu-server Welcome: lang: en_US Refresh: diff --git a/examples/answers-raid.yaml b/examples/answers-raid.yaml index f5ab1c2b..ffeafe9a 100644 --- a/examples/answers-raid.yaml +++ b/examples/answers-raid.yaml @@ -1,3 +1,5 @@ +Source: + source: ubuntu-server Welcome: lang: en_US Refresh: diff --git a/examples/answers-serial.yaml b/examples/answers-serial.yaml index a7257dcb..89b967e9 100644 --- a/examples/answers-serial.yaml +++ b/examples/answers-serial.yaml @@ -1,4 +1,6 @@ #serial +Source: + source: ubuntu-server Serial: rich: false Welcome: diff --git a/examples/answers-swap.yaml b/examples/answers-swap.yaml index 6135c1eb..cf75d7bf 100644 --- a/examples/answers-swap.yaml +++ b/examples/answers-swap.yaml @@ -1,3 +1,5 @@ +Source: + source: ubuntu-server Welcome: lang: en_US Refresh: diff --git a/examples/answers.yaml b/examples/answers.yaml index 03ef4cc9..88c76794 100644 --- a/examples/answers.yaml +++ b/examples/answers.yaml @@ -1,3 +1,5 @@ +Source: + source: ubuntu-server Welcome: lang: en_US Refresh: diff --git a/scripts/runtests.sh b/scripts/runtests.sh index 960ca47f..e9389034 100755 --- a/scripts/runtests.sh +++ b/scripts/runtests.sh @@ -161,7 +161,8 @@ for answers in examples/answers*.yaml; do "${opts[@]}" \ --machine-config "$config" \ --bootloader uefi \ - --snaps-from-examples + --snaps-from-examples \ + --source-catalog examples/install-sources.yaml validate grep -q 'finish: subiquity/Install/install/postinstall/run_unattended_upgrades: SUCCESS: downloading and installing security updates' $tmpdir/subiquity-server-debug.log else diff --git a/subiquity/server/controllers/source.py b/subiquity/server/controllers/source.py index af3681d3..6f413da9 100644 --- a/subiquity/server/controllers/source.py +++ b/subiquity/server/controllers/source.py @@ -72,11 +72,6 @@ class SourceController(SubiquityController): current = self.app.base_model.locale.selected_language self.model.lang = current.split('_')[0] - def interactive(self): - if len(self.model.sources) <= 1: - return False - return super().interactive() - async def GET(self) -> SourceSelectionAndSetting: cur_lang = self.app.base_model.locale.selected_language cur_lang = cur_lang.rsplit('.', 1)[0] diff --git a/subiquity/tests/api/test_api.py b/subiquity/tests/api/test_api.py index 94d66cbe..74f99806 100755 --- a/subiquity/tests/api/test_api.py +++ b/subiquity/tests/api/test_api.py @@ -213,7 +213,8 @@ class TestFlow(TestAPI): 'toggle': None } await inst.post('/keyboard', keyboard) - await inst.post('/source', source_id='ubuntu-server') + await inst.post('/source', + source_id='ubuntu-server', search_drivers=True) await inst.post('/network') await inst.post('/proxy', '') await inst.post('/mirror', 'http://us.archive.ubuntu.com/ubuntu') From 8929197010dc4df4dc1563ba0c8e5c50613b3183 Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Tue, 26 Apr 2022 10:55:19 +0200 Subject: [PATCH 09/10] drivers: include source.search_drivers in GET /drivers The search_drivers attribute is set at the source model level, not at the the drivers model level. Having said that, by adding its value in the response to GET /drivers, we can avoid doing multiple HTTP calls in the make_ui function. Signed-off-by: Olivier Gayot --- subiquity/client/controllers/drivers.py | 6 ++---- subiquity/common/types.py | 2 ++ subiquity/server/controllers/drivers.py | 5 ++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/subiquity/client/controllers/drivers.py b/subiquity/client/controllers/drivers.py index 82442338..e5efe49a 100644 --- a/subiquity/client/controllers/drivers.py +++ b/subiquity/client/controllers/drivers.py @@ -31,13 +31,11 @@ class DriversController(SubiquityTuiController): endpoint_name = 'drivers' async def make_ui(self) -> DriversView: - source_endpoint = self.app.client.source - source_response = await source_endpoint.GET() + response: DriversResponse = await self.endpoint.GET() - if not source_response.search_drivers: + if not response.search_drivers: raise Skip - response: DriversResponse = await self.endpoint.GET() return DriversView(self, response.drivers, response.install, response.local_only) diff --git a/subiquity/common/types.py b/subiquity/common/types.py index cf39d081..1dfa5c4a 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -399,10 +399,12 @@ class DriversResponse: to do it. It will bet set to None until we figure out what drivers are available. :local_only: tells if we are looking for drivers only from the ISO. + :search_drivers: enables or disables drivers listing. """ install: bool drivers: Optional[List[str]] local_only: bool + search_drivers: bool @attr.s(auto_attribs=True) diff --git a/subiquity/server/controllers/drivers.py b/subiquity/server/controllers/drivers.py index 325b4f26..ade96456 100644 --- a/subiquity/server/controllers/drivers.py +++ b/subiquity/server/controllers/drivers.py @@ -102,9 +102,12 @@ class DriversController(SubiquityController): if wait: await asyncio.shield(self._drivers_task) + search_drivers = self.app.controllers.Source.model.search_drivers + return DriversResponse(install=self.model.do_install, drivers=self.drivers, - local_only=local_only) + local_only=local_only, + search_drivers=search_drivers) async def POST(self, data: DriversPayload) -> None: self.model.do_install = data.install From caff5f7dd760a0cf0f1647d26024251a94a43a4c Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Tue, 26 Apr 2022 11:23:53 +0200 Subject: [PATCH 10/10] source: add support for autoinstall data Since we added a search_drivers checkbox that is uncheckd by default, there is no longer a way for users to install third-party drivers in an autoinstall context. We now implement the autoinstall support for source so that users can specify what value they want for search_drivers. Futhermore, to be backward compatible with existing autoinstall configurations, we now make search_drivers default to true in autoinstall contexts. Signed-off-by: Olivier Gayot --- autoinstall-schema.json | 11 +++++++++ subiquity/server/controllers/source.py | 32 +++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/autoinstall-schema.json b/autoinstall-schema.json index 98db501f..a6bf8e63 100644 --- a/autoinstall-schema.json +++ b/autoinstall-schema.json @@ -118,6 +118,17 @@ ], "additionalProperties": false }, + "source": { + "type": "object", + "properties": { + "search_drivers": { + "type": "boolean" + } + }, + "required": [ + "search_drivers" + ] + }, "network": { "oneOf": [ { diff --git a/subiquity/server/controllers/source.py b/subiquity/server/controllers/source.py index 6f413da9..703cc4a8 100644 --- a/subiquity/server/controllers/source.py +++ b/subiquity/server/controllers/source.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Optional +from typing import Any, Optional import os from curtin.commands.extract import get_handler_for_source @@ -52,11 +52,41 @@ class SourceController(SubiquityController): endpoint = API.source + autoinstall_key = "source" + autoinstall_schema = { + "type": "object", + "properties": { + "search_drivers": { + "type": "boolean", + }, + }, + "required": ["search_drivers"], + } + # Defaults to true for backward compatibility with existing autoinstall + # configurations. Back then, then users were able to install third-party + # drivers without this field. + autoinstall_default = {"search_drivers": True} + def __init__(self, app): super().__init__(app) self._handler = None self.source_path: Optional[str] = None + def make_autoinstall(self): + return {"search_drivers": self.model.search_drivers} + + def load_autoinstall_data(self, data: Any) -> None: + if data is None: + # For some reason, the schema validator does not reject + # "source: null" despite "type" being "object" + data = self.autoinstall_default + + # search_drivers is marked required so the schema validator should + # reject any missing data. + assert "search_drivers" in data + + self.model.search_drivers = data["search_drivers"] + def start(self): path = '/cdrom/casper/install-sources.yaml' if self.app.opts.source_catalog is not None: