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/proxy.py
|
||||
subiquity/models/snaplist.py
|
||||
subiquity/models/source.py
|
||||
subiquity/models/ssh.py
|
||||
subiquity/models/subiquity.py
|
||||
subiquity/models/tests/__init__.py
|
||||
subiquity/models/tests/test_filesystem.py
|
||||
subiquity/models/tests/test_mirror.py
|
||||
subiquity/models/tests/test_source.py
|
||||
subiquity/models/tests/test_subiquity.py
|
||||
subiquity/models/timezone.py
|
||||
subiquity/models/updates.py
|
||||
|
@ -144,8 +146,11 @@ subiquity/server/controllers/refresh.py
|
|||
subiquity/server/controllers/reporting.py
|
||||
subiquity/server/controllers/shutdown.py
|
||||
subiquity/server/controllers/snaplist.py
|
||||
subiquity/server/controllers/source.py
|
||||
subiquity/server/controllers/ssh.py
|
||||
subiquity/server/controllers/tests/__init__.py
|
||||
subiquity/server/controllers/tests/test_keyboard.py
|
||||
subiquity/server/controllers/tests/test_source.py
|
||||
subiquity/server/controllers/timezone.py
|
||||
subiquity/server/controllers/updates.py
|
||||
subiquity/server/controllers/userdata.py
|
||||
|
|
|
@ -83,7 +83,7 @@ class SubiquityClient(TuiApplication):
|
|||
|
||||
snapd_socket_path = '/run/snapd.socket'
|
||||
|
||||
variant = "server"
|
||||
variant = None
|
||||
cmdline = ['snap', 'run', 'subiquity']
|
||||
dryrun_cmdline_module = 'subiquity.cmd.tui'
|
||||
|
||||
|
@ -101,6 +101,7 @@ class SubiquityClient(TuiApplication):
|
|||
"Welcome",
|
||||
"Refresh",
|
||||
"Keyboard",
|
||||
"Source",
|
||||
"Zdev",
|
||||
"Network",
|
||||
"Proxy",
|
||||
|
@ -443,7 +444,8 @@ class SubiquityClient(TuiApplication):
|
|||
endpoint_names.append(c.endpoint_name)
|
||||
if endpoint_names:
|
||||
await self.client.meta.mark_configured.POST(endpoint_names)
|
||||
await self.client.meta.client_variant.POST(self.variant)
|
||||
if self.variant:
|
||||
await self.client.meta.client_variant.POST(self.variant)
|
||||
self.controllers.index = index - 1
|
||||
self.next_screen()
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ from .proxy import ProxyController
|
|||
from .refresh import RefreshController
|
||||
from .serial import SerialController
|
||||
from .snaplist import SnapListController
|
||||
from .source import SourceController
|
||||
from .ssh import SSHController
|
||||
from .welcome import WelcomeController
|
||||
from .zdev import ZdevController
|
||||
|
@ -41,6 +42,7 @@ __all__ = [
|
|||
'RepeatedController',
|
||||
'SerialController',
|
||||
'SnapListController',
|
||||
'SourceController',
|
||||
'SSHController',
|
||||
'WelcomeController',
|
||||
'ZdevController',
|
||||
|
|
|
@ -42,10 +42,6 @@ BIOS_GRUB_SIZE_BYTES = 1 * 1024 * 1024 # 1MiB
|
|||
PREP_GRUB_SIZE_BYTES = 8 * 1024 * 1024 # 8MiB
|
||||
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):
|
||||
|
||||
|
@ -59,7 +55,7 @@ class FilesystemController(SubiquityTuiController, FilesystemManipulator):
|
|||
self.answers.setdefault('manual', [])
|
||||
|
||||
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:
|
||||
self.app.aio_loop.create_task(self._wait_for_probing())
|
||||
return SlowProbing(self)
|
||||
|
@ -67,7 +63,7 @@ class FilesystemController(SubiquityTuiController, FilesystemManipulator):
|
|||
return self.make_guided_ui(status)
|
||||
|
||||
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):
|
||||
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',
|
||||
help=("Show snaps from this section of the store in the snap "
|
||||
"list screen."))
|
||||
parser.add_argument(
|
||||
'--source-catalog', dest='source_catalog', action='store')
|
||||
return parser
|
||||
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ from subiquity.common.types import (
|
|||
SnapInfo,
|
||||
SnapListResponse,
|
||||
SnapSelection,
|
||||
SourceSelectionAndSetting,
|
||||
SSHData,
|
||||
LiveSessionSSHInfo,
|
||||
StorageResponse,
|
||||
|
@ -123,6 +124,10 @@ class API:
|
|||
class steps:
|
||||
def GET(index: Optional[str]) -> AnyStep: ...
|
||||
|
||||
class source:
|
||||
def GET() -> SourceSelectionAndSetting: ...
|
||||
def POST(source_id: str) -> None: ...
|
||||
|
||||
class zdev:
|
||||
def GET() -> List[ZdevInfo]: ...
|
||||
|
||||
|
@ -208,8 +213,7 @@ class API:
|
|||
|
||||
class storage:
|
||||
class guided:
|
||||
def GET(min_size: int = None, wait: bool = False) \
|
||||
-> GuidedStorageResponse:
|
||||
def GET(wait: bool = False) -> GuidedStorageResponse:
|
||||
pass
|
||||
|
||||
def POST(choice: Optional[GuidedChoice]) \
|
||||
|
|
|
@ -168,6 +168,22 @@ class KeyboardSetup:
|
|||
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)
|
||||
class ZdevInfo:
|
||||
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 .proxy import ProxyModel
|
||||
from .snaplist import SnapListModel
|
||||
from .source import SourceModel
|
||||
from .ssh import SSHModel
|
||||
from .timezone import TimeZoneModel
|
||||
from .updates import UpdatesModel
|
||||
|
@ -116,6 +117,7 @@ class SubiquityModel:
|
|||
self.proxy = ProxyModel()
|
||||
self.snaplist = SnapListModel()
|
||||
self.ssh = SSHModel()
|
||||
self.source = SourceModel()
|
||||
self.timezone = TimeZoneModel()
|
||||
self.updates = UpdatesModel()
|
||||
self.userdata = {}
|
||||
|
@ -307,16 +309,13 @@ class SubiquityModel:
|
|||
config = {
|
||||
'stages': stages,
|
||||
|
||||
'sources': {
|
||||
'ubuntu00': 'cp:///media/filesystem'
|
||||
},
|
||||
|
||||
'curthooks_commands': {
|
||||
'001-configure-apt': [
|
||||
resource_path('bin/subiquity-configure-apt'),
|
||||
sys.executable, str(self.network.has_network).lower(),
|
||||
],
|
||||
},
|
||||
|
||||
'grub': {
|
||||
'terminal': 'unmodified',
|
||||
'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 .shutdown import ShutdownController
|
||||
from .snaplist import SnapListController
|
||||
from .source import SourceController
|
||||
from .ssh import SSHController
|
||||
from .timezone import TimeZoneController
|
||||
from .updates import UpdatesController
|
||||
|
@ -54,6 +55,7 @@ __all__ = [
|
|||
'ReportingController',
|
||||
'ShutdownController',
|
||||
'SnapListController',
|
||||
'SourceController',
|
||||
'SSHController',
|
||||
'TimeZoneController',
|
||||
'UpdatesController',
|
||||
|
|
|
@ -62,10 +62,6 @@ from subiquity.server.controller import (
|
|||
log = logging.getLogger("subiquity.server.controller.filesystem")
|
||||
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):
|
||||
|
||||
|
@ -217,13 +213,15 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
|
|||
config, self.model._probe_data['blockdev'], is_probe_data=False)
|
||||
self.configured()
|
||||
|
||||
async def guided_GET(self, min_size: int = None, wait: bool = False) \
|
||||
-> GuidedStorageResponse:
|
||||
async def guided_GET(self, wait: bool = False) -> GuidedStorageResponse:
|
||||
probe_resp = await self._probe_response(wait, GuidedStorageResponse)
|
||||
if probe_resp is not None:
|
||||
return probe_resp
|
||||
if not min_size:
|
||||
min_size = DEFAULT_MIN_SIZE_GUIDED
|
||||
# This calculation is pretty much a hack and we should
|
||||
# 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 = []
|
||||
for raid in self.model._all(type='raid'):
|
||||
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,7 +116,13 @@ class MetaController:
|
|||
async def client_variant_POST(self, variant: str) -> None:
|
||||
if variant not in self.app.supported_variants:
|
||||
raise ValueError(f'unrecognized client variant {variant}')
|
||||
self.app.base_model.set_source_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)
|
||||
|
||||
async def ssh_info_GET(self) -> Optional[LiveSessionSSHInfo]:
|
||||
ips = []
|
||||
|
@ -170,6 +176,7 @@ INSTALL_MODEL_NAMES = ModelNames({
|
|||
"mirror",
|
||||
"network",
|
||||
"proxy",
|
||||
"source",
|
||||
})
|
||||
|
||||
POSTINSTALL_MODEL_NAMES = ModelNames({
|
||||
|
@ -214,6 +221,7 @@ class SubiquityServer(Application):
|
|||
"Kernel",
|
||||
"Keyboard",
|
||||
"Zdev",
|
||||
"Source",
|
||||
"Network",
|
||||
"Proxy",
|
||||
"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/>.
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import string
|
||||
|
||||
|
||||
def run_coro(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