diff --git a/examples/answers-bond.yaml b/examples/answers-bond.yaml index 1f318154..d9431b8a 100644 --- a/examples/answers-bond.yaml +++ b/examples/answers-bond.yaml @@ -56,4 +56,5 @@ SnapList: classic: false InstallProgress: reboot: yes - +Drivers: + install: no diff --git a/examples/answers-guided-lvm.yaml b/examples/answers-guided-lvm.yaml index 16621f3b..cfbff2bf 100644 --- a/examples/answers-guided-lvm.yaml +++ b/examples/answers-guided-lvm.yaml @@ -32,5 +32,5 @@ SnapList: classic: false InstallProgress: reboot: yes - - +Drivers: + install: no diff --git a/examples/answers-imsm.yaml b/examples/answers-imsm.yaml index b0fe5bb4..0d0c3aa9 100644 --- a/examples/answers-imsm.yaml +++ b/examples/answers-imsm.yaml @@ -35,3 +35,5 @@ SnapList: classic: false InstallProgress: reboot: yes +Drivers: + install: no diff --git a/examples/answers-lvm-dmcrypt.yaml b/examples/answers-lvm-dmcrypt.yaml index acd15a18..5b1137c8 100644 --- a/examples/answers-lvm-dmcrypt.yaml +++ b/examples/answers-lvm-dmcrypt.yaml @@ -79,3 +79,5 @@ SnapList: classic: false InstallProgress: reboot: yes +Drivers: + install: no diff --git a/examples/answers-lvm.yaml b/examples/answers-lvm.yaml index d4e07f64..858aec3c 100644 --- a/examples/answers-lvm.yaml +++ b/examples/answers-lvm.yaml @@ -66,3 +66,5 @@ SnapList: classic: false InstallProgress: reboot: yes +Drivers: + install: no diff --git a/examples/answers-preserve.yaml b/examples/answers-preserve.yaml index ca699e2d..48ea9a96 100644 --- a/examples/answers-preserve.yaml +++ b/examples/answers-preserve.yaml @@ -40,3 +40,5 @@ SnapList: classic: false InstallProgress: reboot: yes +Drivers: + install: no diff --git a/examples/answers-raid-lvm.yaml b/examples/answers-raid-lvm.yaml index 8fae0824..bb970879 100644 --- a/examples/answers-raid-lvm.yaml +++ b/examples/answers-raid-lvm.yaml @@ -107,3 +107,5 @@ SnapList: classic: false InstallProgress: reboot: yes +Drivers: + install: no diff --git a/examples/answers-raid.yaml b/examples/answers-raid.yaml index 012f85cd..f5ab1c2b 100644 --- a/examples/answers-raid.yaml +++ b/examples/answers-raid.yaml @@ -66,3 +66,5 @@ SnapList: classic: false InstallProgress: reboot: yes +Drivers: + install: no diff --git a/examples/answers-serial.yaml b/examples/answers-serial.yaml index 808dd707..a7257dcb 100644 --- a/examples/answers-serial.yaml +++ b/examples/answers-serial.yaml @@ -39,5 +39,5 @@ SnapList: classic: false InstallProgress: reboot: yes - - +Drivers: + install: no diff --git a/examples/answers-swap.yaml b/examples/answers-swap.yaml index 361a8d9c..6135c1eb 100644 --- a/examples/answers-swap.yaml +++ b/examples/answers-swap.yaml @@ -39,5 +39,5 @@ SnapList: classic: false InstallProgress: reboot: yes - - +Drivers: + install: no diff --git a/examples/answers.yaml b/examples/answers.yaml index d0b0d274..03ef4cc9 100644 --- a/examples/answers.yaml +++ b/examples/answers.yaml @@ -31,5 +31,5 @@ SnapList: classic: false InstallProgress: reboot: yes - - +Drivers: + install: yes diff --git a/subiquity/client/client.py b/subiquity/client/client.py index 2e2c6217..eb0f71ea 100644 --- a/subiquity/client/client.py +++ b/subiquity/client/client.py @@ -113,6 +113,7 @@ class SubiquityClient(TuiApplication): "Identity", "UbuntuPro", "SSH", + "Drivers", "SnapList", "Progress", ] diff --git a/subiquity/client/controllers/__init__.py b/subiquity/client/controllers/__init__.py index 3bf3f7e3..47abe8e6 100644 --- a/subiquity/client/controllers/__init__.py +++ b/subiquity/client/controllers/__init__.py @@ -14,6 +14,7 @@ # along with this program. If not, see . from subiquitycore.tuicontroller import RepeatedController +from .drivers import DriversController from .filesystem import FilesystemController from .identity import IdentityController from .keyboard import KeyboardController @@ -32,6 +33,7 @@ from .zdev import ZdevController # see SubiquityClient.controllers for another list __all__ = [ + 'DriversController', 'FilesystemController', 'IdentityController', 'KeyboardController', diff --git a/subiquity/client/controllers/drivers.py b/subiquity/client/controllers/drivers.py new file mode 100644 index 00000000..09d63d75 --- /dev/null +++ b/subiquity/client/controllers/drivers.py @@ -0,0 +1,69 @@ +# Copyright 2021 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 +from typing import List + +from subiquitycore.tuicontroller import Skip + +from subiquity.common.types import DriversPayload, DriversResponse +from subiquity.client.controller import SubiquityTuiController +from subiquity.ui.views.drivers import DriversView, DriversViewStatus + +log = logging.getLogger('subiquity.client.controllers.drivers') + + +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: + raise Skip + return DriversView(self, response.drivers, response.install) + + async def _wait_drivers(self) -> List[str]: + response: DriversResponse = await self.endpoint.GET(wait=True) + assert response.drivers is not None + return response.drivers + + async def run_answers(self): + if 'install' not in self.answers: + return + + from subiquitycore.testing.view_helpers import ( + click, + ) + + view = self.app.ui.body + while view.status == DriversViewStatus.WAITING: + await asyncio.sleep(0.2) + if view.status == DriversViewStatus.NO_DRIVERS: + click(view.cont_btn.base_widget) + return + + view.form.install.value = self.answers['install'] + + click(view.form.done_btn.base_widget) + + def cancel(self) -> None: + self.app.prev_screen() + + def done(self, install: bool) -> None: + log.debug("DriversController.done next_screen install=%s", install) + self.app.next_screen( + self.endpoint.POST(DriversPayload(install=install))) diff --git a/subiquity/server/controllers/drivers.py b/subiquity/server/controllers/drivers.py index 46a0a66a..ed6d9468 100644 --- a/subiquity/server/controllers/drivers.py +++ b/subiquity/server/controllers/drivers.py @@ -88,11 +88,6 @@ class DriversController(SubiquityController): log.debug("Available drivers to install: %s", self.drivers) if not self.drivers: await self.configured() - else: - # TODO Remove this once we have the GUI controller. - await self.POST(data=DriversPayload( - install=True, - )) async def GET(self, wait: bool = False) -> DriversResponse: if wait: diff --git a/subiquity/ui/views/drivers.py b/subiquity/ui/views/drivers.py new file mode 100644 index 00000000..fa5ec9fa --- /dev/null +++ b/subiquity/ui/views/drivers.py @@ -0,0 +1,138 @@ +# Copyright 2021 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 . + +""" Module defining the view for third-party drivers installation. + +""" +import asyncio +from enum import auto, Enum +import logging +from typing import List, Optional + +from urwid import ( + connect_signal, + Text, + ) + +from subiquitycore.ui.buttons import ok_btn +from subiquitycore.ui.form import ( + Form, + BooleanField, +) +from subiquitycore.ui.spinner import Spinner +from subiquitycore.ui.utils import screen +from subiquitycore.view import BaseView + + +log = logging.getLogger('subiquity.ui.views.drivers') + + +class DriversForm(Form): + """ Form that shows a checkbox to configure whether we want to install the + available drivers or not. """ + + cancel_label = _("Back") + + install = BooleanField(_("Install the drivers")) + + +class DriversViewStatus(Enum): + WAITING = auto() + NO_DRIVERS = auto() + MAIN = auto() + + +class DriversView(BaseView): + + title = _("Third-party drivers") + + form = None + + def __init__(self, controller, drivers: Optional[List[str]], + install: bool) -> None: + self.controller = controller + + if drivers is None: + self.make_waiting(install) + else: + self.make_main(install, drivers) + + def make_waiting(self, install: bool) -> None: + """ Change the view into a spinner and start waiting for drivers + asynchronously. """ + self.spinner = Spinner(self.controller.app.aio_loop, style='dots') + self.spinner.start() + rows = [ + Text(_("Looking for applicable third-party drivers...")), + Text(""), + self.spinner, + ] + self.cont_btn = ok_btn( + _("Continue"), + on_press=lambda sender: self.done(False)) + self._w = screen(rows, [self.cont_btn]) + asyncio.create_task(self._wait(install)) + self.status = DriversViewStatus.WAITING + + async def _wait(self, install: bool) -> None: + """ Wait until the "list" of drivers is available and change the view + accordingly. """ + drivers = await self.controller._wait_drivers() + self.spinner.stop() + if drivers: + self.make_main(install, drivers) + else: + self.make_no_drivers() + + def make_no_drivers(self) -> None: + """ 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."))] + self.cont_btn = ok_btn( + _("Continue"), + on_press=lambda sender: self.done(False)) + self._w = screen(rows, [self.cont_btn]) + self.status = DriversViewStatus.NO_DRIVERS + + def make_main(self, install: bool, drivers: List[str]) -> None: + """ Change the view to display the drivers form. """ + self.form = DriversForm(initial={'install': install}) + + excerpt = _( + "The following third-party drivers were found. " + "Do you want to install them?") + + def on_cancel(_: DriversForm) -> None: + self.cancel() + + connect_signal( + self.form, 'submit', + lambda result: self.done(result.install.value)) + connect_signal(self.form, 'cancel', on_cancel) + + rows = [Text(f"* {driver}") for driver in drivers] + rows.append(Text("")) + rows.extend(self.form.as_rows()) + + self._w = screen(rows, self.form.buttons, excerpt=excerpt) + self.status = DriversViewStatus.MAIN + + def done(self, install: bool) -> None: + log.debug("User input: %r", install) + self.controller.done(install) + + def cancel(self) -> None: + self.controller.cancel()