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()