Merge pull request #1030 from mwhudson/source-selection-3
install source selection
This commit is contained in:
commit
1519c49d8e
|
@ -118,11 +118,13 @@ subiquity/models/mirror.py
|
||||||
subiquity/models/network.py
|
subiquity/models/network.py
|
||||||
subiquity/models/proxy.py
|
subiquity/models/proxy.py
|
||||||
subiquity/models/snaplist.py
|
subiquity/models/snaplist.py
|
||||||
|
subiquity/models/source.py
|
||||||
subiquity/models/ssh.py
|
subiquity/models/ssh.py
|
||||||
subiquity/models/subiquity.py
|
subiquity/models/subiquity.py
|
||||||
subiquity/models/tests/__init__.py
|
subiquity/models/tests/__init__.py
|
||||||
subiquity/models/tests/test_filesystem.py
|
subiquity/models/tests/test_filesystem.py
|
||||||
subiquity/models/tests/test_mirror.py
|
subiquity/models/tests/test_mirror.py
|
||||||
|
subiquity/models/tests/test_source.py
|
||||||
subiquity/models/tests/test_subiquity.py
|
subiquity/models/tests/test_subiquity.py
|
||||||
subiquity/models/timezone.py
|
subiquity/models/timezone.py
|
||||||
subiquity/models/updates.py
|
subiquity/models/updates.py
|
||||||
|
@ -144,8 +146,11 @@ subiquity/server/controllers/refresh.py
|
||||||
subiquity/server/controllers/reporting.py
|
subiquity/server/controllers/reporting.py
|
||||||
subiquity/server/controllers/shutdown.py
|
subiquity/server/controllers/shutdown.py
|
||||||
subiquity/server/controllers/snaplist.py
|
subiquity/server/controllers/snaplist.py
|
||||||
|
subiquity/server/controllers/source.py
|
||||||
subiquity/server/controllers/ssh.py
|
subiquity/server/controllers/ssh.py
|
||||||
|
subiquity/server/controllers/tests/__init__.py
|
||||||
subiquity/server/controllers/tests/test_keyboard.py
|
subiquity/server/controllers/tests/test_keyboard.py
|
||||||
|
subiquity/server/controllers/tests/test_source.py
|
||||||
subiquity/server/controllers/timezone.py
|
subiquity/server/controllers/timezone.py
|
||||||
subiquity/server/controllers/updates.py
|
subiquity/server/controllers/updates.py
|
||||||
subiquity/server/controllers/userdata.py
|
subiquity/server/controllers/userdata.py
|
||||||
|
|
|
@ -83,7 +83,7 @@ class SubiquityClient(TuiApplication):
|
||||||
|
|
||||||
snapd_socket_path = '/run/snapd.socket'
|
snapd_socket_path = '/run/snapd.socket'
|
||||||
|
|
||||||
variant = "server"
|
variant = None
|
||||||
cmdline = ['snap', 'run', 'subiquity']
|
cmdline = ['snap', 'run', 'subiquity']
|
||||||
dryrun_cmdline_module = 'subiquity.cmd.tui'
|
dryrun_cmdline_module = 'subiquity.cmd.tui'
|
||||||
|
|
||||||
|
@ -101,6 +101,7 @@ class SubiquityClient(TuiApplication):
|
||||||
"Welcome",
|
"Welcome",
|
||||||
"Refresh",
|
"Refresh",
|
||||||
"Keyboard",
|
"Keyboard",
|
||||||
|
"Source",
|
||||||
"Zdev",
|
"Zdev",
|
||||||
"Network",
|
"Network",
|
||||||
"Proxy",
|
"Proxy",
|
||||||
|
@ -443,6 +444,7 @@ class SubiquityClient(TuiApplication):
|
||||||
endpoint_names.append(c.endpoint_name)
|
endpoint_names.append(c.endpoint_name)
|
||||||
if endpoint_names:
|
if endpoint_names:
|
||||||
await self.client.meta.mark_configured.POST(endpoint_names)
|
await self.client.meta.mark_configured.POST(endpoint_names)
|
||||||
|
if self.variant:
|
||||||
await self.client.meta.client_variant.POST(self.variant)
|
await self.client.meta.client_variant.POST(self.variant)
|
||||||
self.controllers.index = index - 1
|
self.controllers.index = index - 1
|
||||||
self.next_screen()
|
self.next_screen()
|
||||||
|
|
|
@ -24,6 +24,7 @@ from .proxy import ProxyController
|
||||||
from .refresh import RefreshController
|
from .refresh import RefreshController
|
||||||
from .serial import SerialController
|
from .serial import SerialController
|
||||||
from .snaplist import SnapListController
|
from .snaplist import SnapListController
|
||||||
|
from .source import SourceController
|
||||||
from .ssh import SSHController
|
from .ssh import SSHController
|
||||||
from .welcome import WelcomeController
|
from .welcome import WelcomeController
|
||||||
from .zdev import ZdevController
|
from .zdev import ZdevController
|
||||||
|
@ -41,6 +42,7 @@ __all__ = [
|
||||||
'RepeatedController',
|
'RepeatedController',
|
||||||
'SerialController',
|
'SerialController',
|
||||||
'SnapListController',
|
'SnapListController',
|
||||||
|
'SourceController',
|
||||||
'SSHController',
|
'SSHController',
|
||||||
'WelcomeController',
|
'WelcomeController',
|
||||||
'ZdevController',
|
'ZdevController',
|
||||||
|
|
|
@ -42,10 +42,6 @@ BIOS_GRUB_SIZE_BYTES = 1 * 1024 * 1024 # 1MiB
|
||||||
PREP_GRUB_SIZE_BYTES = 8 * 1024 * 1024 # 8MiB
|
PREP_GRUB_SIZE_BYTES = 8 * 1024 * 1024 # 8MiB
|
||||||
UEFI_GRUB_SIZE_BYTES = 512 * 1024 * 1024 # 512MiB EFI partition
|
UEFI_GRUB_SIZE_BYTES = 512 * 1024 * 1024 # 512MiB EFI partition
|
||||||
|
|
||||||
# Disks larger than this are considered sensible targets for guided
|
|
||||||
# installation.
|
|
||||||
MIN_SIZE_GUIDED = 6 * (1 << 30)
|
|
||||||
|
|
||||||
|
|
||||||
class FilesystemController(SubiquityTuiController, FilesystemManipulator):
|
class FilesystemController(SubiquityTuiController, FilesystemManipulator):
|
||||||
|
|
||||||
|
@ -59,7 +55,7 @@ class FilesystemController(SubiquityTuiController, FilesystemManipulator):
|
||||||
self.answers.setdefault('manual', [])
|
self.answers.setdefault('manual', [])
|
||||||
|
|
||||||
async def make_ui(self):
|
async def make_ui(self):
|
||||||
status = await self.endpoint.guided.GET(MIN_SIZE_GUIDED)
|
status = await self.endpoint.guided.GET()
|
||||||
if status.status == ProbeStatus.PROBING:
|
if status.status == ProbeStatus.PROBING:
|
||||||
self.app.aio_loop.create_task(self._wait_for_probing())
|
self.app.aio_loop.create_task(self._wait_for_probing())
|
||||||
return SlowProbing(self)
|
return SlowProbing(self)
|
||||||
|
@ -67,7 +63,7 @@ class FilesystemController(SubiquityTuiController, FilesystemManipulator):
|
||||||
return self.make_guided_ui(status)
|
return self.make_guided_ui(status)
|
||||||
|
|
||||||
async def _wait_for_probing(self):
|
async def _wait_for_probing(self):
|
||||||
status = await self.endpoint.guided.GET(MIN_SIZE_GUIDED, wait=True)
|
status = await self.endpoint.guided.GET(wait=True)
|
||||||
if isinstance(self.ui.body, SlowProbing):
|
if isinstance(self.ui.body, SlowProbing):
|
||||||
self.ui.set_body(self.make_guided_ui(status))
|
self.ui.set_body(self.make_guided_ui(status))
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from subiquity.client.controller import SubiquityTuiController
|
||||||
|
from subiquity.ui.views.source import SourceView
|
||||||
|
|
||||||
|
log = logging.getLogger('subiquity.client.controllers.source')
|
||||||
|
|
||||||
|
|
||||||
|
class SourceController(SubiquityTuiController):
|
||||||
|
|
||||||
|
endpoint_name = 'source'
|
||||||
|
|
||||||
|
async def make_ui(self):
|
||||||
|
sources = await self.endpoint.GET()
|
||||||
|
return SourceView(self, sources.sources, sources.current_id)
|
||||||
|
|
||||||
|
def run_answers(self):
|
||||||
|
if 'source' in self.answers:
|
||||||
|
self.app.ui.body.form.source.value = self.answers['source']
|
||||||
|
self.app.ui.body.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))
|
|
@ -65,6 +65,8 @@ def make_server_args_parser():
|
||||||
'--snap-section', action='store', default='server',
|
'--snap-section', action='store', default='server',
|
||||||
help=("Show snaps from this section of the store in the snap "
|
help=("Show snaps from this section of the store in the snap "
|
||||||
"list screen."))
|
"list screen."))
|
||||||
|
parser.add_argument(
|
||||||
|
'--source-catalog', dest='source_catalog', action='store')
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ from subiquity.common.types import (
|
||||||
SnapInfo,
|
SnapInfo,
|
||||||
SnapListResponse,
|
SnapListResponse,
|
||||||
SnapSelection,
|
SnapSelection,
|
||||||
|
SourceSelectionAndSetting,
|
||||||
SSHData,
|
SSHData,
|
||||||
LiveSessionSSHInfo,
|
LiveSessionSSHInfo,
|
||||||
StorageResponse,
|
StorageResponse,
|
||||||
|
@ -123,6 +124,10 @@ class API:
|
||||||
class steps:
|
class steps:
|
||||||
def GET(index: Optional[str]) -> AnyStep: ...
|
def GET(index: Optional[str]) -> AnyStep: ...
|
||||||
|
|
||||||
|
class source:
|
||||||
|
def GET() -> SourceSelectionAndSetting: ...
|
||||||
|
def POST(source_id: str) -> None: ...
|
||||||
|
|
||||||
class zdev:
|
class zdev:
|
||||||
def GET() -> List[ZdevInfo]: ...
|
def GET() -> List[ZdevInfo]: ...
|
||||||
|
|
||||||
|
@ -208,8 +213,7 @@ class API:
|
||||||
|
|
||||||
class storage:
|
class storage:
|
||||||
class guided:
|
class guided:
|
||||||
def GET(min_size: int = None, wait: bool = False) \
|
def GET(wait: bool = False) -> GuidedStorageResponse:
|
||||||
-> GuidedStorageResponse:
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def POST(choice: Optional[GuidedChoice]) \
|
def POST(choice: Optional[GuidedChoice]) \
|
||||||
|
|
|
@ -168,6 +168,22 @@ class KeyboardSetup:
|
||||||
layouts: List[KeyboardLayout]
|
layouts: List[KeyboardLayout]
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(auto_attribs=True)
|
||||||
|
class SourceSelection:
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
id: str
|
||||||
|
size: int
|
||||||
|
variant: str
|
||||||
|
default: bool
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(auto_attribs=True)
|
||||||
|
class SourceSelectionAndSetting:
|
||||||
|
sources: List[SourceSelection]
|
||||||
|
current_id: str
|
||||||
|
|
||||||
|
|
||||||
@attr.s(auto_attribs=True)
|
@attr.s(auto_attribs=True)
|
||||||
class ZdevInfo:
|
class ZdevInfo:
|
||||||
id: str
|
id: str
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import typing
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
import attr
|
||||||
|
|
||||||
|
log = logging.getLogger('subiquity.models.source')
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(auto_attribs=True)
|
||||||
|
class CatalogEntry:
|
||||||
|
variant: str
|
||||||
|
id: str
|
||||||
|
name: typing.Dict[str, str]
|
||||||
|
description: typing.Dict[str, str]
|
||||||
|
path: str
|
||||||
|
size: int
|
||||||
|
type: str
|
||||||
|
default: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
fake_entries = {
|
||||||
|
'server': CatalogEntry(
|
||||||
|
variant='server',
|
||||||
|
id='synthesized',
|
||||||
|
name={'en': 'Ubuntu Server'},
|
||||||
|
description={'en': 'the default'},
|
||||||
|
path='/media/filesystem',
|
||||||
|
type='cp',
|
||||||
|
default=True,
|
||||||
|
size=2 << 30),
|
||||||
|
'desktop': CatalogEntry(
|
||||||
|
variant='desktop',
|
||||||
|
id='synthesized',
|
||||||
|
name={'en': 'Ubuntu Desktop'},
|
||||||
|
description={'en': 'the default'},
|
||||||
|
path='/media/filesystem',
|
||||||
|
type='cp',
|
||||||
|
default=True,
|
||||||
|
size=5 << 30),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SourceModel:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._dir = '/cdrom/casper'
|
||||||
|
self.current = fake_entries['server']
|
||||||
|
self.sources = [self.current]
|
||||||
|
|
||||||
|
def load_from_file(self, fp):
|
||||||
|
self._dir = os.path.dirname(fp.name)
|
||||||
|
self.sources = []
|
||||||
|
self.current = None
|
||||||
|
entries = yaml.safe_load(fp)
|
||||||
|
for entry in entries:
|
||||||
|
kw = {}
|
||||||
|
for field in attr.fields(CatalogEntry):
|
||||||
|
if field.name in entry:
|
||||||
|
kw[field.name] = entry[field.name]
|
||||||
|
c = CatalogEntry(**kw)
|
||||||
|
self.sources.append(c)
|
||||||
|
if c.default:
|
||||||
|
self.current = c
|
||||||
|
log.debug("loaded %d sources from %r", len(self.sources), fp.name)
|
||||||
|
if self.current is None:
|
||||||
|
self.current = self.sources[0]
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
path = os.path.join(self._dir, self.current.path)
|
||||||
|
scheme = self.current.type
|
||||||
|
return {
|
||||||
|
'sources': {
|
||||||
|
'ubuntu00': f'{scheme}://{path}'
|
||||||
|
},
|
||||||
|
}
|
|
@ -39,6 +39,7 @@ from .mirror import MirrorModel
|
||||||
from .network import NetworkModel
|
from .network import NetworkModel
|
||||||
from .proxy import ProxyModel
|
from .proxy import ProxyModel
|
||||||
from .snaplist import SnapListModel
|
from .snaplist import SnapListModel
|
||||||
|
from .source import SourceModel
|
||||||
from .ssh import SSHModel
|
from .ssh import SSHModel
|
||||||
from .timezone import TimeZoneModel
|
from .timezone import TimeZoneModel
|
||||||
from .updates import UpdatesModel
|
from .updates import UpdatesModel
|
||||||
|
@ -116,6 +117,7 @@ class SubiquityModel:
|
||||||
self.proxy = ProxyModel()
|
self.proxy = ProxyModel()
|
||||||
self.snaplist = SnapListModel()
|
self.snaplist = SnapListModel()
|
||||||
self.ssh = SSHModel()
|
self.ssh = SSHModel()
|
||||||
|
self.source = SourceModel()
|
||||||
self.timezone = TimeZoneModel()
|
self.timezone = TimeZoneModel()
|
||||||
self.updates = UpdatesModel()
|
self.updates = UpdatesModel()
|
||||||
self.userdata = {}
|
self.userdata = {}
|
||||||
|
@ -307,16 +309,13 @@ class SubiquityModel:
|
||||||
config = {
|
config = {
|
||||||
'stages': stages,
|
'stages': stages,
|
||||||
|
|
||||||
'sources': {
|
|
||||||
'ubuntu00': 'cp:///media/filesystem'
|
|
||||||
},
|
|
||||||
|
|
||||||
'curthooks_commands': {
|
'curthooks_commands': {
|
||||||
'001-configure-apt': [
|
'001-configure-apt': [
|
||||||
resource_path('bin/subiquity-configure-apt'),
|
resource_path('bin/subiquity-configure-apt'),
|
||||||
sys.executable, str(self.network.has_network).lower(),
|
sys.executable, str(self.network.has_network).lower(),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
'grub': {
|
'grub': {
|
||||||
'terminal': 'unmodified',
|
'terminal': 'unmodified',
|
||||||
'probe_additional_os': True
|
'probe_additional_os': True
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from subiquitycore.tests.util import random_string
|
||||||
|
|
||||||
|
from subiquity.models.source import (
|
||||||
|
SourceModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_entry(**fields):
|
||||||
|
raw = {
|
||||||
|
'name': {
|
||||||
|
'en': random_string(),
|
||||||
|
},
|
||||||
|
'description': {
|
||||||
|
'en': random_string(),
|
||||||
|
},
|
||||||
|
'id': random_string(),
|
||||||
|
'type': random_string(),
|
||||||
|
'variant': random_string(),
|
||||||
|
'path': random_string(),
|
||||||
|
'size': random.randint(1000000, 2000000),
|
||||||
|
}
|
||||||
|
for k, v in fields.items():
|
||||||
|
raw[k] = v
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
class TestMirrorModel(unittest.TestCase):
|
||||||
|
|
||||||
|
def tdir(self):
|
||||||
|
tdir = tempfile.mkdtemp()
|
||||||
|
self.addCleanup(shutil.rmtree, tdir)
|
||||||
|
return tdir
|
||||||
|
|
||||||
|
def write_and_load_entries(self, model, entries, dir=None):
|
||||||
|
if dir is None:
|
||||||
|
dir = self.tdir()
|
||||||
|
cat_path = os.path.join(dir, 'catalog.yaml')
|
||||||
|
with open(cat_path, 'w') as fp:
|
||||||
|
yaml.dump(entries, fp)
|
||||||
|
with open(cat_path) as fp:
|
||||||
|
model.load_from_file(fp)
|
||||||
|
|
||||||
|
def test_initially_server(self):
|
||||||
|
model = SourceModel()
|
||||||
|
self.assertEqual(model.current.variant, 'server')
|
||||||
|
|
||||||
|
def test_load_from_file_simple(self):
|
||||||
|
entry = make_entry(id='id1')
|
||||||
|
model = SourceModel()
|
||||||
|
self.write_and_load_entries(model, [entry])
|
||||||
|
self.assertEqual(model.current.id, 'id1')
|
||||||
|
|
||||||
|
def test_load_from_file_ignores_extra_keys(self):
|
||||||
|
entry = make_entry(id='id1', foobarbaz=random_string())
|
||||||
|
model = SourceModel()
|
||||||
|
self.write_and_load_entries(model, [entry])
|
||||||
|
self.assertEqual(model.current.id, 'id1')
|
||||||
|
|
||||||
|
def test_default(self):
|
||||||
|
entries = [
|
||||||
|
make_entry(id='id1'),
|
||||||
|
make_entry(id='id2', default=True),
|
||||||
|
make_entry(id='id3'),
|
||||||
|
]
|
||||||
|
model = SourceModel()
|
||||||
|
self.write_and_load_entries(model, entries)
|
||||||
|
self.assertEqual(model.current.id, 'id2')
|
||||||
|
|
||||||
|
def test_render_absolute(self):
|
||||||
|
entry = make_entry(
|
||||||
|
type='scheme',
|
||||||
|
path='/foo/bar/baz',
|
||||||
|
)
|
||||||
|
model = SourceModel()
|
||||||
|
self.write_and_load_entries(model, [entry])
|
||||||
|
self.assertEqual(
|
||||||
|
model.render(), {'sources': {'ubuntu00': 'scheme:///foo/bar/baz'}})
|
||||||
|
|
||||||
|
def test_render_relative(self):
|
||||||
|
dir = self.tdir()
|
||||||
|
entry = make_entry(
|
||||||
|
type='scheme',
|
||||||
|
path='foo/bar/baz',
|
||||||
|
)
|
||||||
|
model = SourceModel()
|
||||||
|
self.write_and_load_entries(model, [entry], dir)
|
||||||
|
self.assertEqual(
|
||||||
|
model.render(),
|
||||||
|
{'sources': {'ubuntu00': f'scheme://{dir}/foo/bar/baz'}})
|
||||||
|
|
||||||
|
def test_render_initial(self):
|
||||||
|
model = SourceModel()
|
||||||
|
self.assertEqual(
|
||||||
|
model.render(),
|
||||||
|
{'sources': {'ubuntu00': 'cp:///media/filesystem'}})
|
|
@ -29,6 +29,7 @@ from .refresh import RefreshController
|
||||||
from .reporting import ReportingController
|
from .reporting import ReportingController
|
||||||
from .shutdown import ShutdownController
|
from .shutdown import ShutdownController
|
||||||
from .snaplist import SnapListController
|
from .snaplist import SnapListController
|
||||||
|
from .source import SourceController
|
||||||
from .ssh import SSHController
|
from .ssh import SSHController
|
||||||
from .timezone import TimeZoneController
|
from .timezone import TimeZoneController
|
||||||
from .updates import UpdatesController
|
from .updates import UpdatesController
|
||||||
|
@ -54,6 +55,7 @@ __all__ = [
|
||||||
'ReportingController',
|
'ReportingController',
|
||||||
'ShutdownController',
|
'ShutdownController',
|
||||||
'SnapListController',
|
'SnapListController',
|
||||||
|
'SourceController',
|
||||||
'SSHController',
|
'SSHController',
|
||||||
'TimeZoneController',
|
'TimeZoneController',
|
||||||
'UpdatesController',
|
'UpdatesController',
|
||||||
|
|
|
@ -62,10 +62,6 @@ from subiquity.server.controller import (
|
||||||
log = logging.getLogger("subiquity.server.controller.filesystem")
|
log = logging.getLogger("subiquity.server.controller.filesystem")
|
||||||
block_discover_log = logging.getLogger('block-discover')
|
block_discover_log = logging.getLogger('block-discover')
|
||||||
|
|
||||||
# Disks larger than this are considered sensible targets for guided
|
|
||||||
# installation.
|
|
||||||
DEFAULT_MIN_SIZE_GUIDED = 6 * (1 << 30)
|
|
||||||
|
|
||||||
|
|
||||||
class FilesystemController(SubiquityController, FilesystemManipulator):
|
class FilesystemController(SubiquityController, FilesystemManipulator):
|
||||||
|
|
||||||
|
@ -217,13 +213,15 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
|
||||||
config, self.model._probe_data['blockdev'], is_probe_data=False)
|
config, self.model._probe_data['blockdev'], is_probe_data=False)
|
||||||
self.configured()
|
self.configured()
|
||||||
|
|
||||||
async def guided_GET(self, min_size: int = None, wait: bool = False) \
|
async def guided_GET(self, wait: bool = False) -> GuidedStorageResponse:
|
||||||
-> GuidedStorageResponse:
|
|
||||||
probe_resp = await self._probe_response(wait, GuidedStorageResponse)
|
probe_resp = await self._probe_response(wait, GuidedStorageResponse)
|
||||||
if probe_resp is not None:
|
if probe_resp is not None:
|
||||||
return probe_resp
|
return probe_resp
|
||||||
if not min_size:
|
# This calculation is pretty much a hack and we should
|
||||||
min_size = DEFAULT_MIN_SIZE_GUIDED
|
# actually think about it at some point (like: maybe the
|
||||||
|
# source catalog should directly specify the minimum suitable
|
||||||
|
# size?)
|
||||||
|
min_size = 2*self.app.base_model.source.current.size + (1 << 30)
|
||||||
disks = []
|
disks = []
|
||||||
for raid in self.model._all(type='raid'):
|
for raid in self.model._all(type='raid'):
|
||||||
if not boot.can_be_boot_device(raid, with_reformatting=True):
|
if not boot.can_be_boot_device(raid, with_reformatting=True):
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from subiquity.common.apidef import API
|
||||||
|
from subiquity.common.types import (
|
||||||
|
SourceSelection,
|
||||||
|
SourceSelectionAndSetting,
|
||||||
|
)
|
||||||
|
from subiquity.server.controller import SubiquityController
|
||||||
|
|
||||||
|
|
||||||
|
def _translate(d, lang):
|
||||||
|
if lang:
|
||||||
|
for lang in lang, lang.split('_', 1)[0]:
|
||||||
|
if lang in d:
|
||||||
|
return d[lang]
|
||||||
|
return _(d['en'])
|
||||||
|
|
||||||
|
|
||||||
|
def convert_source(source, lang):
|
||||||
|
return SourceSelection(
|
||||||
|
name=_translate(source.name, lang),
|
||||||
|
description=_translate(source.description, lang),
|
||||||
|
id=source.id,
|
||||||
|
size=source.size,
|
||||||
|
variant=source.variant,
|
||||||
|
default=source.default)
|
||||||
|
|
||||||
|
|
||||||
|
class SourceController(SubiquityController):
|
||||||
|
|
||||||
|
model_name = "source"
|
||||||
|
|
||||||
|
endpoint = API.source
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
path = '/cdrom/casper/install-sources.yaml'
|
||||||
|
if self.app.opts.source_catalog is not None:
|
||||||
|
path = self.app.opts.source_catalog
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return
|
||||||
|
with open(path) as fp:
|
||||||
|
self.model.load_from_file(fp)
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
return SourceSelectionAndSetting(
|
||||||
|
[
|
||||||
|
convert_source(source, cur_lang)
|
||||||
|
for source in self.model.sources
|
||||||
|
],
|
||||||
|
self.model.current.id)
|
||||||
|
|
||||||
|
def configured(self):
|
||||||
|
super().configured()
|
||||||
|
self.app.base_model.set_source_variant(self.model.current.variant)
|
||||||
|
|
||||||
|
async def POST(self, source_id: str) -> None:
|
||||||
|
for source in self.model.sources:
|
||||||
|
if source.id == source_id:
|
||||||
|
self.model.current = source
|
||||||
|
self.configured()
|
|
@ -0,0 +1 @@
|
||||||
|
#
|
|
@ -0,0 +1,65 @@
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import random
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import attr
|
||||||
|
|
||||||
|
from subiquitycore.tests.util import random_string
|
||||||
|
|
||||||
|
from subiquity.models.source import CatalogEntry
|
||||||
|
from subiquity.server.controllers.source import convert_source
|
||||||
|
|
||||||
|
|
||||||
|
def make_entry(**kw):
|
||||||
|
fields = {
|
||||||
|
'default': False,
|
||||||
|
'name': {'en': random_string()},
|
||||||
|
'description': {'en': random_string()},
|
||||||
|
'size': random.randint(1000000, 2000000),
|
||||||
|
}
|
||||||
|
for field in attr.fields(CatalogEntry):
|
||||||
|
if field.name not in fields:
|
||||||
|
fields[field.name] = random_string()
|
||||||
|
for k, v in kw.items():
|
||||||
|
fields[k] = v
|
||||||
|
return CatalogEntry(**fields)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSubiquityModel(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_convert_source(self):
|
||||||
|
entry = make_entry()
|
||||||
|
source = convert_source(entry, 'C')
|
||||||
|
self.assertEqual(source.id, entry.id)
|
||||||
|
|
||||||
|
def test_convert_translations(self):
|
||||||
|
entry = make_entry(
|
||||||
|
name={
|
||||||
|
'en': 'English',
|
||||||
|
'fr': 'French',
|
||||||
|
'fr_CA': 'French Canadian',
|
||||||
|
})
|
||||||
|
self.assertEqual(
|
||||||
|
convert_source(entry, 'C').name, "English")
|
||||||
|
self.assertEqual(
|
||||||
|
convert_source(entry, 'en').name, "English")
|
||||||
|
self.assertEqual(
|
||||||
|
convert_source(entry, 'fr').name, "French")
|
||||||
|
self.assertEqual(
|
||||||
|
convert_source(entry, 'fr_CA').name, "French Canadian")
|
||||||
|
self.assertEqual(
|
||||||
|
convert_source(entry, 'fr_BE').name, "French")
|
|
@ -116,6 +116,12 @@ class MetaController:
|
||||||
async def client_variant_POST(self, variant: str) -> None:
|
async def client_variant_POST(self, variant: str) -> None:
|
||||||
if variant not in self.app.supported_variants:
|
if variant not in self.app.supported_variants:
|
||||||
raise ValueError(f'unrecognized client variant {variant}')
|
raise ValueError(f'unrecognized client variant {variant}')
|
||||||
|
from subiquity.models.source import fake_entries
|
||||||
|
if variant in fake_entries:
|
||||||
|
if self.app.base_model.source.current.variant != variant:
|
||||||
|
self.app.base_model.source.current = fake_entries[variant]
|
||||||
|
self.app.controllers.Source.configured()
|
||||||
|
else:
|
||||||
self.app.base_model.set_source_variant(variant)
|
self.app.base_model.set_source_variant(variant)
|
||||||
|
|
||||||
async def ssh_info_GET(self) -> Optional[LiveSessionSSHInfo]:
|
async def ssh_info_GET(self) -> Optional[LiveSessionSSHInfo]:
|
||||||
|
@ -170,6 +176,7 @@ INSTALL_MODEL_NAMES = ModelNames({
|
||||||
"mirror",
|
"mirror",
|
||||||
"network",
|
"network",
|
||||||
"proxy",
|
"proxy",
|
||||||
|
"source",
|
||||||
})
|
})
|
||||||
|
|
||||||
POSTINSTALL_MODEL_NAMES = ModelNames({
|
POSTINSTALL_MODEL_NAMES = ModelNames({
|
||||||
|
@ -214,6 +221,7 @@ class SubiquityServer(Application):
|
||||||
"Kernel",
|
"Kernel",
|
||||||
"Keyboard",
|
"Keyboard",
|
||||||
"Zdev",
|
"Zdev",
|
||||||
|
"Source",
|
||||||
"Network",
|
"Network",
|
||||||
"Proxy",
|
"Proxy",
|
||||||
"Mirror",
|
"Mirror",
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from urwid import connect_signal
|
||||||
|
|
||||||
|
from subiquitycore.view import BaseView
|
||||||
|
from subiquitycore.ui.form import (
|
||||||
|
Form,
|
||||||
|
RadioButtonField,
|
||||||
|
)
|
||||||
|
|
||||||
|
log = logging.getLogger('subiquity.ui.views.source')
|
||||||
|
|
||||||
|
|
||||||
|
class SourceView(BaseView):
|
||||||
|
|
||||||
|
title = _("Choose type of install")
|
||||||
|
|
||||||
|
def __init__(self, controller, sources, current_id):
|
||||||
|
self.controller = controller
|
||||||
|
|
||||||
|
group = []
|
||||||
|
|
||||||
|
ns = {
|
||||||
|
'cancel_label': _("Back"),
|
||||||
|
}
|
||||||
|
initial = {}
|
||||||
|
|
||||||
|
for default in True, False:
|
||||||
|
for source in sorted(sources, key=lambda s: s.id):
|
||||||
|
if source.default != default:
|
||||||
|
continue
|
||||||
|
ns[source.id] = RadioButtonField(
|
||||||
|
group, source.name, '\n' + source.description)
|
||||||
|
initial[source.id] = source.id == current_id
|
||||||
|
|
||||||
|
SourceForm = type(Form)('SourceForm', (Form,), ns)
|
||||||
|
log.debug('%r %r', ns, current_id)
|
||||||
|
|
||||||
|
self.form = SourceForm(initial=initial)
|
||||||
|
|
||||||
|
connect_signal(self.form, 'submit', self.done)
|
||||||
|
connect_signal(self.form, 'cancel', self.cancel)
|
||||||
|
|
||||||
|
excerpt = _("Choose the base for the installation.")
|
||||||
|
|
||||||
|
super().__init__(self.form.as_screen(excerpt=excerpt))
|
||||||
|
|
||||||
|
def done(self, result):
|
||||||
|
log.debug("User input: {}".format(result.as_data()))
|
||||||
|
for k, v in result.as_data().items():
|
||||||
|
if v:
|
||||||
|
self.controller.done(k)
|
||||||
|
|
||||||
|
def cancel(self, result=None):
|
||||||
|
self.controller.cancel()
|
|
@ -14,7 +14,13 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
|
||||||
def run_coro(coro):
|
def run_coro(coro):
|
||||||
return asyncio.get_event_loop().run_until_complete(coro)
|
return asyncio.get_event_loop().run_until_complete(coro)
|
||||||
|
|
||||||
|
|
||||||
|
def random_string():
|
||||||
|
return ''.join(random.choice(string.ascii_letters) for _ in range(8))
|
||||||
|
|
Loading…
Reference in New Issue