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/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/client/controllers/drivers.py b/subiquity/client/controllers/drivers.py
index 09d63d75..e5efe49a 100644
--- a/subiquity/client/controllers/drivers.py
+++ b/subiquity/client/controllers/drivers.py
@@ -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)
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 5e82a6cb..35852fb4 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 ad89f397..bafc7ed9 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)
@@ -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)
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/drivers.py b/subiquity/server/controllers/drivers.py
index 96e6285d..ade96456 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:
@@ -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
diff --git a/subiquity/server/controllers/source.py b/subiquity/server/controllers/source.py
index 385ed3cf..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:
@@ -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
diff --git a/subiquity/tests/api/test_api.py b/subiquity/tests/api/test_api.py
index 8151caa0..e5202343 100755
--- a/subiquity/tests/api/test_api.py
+++ b/subiquity/tests/api/test_api.py
@@ -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.
diff --git a/subiquity/ui/views/drivers.py b/subiquity/ui/views/drivers.py
index fa5ec9fa..a10b707e 100644
--- a/subiquity/ui/views/drivers.py
+++ b/subiquity/ui/views/drivers.py
@@ -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
diff --git a/subiquity/ui/views/source.py b/subiquity/ui/views/source.py
index 634c5d08..1991a336 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()))
+ 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()