Merge pull request #1252 from ogayot/FR-2149
Revisit design for third-party drivers
This commit is contained in:
commit
9ad7ff2747
|
@ -118,6 +118,17 @@
|
|||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"source": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"search_drivers": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"search_drivers"
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"oneOf": [
|
||||
{
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
Source:
|
||||
source: ubuntu-server-minimal
|
||||
search_drivers: true
|
||||
Welcome:
|
||||
lang: en_US
|
||||
Refresh:
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
Source:
|
||||
source: ubuntu-server
|
||||
search_drivers: false
|
||||
Welcome:
|
||||
lang: en_US
|
||||
Refresh:
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
#machine-config: examples/imsm.json
|
||||
Source:
|
||||
source: ubuntu-server
|
||||
Welcome:
|
||||
lang: en_US
|
||||
Refresh:
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
Source:
|
||||
source: ubuntu-server-minimal
|
||||
Welcome:
|
||||
lang: en_US
|
||||
Refresh:
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
Source:
|
||||
source: ubuntu-server
|
||||
Welcome:
|
||||
lang: en_US
|
||||
Refresh:
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
#machine-config: examples/existing-partitions.json
|
||||
Source:
|
||||
source: ubuntu-server
|
||||
Welcome:
|
||||
lang: en_US
|
||||
Refresh:
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
Source:
|
||||
source: ubuntu-server
|
||||
Welcome:
|
||||
lang: en_US
|
||||
Refresh:
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
Source:
|
||||
source: ubuntu-server
|
||||
Welcome:
|
||||
lang: en_US
|
||||
Refresh:
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
#serial
|
||||
Source:
|
||||
source: ubuntu-server
|
||||
Serial:
|
||||
rich: false
|
||||
Welcome:
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
Source:
|
||||
source: ubuntu-server
|
||||
Welcome:
|
||||
lang: en_US
|
||||
Refresh:
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
Source:
|
||||
source: ubuntu-server
|
||||
Welcome:
|
||||
lang: en_US
|
||||
Refresh:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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]: ...
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue