Merge pull request #1252 from ogayot/FR-2149

Revisit design for third-party drivers
This commit is contained in:
Olivier Gayot 2022-04-27 19:40:20 +02:00 committed by GitHub
commit 9ad7ff2747
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 213 additions and 38 deletions

View File

@ -118,6 +118,17 @@
],
"additionalProperties": false
},
"source": {
"type": "object",
"properties": {
"search_drivers": {
"type": "boolean"
}
},
"required": [
"search_drivers"
]
},
"network": {
"oneOf": [
{

View File

@ -1,3 +1,6 @@
Source:
source: ubuntu-server-minimal
search_drivers: true
Welcome:
lang: en_US
Refresh:

View File

@ -1,3 +1,6 @@
Source:
source: ubuntu-server
search_drivers: false
Welcome:
lang: en_US
Refresh:

View File

@ -1,4 +1,6 @@
#machine-config: examples/imsm.json
Source:
source: ubuntu-server
Welcome:
lang: en_US
Refresh:

View File

@ -1,3 +1,5 @@
Source:
source: ubuntu-server-minimal
Welcome:
lang: en_US
Refresh:

View File

@ -1,3 +1,5 @@
Source:
source: ubuntu-server
Welcome:
lang: en_US
Refresh:

View File

@ -1,4 +1,6 @@
#machine-config: examples/existing-partitions.json
Source:
source: ubuntu-server
Welcome:
lang: en_US
Refresh:

View File

@ -1,3 +1,5 @@
Source:
source: ubuntu-server
Welcome:
lang: en_US
Refresh:

View File

@ -1,3 +1,5 @@
Source:
source: ubuntu-server
Welcome:
lang: en_US
Refresh:

View File

@ -1,4 +1,6 @@
#serial
Source:
source: ubuntu-server
Serial:
rich: false
Welcome:

View File

@ -1,3 +1,5 @@
Source:
source: ubuntu-server
Welcome:
lang: en_US
Refresh:

View File

@ -1,3 +1,5 @@
Source:
source: ubuntu-server
Welcome:
lang: en_US
Refresh:

View File

@ -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

View File

@ -32,9 +32,12 @@ class DriversController(SubiquityTuiController):
async def make_ui(self) -> DriversView:
response: DriversResponse = await self.endpoint.GET()
if not response.drivers and response.drivers is not None:
if not response.search_drivers:
raise Skip
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)

View File

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

View File

@ -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]: ...

View File

@ -183,6 +183,7 @@ class SourceSelection:
class SourceSelectionAndSetting:
sources: List[SourceSelection]
current_id: str
search_drivers: bool
@attr.s(auto_attribs=True)
@ -398,9 +399,13 @@ 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.
:search_drivers: enables or disables drivers listing.
"""
install: bool
drivers: Optional[List[str]]
local_only: bool
search_drivers: bool
@attr.s(auto_attribs=True)

View File

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

View File

@ -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:
@ -90,10 +98,16 @@ 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)
search_drivers = self.app.controllers.Source.model.search_drivers
return DriversResponse(install=self.model.do_install,
drivers=self.drivers)
drivers=self.drivers,
local_only=local_only,
search_drivers=search_drivers)
async def POST(self, data: DriversPayload) -> None:
self.model.do_install = data.install

View File

@ -13,7 +13,7 @@
# 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 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:
@ -72,11 +102,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]
@ -86,7 +111,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 +126,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

View File

@ -219,7 +219,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')
@ -1022,6 +1023,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.

View File

@ -26,10 +26,10 @@ 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,
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):
@ -61,11 +69,22 @@ 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: 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"),
]
if drivers is None:
self.make_waiting(install)
elif not drivers:
self.make_no_drivers()
else:
self.make_main(install, drivers)
@ -74,15 +93,24 @@ 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,
]
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
@ -100,16 +128,33 @@ 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."))]
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))
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:
""" 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. "
@ -127,6 +172,10 @@ class DriversView(BaseView):
rows.append(Text(""))
rows.extend(self.form.as_rows())
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

View File

@ -14,13 +14,21 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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()))
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":
continue
if v:
self.controller.done(k)
self.controller.done(k, search_drivers=search_drivers)
def cancel(self, result=None):
self.controller.cancel()