Merge pull request #1038 from canonical/wsl_oobe
OOBE and configuration for WSL
This commit is contained in:
commit
221f7f98f0
|
@ -73,3 +73,6 @@ venv
|
|||
|
||||
# Runtime output
|
||||
.subiquity
|
||||
|
||||
# ignore local vscode config
|
||||
.vscode
|
||||
|
|
16
Makefile
16
Makefile
|
@ -7,10 +7,12 @@ PYTHONPATH=$(shell pwd):$(shell pwd)/probert:$(shell pwd)/curtin
|
|||
PROBERTDIR=./probert
|
||||
PROBERT_REPO=https://github.com/canonical/probert
|
||||
DRYRUN?=--dry-run --bootloader uefi --machine-config examples/simple.json
|
||||
SYSTEM_SETUP_DRYRUN?=--dry-run
|
||||
RECONFIG?=--reconfigure
|
||||
export PYTHONPATH
|
||||
CWD := $(shell pwd)
|
||||
|
||||
CHECK_DIRS := console_conf/ subiquity/ subiquitycore/
|
||||
CHECK_DIRS := console_conf/ subiquity/ subiquitycore/ system_setup/
|
||||
PYTHON := python3
|
||||
|
||||
ifneq (,$(MACHINE))
|
||||
|
@ -48,6 +50,18 @@ dryrun-serial ui-view-serial:
|
|||
dryrun-server:
|
||||
$(PYTHON) -m subiquity.cmd.server $(DRYRUN)
|
||||
|
||||
dryrun-system-setup:
|
||||
$(PYTHON) -m system_setup.cmd.tui $(SYSTEM_SETUP_DRYRUN)
|
||||
|
||||
dryrun-system-setup-server:
|
||||
$(PYTHON) -m system_setup.cmd.server $(SYSTEM_SETUP_DRYRUN)
|
||||
|
||||
dryrun-system-setup-recon:
|
||||
$(PYTHON) -m system_setup.cmd.tui $(SYSTEM_SETUP_DRYRUN) $(RECONFIG)
|
||||
|
||||
dryrun-system-setup-server-recon:
|
||||
$(PYTHON) -m system_setup.cmd.server $(SYSTEM_SETUP_DRYRUN) $(RECONFIG)
|
||||
|
||||
lint: flake8
|
||||
|
||||
flake8:
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
Welcome:
|
||||
lang: en_US
|
||||
WSLIdentity:
|
||||
realname: Ubuntu
|
||||
username: ubuntu
|
||||
# ubuntu
|
||||
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
|
||||
WSLConfigurationBase:
|
||||
custom_path: '/custom_mnt_path'
|
||||
custom_mount_opt: 'opt1 opt2 opt3'
|
||||
gen_host: false
|
||||
gen_resolvconf: false
|
||||
Summary:
|
||||
reboot: yes
|
|
@ -5,6 +5,10 @@ testschema=.subiquity/test-autoinstall-schema.json
|
|||
export PYTHONPATH=$PWD:$PWD/probert:$PWD/curtin
|
||||
|
||||
validate () {
|
||||
mode="install"
|
||||
[ $# -gt 0 ] && mode="$1"
|
||||
|
||||
if [ "${mode}" = "install" ]; then
|
||||
python3 scripts/validate-yaml.py .subiquity/subiquity-curtin-install.conf
|
||||
if [ ! -e .subiquity/subiquity-client-debug.log ] || [ ! -e .subiquity/subiquity-server-debug.log ]; then
|
||||
echo "log file not created"
|
||||
|
@ -15,6 +19,12 @@ validate () {
|
|||
exit 1
|
||||
fi
|
||||
netplan generate --root .subiquity
|
||||
elif [ "${mode}" = "system_setup" ]; then
|
||||
# TODO WSL: Compare generated wsl.conf to oracle
|
||||
echo "system setup validation"
|
||||
else
|
||||
echo "W: Unknown validation mode: ${mode}"
|
||||
fi
|
||||
}
|
||||
|
||||
clean () {
|
||||
|
@ -45,6 +55,7 @@ tty=$(tty) || tty=/dev/console
|
|||
export SUBIQUITY_REPLAY_TIMESCALE=100
|
||||
for answers in examples/answers*.yaml; do
|
||||
clean
|
||||
if echo $answers|grep -vq system-setup; then
|
||||
config=$(sed -n 's/^#machine-config: \(.*\)/\1/p' $answers || true)
|
||||
if [ -z "$config" ]; then
|
||||
config=examples/simple.json
|
||||
|
@ -58,6 +69,10 @@ for answers in examples/answers*.yaml; do
|
|||
timeout --foreground 60 sh -c "LANG=C.UTF-8 python3 -m subiquity.cmd.tui --bootloader uefi --answers $answers --dry-run --snaps-from-examples --machine-config $config $opts" < $tty
|
||||
validate
|
||||
grep -q 'finish: subiquity/Install/install/postinstall/run_unattended_upgrades: SUCCESS: downloading and installing security updates' .subiquity/subiquity-server-debug.log
|
||||
else
|
||||
timeout --foreground 60 sh -c "LANG=C.UTF-8 python3 -m system_setup.cmd.tui --answers $answers --dry-run " < $tty
|
||||
validate "system_setup"
|
||||
fi
|
||||
done
|
||||
|
||||
clean
|
||||
|
|
2
setup.py
2
setup.py
|
@ -117,6 +117,8 @@ setup(name='subiquity',
|
|||
'subiquity-server = subiquity.cmd.server:main',
|
||||
'subiquity-tui = subiquity.cmd.tui:main',
|
||||
'console-conf-tui = console_conf.cmd.tui:main',
|
||||
'system-setup-server = system_setup.cmd.server:main',
|
||||
'system-setup-tui = system_setup.cmd.tui:main',
|
||||
('console-conf-write-login-details = '
|
||||
'console_conf.cmd.write_login_details:main'),
|
||||
],
|
||||
|
|
|
@ -83,6 +83,10 @@ class SubiquityClient(TuiApplication):
|
|||
|
||||
snapd_socket_path = '/run/snapd.socket'
|
||||
|
||||
variant = "server"
|
||||
cmdline = ['snap', 'run', 'subiquity']
|
||||
dryrun_cmdline_module = 'subiquity.cmd.tui'
|
||||
|
||||
from subiquity.client import controllers as controllers_mod
|
||||
project = "subiquity"
|
||||
|
||||
|
@ -171,10 +175,10 @@ class SubiquityClient(TuiApplication):
|
|||
return
|
||||
if self.urwid_loop is not None:
|
||||
self.urwid_loop.stop()
|
||||
cmdline = ['snap', 'run', 'subiquity']
|
||||
cmdline = self.cmdline
|
||||
if self.opts.dry_run:
|
||||
cmdline = [
|
||||
sys.executable, '-m', 'subiquity.cmd.tui',
|
||||
sys.executable, '-m', self.dryrun_cmdline_module,
|
||||
] + sys.argv[1:] + ['--socket', self.opts.socket]
|
||||
if self.opts.server_pid is not None:
|
||||
cmdline.extend(['--server-pid', self.opts.server_pid])
|
||||
|
@ -317,6 +321,10 @@ class SubiquityClient(TuiApplication):
|
|||
print(line)
|
||||
return
|
||||
await super().start()
|
||||
# Progress uses systemd to collect and display the installation
|
||||
# logs. Although some system don't have systemd, so we disable
|
||||
# the progress page
|
||||
if hasattr(self.controllers, "Progress"):
|
||||
journald_listen(
|
||||
self.aio_loop,
|
||||
[status.event_syslog_id],
|
||||
|
@ -435,7 +443,7 @@ 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('server')
|
||||
await self.client.meta.client_variant.POST(self.variant)
|
||||
self.controllers.index = index - 1
|
||||
self.next_screen()
|
||||
|
||||
|
|
|
@ -47,6 +47,8 @@ from subiquity.common.types import (
|
|||
TimeZoneInfo,
|
||||
WLANSupportInstallState,
|
||||
ZdevInfo,
|
||||
WSLConfigurationBase,
|
||||
WSLConfigurationAdvanced,
|
||||
)
|
||||
|
||||
|
||||
|
@ -59,6 +61,8 @@ class API:
|
|||
proxy = simple_endpoint(str)
|
||||
ssh = simple_endpoint(SSHData)
|
||||
updates = simple_endpoint(str)
|
||||
wslconfbase = simple_endpoint(WSLConfigurationBase)
|
||||
wslconfadvanced = simple_endpoint(WSLConfigurationAdvanced)
|
||||
|
||||
class meta:
|
||||
class status:
|
||||
|
|
|
@ -335,3 +335,30 @@ class TimeZoneInfo:
|
|||
class ShutdownMode(enum.Enum):
|
||||
REBOOT = enum.auto()
|
||||
POWEROFF = enum.auto()
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
class WSLConfigurationBase:
|
||||
custom_path: str = attr.ib(default='/mnt/')
|
||||
custom_mount_opt: str = ''
|
||||
gen_host: bool = attr.ib(default=True)
|
||||
gen_resolvconf: bool = attr.ib(default=True)
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
class WSLConfigurationAdvanced:
|
||||
gui_theme: str = attr.ib(default='default')
|
||||
gui_followwintheme: bool = attr.ib(default=True)
|
||||
legacy_gui: bool = attr.ib(default=False)
|
||||
legacy_audio: bool = attr.ib(default=False)
|
||||
adv_ip_detect: bool = attr.ib(default=False)
|
||||
wsl_motd_news: bool = attr.ib(default=True)
|
||||
automount: bool = attr.ib(default=True)
|
||||
mountfstab: bool = attr.ib(default=True)
|
||||
# TODO WSL: remove all duplications from WSLConfigurationBase
|
||||
custom_path: str = attr.ib(default='/mnt/')
|
||||
custom_mount_opt: str = ''
|
||||
gen_host: bool = attr.ib(default=True)
|
||||
gen_resolvconf: bool = attr.ib(default=True)
|
||||
interop_enabled: bool = attr.ib(default=True)
|
||||
interop_appendwindowspath: bool = attr.ib(default=True)
|
||||
|
|
|
@ -146,6 +146,13 @@ class SubiquityModel:
|
|||
self._confirmation_task.cancel()
|
||||
else:
|
||||
self._install_event.set()
|
||||
unconfigured_postinstall_model_names = \
|
||||
self._cur_postinstall_model_names - self._configured_names
|
||||
if unconfigured_postinstall_model_names:
|
||||
if self._postinstall_event.is_set():
|
||||
self._postinstall_event = asyncio.Event()
|
||||
else:
|
||||
self._postinstall_event.set()
|
||||
|
||||
def configured(self, model_name):
|
||||
self._configured_names.add(model_name)
|
||||
|
|
|
@ -19,12 +19,20 @@ import subprocess
|
|||
from subiquity.common.apidef import API
|
||||
from subiquity.common.types import TimeZoneInfo
|
||||
from subiquity.server.controller import SubiquityController
|
||||
from shutil import which
|
||||
import os
|
||||
|
||||
log = logging.getLogger('subiquity.server.controllers.timezone')
|
||||
|
||||
|
||||
def active_timedatectl():
|
||||
return which('timedatectl') and os.path.exists('/run/systemd/system')
|
||||
|
||||
|
||||
def generate_possible_tzs():
|
||||
special_keys = ['', 'geoip']
|
||||
if not active_timedatectl():
|
||||
return special_keys
|
||||
tzcmd = ['timedatectl', 'list-timezones']
|
||||
list_tz_out = subprocess.check_output(tzcmd, universal_newlines=True)
|
||||
real_tzs = list_tz_out.splitlines()
|
||||
|
|
|
@ -74,6 +74,7 @@ from subiquitycore.snapd import (
|
|||
SnapdConnection,
|
||||
)
|
||||
|
||||
NOPROBERARG = "NOPROBER"
|
||||
|
||||
log = logging.getLogger('subiquity.server.server')
|
||||
|
||||
|
@ -112,12 +113,13 @@ class MetaController:
|
|||
controller.configured()
|
||||
|
||||
async def client_variant_POST(self, variant: str) -> None:
|
||||
if variant not in ('desktop', 'server'):
|
||||
if variant not in self.app.supported_variants:
|
||||
raise ValueError(f'unrecognized client variant {variant}')
|
||||
self.app.base_model.set_source_variant(variant)
|
||||
|
||||
async def ssh_info_GET(self) -> Optional[LiveSessionSSHInfo]:
|
||||
ips = []
|
||||
if self.app.base_model.network:
|
||||
for dev in self.app.base_model.network.get_all_netdevs():
|
||||
ips.extend(map(str, dev.actual_global_ip_addresses))
|
||||
if not ips:
|
||||
|
@ -225,6 +227,8 @@ class SubiquityServer(Application):
|
|||
"Shutdown",
|
||||
]
|
||||
|
||||
supported_variants = ["server", "desktop"]
|
||||
|
||||
def make_model(self):
|
||||
root = '/'
|
||||
if self.opts.dry_run:
|
||||
|
@ -253,6 +257,9 @@ class SubiquityServer(Application):
|
|||
|
||||
self.error_reporter = ErrorReporter(
|
||||
self.context.child("ErrorReporter"), self.opts.dry_run, self.root)
|
||||
if opts.machine_config == NOPROBERARG:
|
||||
self.prober = None
|
||||
else:
|
||||
self.prober = Prober(opts.machine_config, self.debug_flags)
|
||||
self.kernel_cmdline = shlex.split(opts.kernel_cmdline)
|
||||
if opts.snaps_from_examples:
|
||||
|
@ -263,9 +270,13 @@ class SubiquityServer(Application):
|
|||
os.path.dirname(__file__))),
|
||||
"examples", "snaps"),
|
||||
self.scale_factor)
|
||||
else:
|
||||
self.snapd = AsyncSnapd(connection)
|
||||
elif os.path.exists(self.snapd_socket_path):
|
||||
connection = SnapdConnection(self.root, self.snapd_socket_path)
|
||||
self.snapd = AsyncSnapd(connection)
|
||||
else:
|
||||
log.info("no snapd socket found. Snap support is disabled")
|
||||
self.snapd = None
|
||||
self.note_data_for_apport("SnapUpdated", str(self.updated))
|
||||
self.event_listeners = []
|
||||
self.autoinstall_config = None
|
||||
|
@ -567,14 +578,20 @@ class SubiquityServer(Application):
|
|||
await self.apply_autoinstall_config()
|
||||
|
||||
def _network_change(self):
|
||||
if not self.snapd:
|
||||
return
|
||||
self.hub.broadcast('snapd-network-change')
|
||||
|
||||
async def _proxy_set(self):
|
||||
if not self.snapd:
|
||||
return
|
||||
await run_in_thread(
|
||||
self.snapd.connection.configure_proxy, self.base_model.proxy)
|
||||
self.hub.broadcast('snapd-network-change')
|
||||
|
||||
def restart(self):
|
||||
if not self.snapd:
|
||||
return
|
||||
cmdline = ['snap', 'run', 'subiquity.subiquity-server']
|
||||
if self.opts.dry_run:
|
||||
cmdline = [
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# Copyright 2015 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/>.
|
||||
|
||||
""" Subiquity """
|
||||
|
||||
from subiquitycore import i18n
|
||||
__all__ = [
|
||||
'i18n',
|
||||
]
|
||||
|
||||
__version__ = "0.0.1"
|
|
@ -0,0 +1,20 @@
|
|||
# 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 sys
|
||||
|
||||
if __name__ == '__main__':
|
||||
from system_setup.cmd.tui import main
|
||||
sys.exit(main())
|
|
@ -0,0 +1,14 @@
|
|||
# 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/>.
|
|
@ -0,0 +1,56 @@
|
|||
# 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 sys
|
||||
|
||||
from subiquity.client.client import SubiquityClient
|
||||
|
||||
log = logging.getLogger('system_setup.client.client')
|
||||
|
||||
|
||||
class SystemSetupClient(SubiquityClient):
|
||||
|
||||
from system_setup.client import controllers as controllers_mod
|
||||
|
||||
snapd_socket_path = None
|
||||
|
||||
variant = "wsl_setup"
|
||||
cmdline = sys.argv
|
||||
dryrun_cmdline_module = 'system_setup.cmd.tui'
|
||||
|
||||
controllers = [
|
||||
"Welcome",
|
||||
"WSLIdentity",
|
||||
"WSLConfigurationBase",
|
||||
"Summary",
|
||||
]
|
||||
|
||||
def __init__(self, opts):
|
||||
# TODO WSL:
|
||||
# 1. remove reconfigure flag
|
||||
# 2. decide on which UI to show up based on existing user UID >=1000
|
||||
# (or default user set in wsl.conf?)
|
||||
# 3. provide an API for this for the flutter UI to know about it
|
||||
# 4. Add Configuration Base page before Advanced
|
||||
# 5. Add language page
|
||||
# self.variant = "wsl_configuration"
|
||||
if opts.reconfigure:
|
||||
self.controllers = [
|
||||
"WSLConfigurationBase",
|
||||
"WSLConfigurationAdvanced",
|
||||
"Summary",
|
||||
]
|
||||
super().__init__(opts)
|
|
@ -0,0 +1,31 @@
|
|||
# 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/>.
|
||||
|
||||
|
||||
from .identity import WSLIdentityController
|
||||
from .wslconfbase import WSLConfigurationBaseController
|
||||
from .summary import SummaryController
|
||||
from .wslconfadvanced import WSLConfigurationAdvancedController
|
||||
|
||||
from subiquity.client.controllers import WelcomeController
|
||||
|
||||
|
||||
__all__ = [
|
||||
'WelcomeController',
|
||||
'WSLIdentityController',
|
||||
'WSLConfigurationBaseController',
|
||||
'WSLConfigurationAdvancedController',
|
||||
'SummaryController',
|
||||
]
|
|
@ -0,0 +1,49 @@
|
|||
# Copyright 2015-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.common.types import IdentityData
|
||||
from system_setup.ui.views import WSLIdentityView
|
||||
|
||||
log = logging.getLogger('system_setup.client.controllers.identity')
|
||||
|
||||
|
||||
class WSLIdentityController(SubiquityTuiController):
|
||||
|
||||
endpoint_name = 'identity'
|
||||
|
||||
async def make_ui(self):
|
||||
data = await self.endpoint.GET()
|
||||
return WSLIdentityView(self, data)
|
||||
|
||||
def run_answers(self):
|
||||
if all(elem in self.answers for elem in
|
||||
['realname', 'username', 'password']):
|
||||
identity = IdentityData(
|
||||
realname=self.answers['realname'],
|
||||
username=self.answers['username'],
|
||||
crypted_password=self.answers['password'])
|
||||
self.done(identity)
|
||||
|
||||
def cancel(self):
|
||||
self.app.prev_screen()
|
||||
|
||||
def done(self, identity_data):
|
||||
log.debug(
|
||||
"IdentityController.done next_screen user_spec=%s",
|
||||
identity_data)
|
||||
self.app.next_screen(self.endpoint.POST(identity_data))
|
|
@ -0,0 +1,94 @@
|
|||
import aiohttp
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from subiquitycore.context import with_context
|
||||
|
||||
from subiquity.client.controller import SubiquityTuiController
|
||||
from subiquity.common.types import (
|
||||
ApplicationState,
|
||||
ShutdownMode
|
||||
)
|
||||
from subiquity.ui.views.installprogress import (
|
||||
InstallRunning,
|
||||
)
|
||||
|
||||
from system_setup.ui.views.summary import SummaryView
|
||||
|
||||
|
||||
log = logging.getLogger('ubuntu_wsl_oobe.controllers.summary')
|
||||
|
||||
|
||||
class SummaryController(SubiquityTuiController):
|
||||
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
self.app_state = None
|
||||
self.crash_report_ref = None
|
||||
self.summary_view = None
|
||||
|
||||
def start(self):
|
||||
self.app.aio_loop.create_task(self._wait_status())
|
||||
|
||||
def cancel(self):
|
||||
self.app.cancel()
|
||||
|
||||
def run_answers(self):
|
||||
pass
|
||||
|
||||
def click_reboot(self):
|
||||
self.app.aio_loop.create_task(self.send_reboot_and_wait())
|
||||
|
||||
async def send_reboot_and_wait(self):
|
||||
try:
|
||||
await self.app.client.shutdown.POST(mode=ShutdownMode.REBOOT)
|
||||
except aiohttp.ClientError:
|
||||
pass
|
||||
self.app.exit()
|
||||
|
||||
@with_context()
|
||||
async def _wait_status(self, context):
|
||||
install_running = None
|
||||
while True:
|
||||
try:
|
||||
app_status = await self.app.client.meta.status.GET(
|
||||
cur=self.app_state)
|
||||
except aiohttp.ClientError:
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
self.app_state = app_status.state
|
||||
|
||||
if self.summary_view:
|
||||
self.summary_view.update_for_state(self.app_state)
|
||||
if app_status.error is not None:
|
||||
if self.crash_report_ref is None:
|
||||
self.crash_report_ref = app_status.error
|
||||
if self.summary_view:
|
||||
self.ui.set_body(self.summary_view)
|
||||
self.app.show_error_report(self.crash_report_ref)
|
||||
|
||||
if self.app_state == ApplicationState.RUNNING:
|
||||
if app_status.confirming_tty != self.app.our_tty:
|
||||
install_running = InstallRunning(
|
||||
self.app, app_status.confirming_tty)
|
||||
self.app.add_global_overlay(install_running)
|
||||
else:
|
||||
if install_running is not None:
|
||||
self.app.remove_global_overlay(install_running)
|
||||
install_running = None
|
||||
|
||||
if self.app_state == ApplicationState.DONE:
|
||||
if self.answers.get('reboot', False):
|
||||
self.click_reboot()
|
||||
|
||||
async def make_ui(self):
|
||||
real_name = ""
|
||||
identity = getattr(self.app.client, "identity")
|
||||
if identity is not None:
|
||||
data = await identity.GET()
|
||||
real_name = data.realname
|
||||
self.summary_view = SummaryView(self, real_name)
|
||||
# We may reach the DONE or ERROR state even before we had a chance
|
||||
# to show the UI.
|
||||
self.summary_view.update_for_state(self.app_state)
|
||||
return self.summary_view
|
|
@ -0,0 +1,67 @@
|
|||
# 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.common.types import WSLConfigurationAdvanced
|
||||
from system_setup.ui.views.wslconfadvanced import WSLConfigurationAdvancedView
|
||||
|
||||
log = logging.getLogger(
|
||||
'system_setup.client.controllers.wslconfigurationadvanced')
|
||||
|
||||
|
||||
class WSLConfigurationAdvancedController(SubiquityTuiController):
|
||||
endpoint_name = 'wslconfadvanced'
|
||||
|
||||
async def make_ui(self):
|
||||
data = await self.endpoint.GET()
|
||||
return WSLConfigurationAdvancedView(self, data)
|
||||
|
||||
def run_answers(self):
|
||||
if all(elem in self.answers for elem in
|
||||
['custom_path', 'custom_mount_opt', 'gen_host',
|
||||
'gen_resolvconf', 'interop_enabled',
|
||||
'interop_appendwindowspath', 'gui_theme',
|
||||
'gui_followwintheme', 'legacy_gui',
|
||||
'legacy_audio', 'adv_ip_detect',
|
||||
'wsl_motd_news', 'automount', 'mountfstab']):
|
||||
reconfiguration = WSLConfigurationAdvanced(
|
||||
custom_path=self.answers['custom_path'],
|
||||
custom_mount_opt=self.answers['custom_mount_opt'],
|
||||
gen_host=self.answers['gen_host'],
|
||||
gen_resolvconf=self.answers['gen_resolvconf'],
|
||||
interop_enabled=self.answers['interop_enabled'],
|
||||
interop_appendwindowspath=self
|
||||
.answers['interop_appendwindowspath'],
|
||||
gui_theme=self.answers['gui_theme'],
|
||||
gui_followwintheme=self.answers['gui_followwintheme'],
|
||||
legacy_gui=self.answers['legacy_gui'],
|
||||
legacy_audio=self.answers['legacy_audio'],
|
||||
adv_ip_detect=self.answers['adv_ip_detect'],
|
||||
wsl_motd_news=self.answers['wsl_motd_news'],
|
||||
automount=self.answers['automount'],
|
||||
mountfstab=self.answers['mountfstab']
|
||||
)
|
||||
self.done(reconfiguration)
|
||||
|
||||
def done(self, reconf_data):
|
||||
log.debug(
|
||||
"WSLConfigurationAdvancedController.done next_screen user_spec=%s",
|
||||
reconf_data)
|
||||
self.app.next_screen(self.endpoint.POST(reconf_data))
|
||||
|
||||
def cancel(self):
|
||||
self.app.prev_screen()
|
|
@ -0,0 +1,35 @@
|
|||
import logging
|
||||
|
||||
from subiquity.client.controller import SubiquityTuiController
|
||||
from subiquity.common.types import WSLConfigurationBase
|
||||
from system_setup.ui.views.wslconfbase import WSLConfigurationBaseView
|
||||
|
||||
log = logging.getLogger('system_setup.client.controllers.wslconfigurationbase')
|
||||
|
||||
|
||||
class WSLConfigurationBaseController(SubiquityTuiController):
|
||||
endpoint_name = 'wslconfbase'
|
||||
|
||||
async def make_ui(self):
|
||||
data = await self.endpoint.GET()
|
||||
return WSLConfigurationBaseView(self, data)
|
||||
|
||||
def run_answers(self):
|
||||
if all(elem in self.answers for elem in
|
||||
['custom_path', 'custom_mount_opt',
|
||||
'gen_host', 'gen_resolvconf']):
|
||||
configuration = WSLConfigurationBase(
|
||||
custom_path=self.answers['custom_path'],
|
||||
custom_mount_opt=self.answers['custom_mount_opt'],
|
||||
gen_host=self.answers['gen_host'],
|
||||
gen_resolvconf=self.answers['gen_resolvconf'])
|
||||
self.done(configuration)
|
||||
|
||||
def done(self, configuration_data):
|
||||
log.debug(
|
||||
"WSLConfigurationBaseController.done next_screen user_spec=%s",
|
||||
configuration_data)
|
||||
self.app.next_screen(self.endpoint.POST(configuration_data))
|
||||
|
||||
def cancel(self):
|
||||
self.app.prev_screen()
|
|
@ -0,0 +1,14 @@
|
|||
# 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/>.
|
|
@ -0,0 +1,88 @@
|
|||
# Copyright 2020-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 argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from subiquitycore.log import setup_logger
|
||||
|
||||
from subiquity.cmd.common import (
|
||||
LOGDIR,
|
||||
setup_environment,
|
||||
)
|
||||
|
||||
|
||||
def make_server_args_parser():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='System Setup - Initial Boot Setup',
|
||||
prog='system_setup')
|
||||
parser.add_argument('--dry-run', action='store_true',
|
||||
dest='dry_run',
|
||||
help='menu-only, do not call installer function')
|
||||
parser.add_argument('--socket')
|
||||
parser.add_argument('--autoinstall', action='store')
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
print('starting server')
|
||||
setup_environment()
|
||||
# setup_environment sets $APPORT_DATA_DIR which must be set before
|
||||
# apport is imported, which is done by this import:
|
||||
from system_setup.server.server import SystemSetupServer
|
||||
from subiquity.server.server import NOPROBERARG
|
||||
parser = make_server_args_parser()
|
||||
opts = parser.parse_args(sys.argv[1:])
|
||||
logdir = LOGDIR
|
||||
opts.snaps_from_examples = False
|
||||
opts.kernel_cmdline = ""
|
||||
opts.machine_config = NOPROBERARG
|
||||
if opts.dry_run:
|
||||
logdir = ".subiquity"
|
||||
if opts.socket is None:
|
||||
if opts.dry_run:
|
||||
opts.socket = '.subiquity/socket'
|
||||
else:
|
||||
opts.socket = '/run/subiquity/socket'
|
||||
os.makedirs(os.path.dirname(opts.socket), exist_ok=True)
|
||||
|
||||
block_log_dir = os.path.join(logdir, "block")
|
||||
os.makedirs(block_log_dir, exist_ok=True)
|
||||
handler = logging.FileHandler(os.path.join(block_log_dir, 'discover.log'))
|
||||
handler.setLevel('DEBUG')
|
||||
handler.setFormatter(
|
||||
logging.Formatter("%(asctime)s %(name)s:%(lineno)d %(message)s"))
|
||||
|
||||
logfiles = setup_logger(dir=logdir, base='systemsetup-server')
|
||||
|
||||
logger = logging.getLogger('systemsetup-server')
|
||||
version = "unknown"
|
||||
logger.info("Starting System Setup server revision {}".format(version))
|
||||
logger.info("Arguments passed: {}".format(sys.argv))
|
||||
|
||||
server = SystemSetupServer(opts, block_log_dir)
|
||||
|
||||
server.note_file_for_apport(
|
||||
"InstallerServerLog", logfiles['debug'])
|
||||
server.note_file_for_apport(
|
||||
"InstallerServerLogInfo", logfiles['info'])
|
||||
|
||||
server.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright 2015-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 argparse
|
||||
import logging
|
||||
import os
|
||||
import fcntl
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from subiquitycore.log import setup_logger
|
||||
|
||||
from subiquity.cmd.common import (
|
||||
LOGDIR,
|
||||
setup_environment,
|
||||
)
|
||||
from system_setup.cmd.server import make_server_args_parser
|
||||
|
||||
|
||||
class ClickAction(argparse.Action):
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
namespace.scripts.append("c(" + repr(values) + ")")
|
||||
|
||||
|
||||
def make_client_args_parser():
|
||||
# TODO WSL: update this. We have already done it on the past…
|
||||
parser = argparse.ArgumentParser(
|
||||
description='SUbiquity - Ubiquity for Servers',
|
||||
prog='subiquity')
|
||||
try:
|
||||
ascii_default = os.ttyname(0) == "/dev/ttysclp0"
|
||||
except OSError:
|
||||
ascii_default = False
|
||||
parser.set_defaults(ascii=ascii_default)
|
||||
parser.add_argument('--dry-run', action='store_true',
|
||||
dest='dry_run',
|
||||
help='menu-only, do not call installer function')
|
||||
# TODO WSL: remove any uneeded arguments
|
||||
parser.add_argument('--socket')
|
||||
parser.add_argument('--serial', action='store_true',
|
||||
dest='run_on_serial',
|
||||
help='Run the installer over serial console.')
|
||||
parser.add_argument('--ssh', action='store_true',
|
||||
dest='ssh',
|
||||
help='Print ssh login details')
|
||||
parser.add_argument('--ascii', action='store_true',
|
||||
dest='ascii',
|
||||
help='Run the installer in ascii mode.')
|
||||
parser.add_argument('--unicode', action='store_false',
|
||||
dest='ascii',
|
||||
help='Run the installer in unicode mode.')
|
||||
parser.add_argument('--screens', action='append', dest='screens',
|
||||
default=[])
|
||||
parser.add_argument('--script', metavar="SCRIPT", action='append',
|
||||
dest='scripts', default=[],
|
||||
help=('Execute SCRIPT in a namespace containing view '
|
||||
'helpers and "ui"'))
|
||||
parser.add_argument('--click', metavar="PAT", action=ClickAction,
|
||||
help='Synthesize a click on a button matching PAT')
|
||||
parser.add_argument('--answers')
|
||||
parser.add_argument('--server-pid')
|
||||
# TODO WSL: remove reconfigure flag and use dynamic decision (see below)
|
||||
# Expose that as an endpoint on the server and decide in the client what
|
||||
# to show
|
||||
parser.add_argument('--reconfigure', action='store_true')
|
||||
return parser
|
||||
|
||||
|
||||
AUTO_ANSWERS_FILE = "/subiquity_config/answers.yaml"
|
||||
|
||||
|
||||
def main():
|
||||
setup_environment()
|
||||
# setup_environment sets $APPORT_DATA_DIR which must be set before
|
||||
# apport is imported, which is done by this import:
|
||||
from system_setup.client.client import SystemSetupClient
|
||||
parser = make_client_args_parser()
|
||||
args = sys.argv[1:]
|
||||
# TODO: make that a common helper between subiquity and system_setup
|
||||
if '--dry-run' in args:
|
||||
opts, unknown = parser.parse_known_args(args)
|
||||
if opts.socket is None:
|
||||
os.makedirs('.subiquity', exist_ok=True)
|
||||
sock_path = '.subiquity/socket'
|
||||
opts.socket = sock_path
|
||||
server_args = ['--dry-run', '--socket=' + sock_path] + unknown
|
||||
server_parser = make_server_args_parser()
|
||||
server_parser.parse_args(server_args) # just to check
|
||||
server_output = open('.subiquity/server-output', 'w')
|
||||
server_cmd = [sys.executable, '-m', 'system_setup.cmd.server'] + \
|
||||
server_args
|
||||
server_proc = subprocess.Popen(
|
||||
server_cmd, stdout=server_output, stderr=subprocess.STDOUT)
|
||||
opts.server_pid = str(server_proc.pid)
|
||||
print("running server pid {}".format(server_proc.pid))
|
||||
elif opts.server_pid is not None:
|
||||
print("reconnecting to server pid {}".format(opts.server_pid))
|
||||
else:
|
||||
opts = parser.parse_args(args)
|
||||
else:
|
||||
opts = parser.parse_args(args)
|
||||
if opts.socket is None:
|
||||
opts.socket = '/run/subiquity/socket'
|
||||
os.makedirs(os.path.basename(opts.socket), exist_ok=True)
|
||||
logdir = LOGDIR
|
||||
if opts.dry_run:
|
||||
logdir = ".subiquity"
|
||||
logfiles = setup_logger(dir=logdir, base='systemsetup-client')
|
||||
|
||||
logger = logging.getLogger('subiquity')
|
||||
version = "unknown"
|
||||
logger.info("Starting System Setup revision {}".format(version))
|
||||
logger.info("Arguments passed: {}".format(sys.argv))
|
||||
|
||||
if opts.answers is None and os.path.exists(AUTO_ANSWERS_FILE):
|
||||
logger.debug("Autoloading answers from %s", AUTO_ANSWERS_FILE)
|
||||
opts.answers = AUTO_ANSWERS_FILE
|
||||
|
||||
if opts.answers:
|
||||
opts.answers = open(opts.answers)
|
||||
try:
|
||||
fcntl.flock(opts.answers, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except OSError:
|
||||
logger.exception(
|
||||
'Failed to lock auto answers file, proceding without it.')
|
||||
opts.answers.close()
|
||||
opts.answers = None
|
||||
|
||||
subiquity_interface = SystemSetupClient(opts)
|
||||
|
||||
subiquity_interface.note_file_for_apport(
|
||||
"InstallerLog", logfiles['debug'])
|
||||
subiquity_interface.note_file_for_apport(
|
||||
"InstallerLogInfo", logfiles['info'])
|
||||
|
||||
subiquity_interface.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
|
@ -0,0 +1,14 @@
|
|||
# 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/>.
|
|
@ -0,0 +1,70 @@
|
|||
# Copyright 2015 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 asyncio
|
||||
import logging
|
||||
|
||||
from subiquity.models.subiquity import SubiquityModel
|
||||
|
||||
from subiquity.models.locale import LocaleModel
|
||||
from subiquity.models.identity import IdentityModel
|
||||
from .wslconfbase import WSLConfigurationBaseModel
|
||||
from .wslconfadvanced import WSLConfigurationAdvancedModel
|
||||
|
||||
|
||||
log = logging.getLogger('system_setup.models.system_server')
|
||||
|
||||
HOSTS_CONTENT = """\
|
||||
127.0.0.1 localhost
|
||||
127.0.1.1 {hostname}
|
||||
|
||||
# The following lines are desirable for IPv6 capable hosts
|
||||
::1 ip6-localhost ip6-loopback
|
||||
fe00::0 ip6-localnet
|
||||
ff00::0 ip6-mcastprefix
|
||||
ff02::1 ip6-allnodes
|
||||
ff02::2 ip6-allrouters
|
||||
"""
|
||||
|
||||
|
||||
class SystemSetupModel(SubiquityModel):
|
||||
"""The overall model for subiquity."""
|
||||
|
||||
target = '/'
|
||||
|
||||
def __init__(self, root, install_model_names, postinstall_model_names):
|
||||
# Parent class init is not called to not load models we don't need.
|
||||
self.root = root
|
||||
if root != '/':
|
||||
self.target = root
|
||||
|
||||
self.packages = []
|
||||
self.userdata = {}
|
||||
self.locale = LocaleModel()
|
||||
self.identity = IdentityModel()
|
||||
self.wslconfbase = WSLConfigurationBaseModel()
|
||||
self.wslconfadvanced = WSLConfigurationAdvancedModel()
|
||||
|
||||
self._confirmation = asyncio.Event()
|
||||
self._confirmation_task = None
|
||||
|
||||
self._configured_names = set()
|
||||
self._install_model_names = install_model_names
|
||||
self._postinstall_model_names = postinstall_model_names
|
||||
self._cur_install_model_names = install_model_names.default_names
|
||||
self._cur_postinstall_model_names = \
|
||||
postinstall_model_names.default_names
|
||||
self._install_event = asyncio.Event()
|
||||
self._postinstall_event = asyncio.Event()
|
|
@ -0,0 +1,135 @@
|
|||
# Copyright 2015 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 subprocess
|
||||
import attr
|
||||
|
||||
from subiquitycore.utils import run_command
|
||||
|
||||
log = logging.getLogger('subiquity.models.wsl_configuration_advanced')
|
||||
|
||||
# TODO WSL: Remove all common attributes with wslconfbase
|
||||
|
||||
|
||||
@attr.s
|
||||
class WSLConfigurationAdvanced(object):
|
||||
gui_theme = attr.ib()
|
||||
gui_followwintheme = attr.ib()
|
||||
legacy_gui = attr.ib()
|
||||
legacy_audio = attr.ib()
|
||||
adv_ip_detect = attr.ib()
|
||||
wsl_motd_news = attr.ib()
|
||||
automount = attr.ib()
|
||||
mountfstab = attr.ib()
|
||||
custom_path = attr.ib()
|
||||
custom_mount_opt = attr.ib()
|
||||
gen_host = attr.ib()
|
||||
gen_resolvconf = attr.ib()
|
||||
interop_enabled = attr.ib()
|
||||
interop_appendwindowspath = attr.ib()
|
||||
|
||||
|
||||
class WSLConfigurationAdvancedModel(object):
|
||||
""" Model representing integration
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._wslconfadvanced = None
|
||||
# TODO WSL: Load settings from system
|
||||
|
||||
def apply_settings(self, result, is_dry_run=False):
|
||||
d = {}
|
||||
# TODO: placholder settings; should be dynamically assgined using
|
||||
# ubuntu-wsl-integration
|
||||
d['custom_path'] = result.custom_path
|
||||
d['custom_mount_opt'] = result.custom_mount_opt
|
||||
d['gen_host'] = result.gen_host
|
||||
d['gen_resolvconf'] = result.gen_resolvconf
|
||||
d['interop_enabled'] = result.interop_enabled
|
||||
d['interop_appendwindowspath'] = result.interop_appendwindowspath
|
||||
d['gui_theme'] = result.gui_theme
|
||||
d['gui_followwintheme'] = result.gui_followwintheme
|
||||
d['legacy_gui'] = result.legacy_gui
|
||||
d['legacy_audio'] = result.legacy_audio
|
||||
d['adv_ip_detect'] = result.adv_ip_detect
|
||||
d['wsl_motd_news'] = result.wsl_motd_news
|
||||
d['automount'] = result.automount
|
||||
d['mountfstab'] = result.mountfstab
|
||||
self._wslconfadvanced = WSLConfigurationAdvancedModel(**d)
|
||||
# TODO WSL: Drop all calls of ubuntuwsl here and ensure the data
|
||||
# are passed to the app model
|
||||
if not is_dry_run:
|
||||
# reset to keep everything as refreshed as new
|
||||
run_command(["/usr/bin/ubuntuwsl", "reset", "-y"],
|
||||
stdout=subprocess.DEVNULL)
|
||||
# set the settings
|
||||
# TODO: placholder settings; should be dynamically generated using
|
||||
# ubuntu-wsl-integration
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"WSL.automount.enabled", result.automount],
|
||||
stdout=subprocess.DEVNULL)
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"WSL.automount.mountfstab", result.mountfstab],
|
||||
stdout=subprocess.DEVNULL)
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"WSL.automount.root", result.custom_path],
|
||||
stdout=subprocess.DEVNULL)
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"WSL.automount.options", result.custom_mount_opt],
|
||||
stdout=subprocess.DEVNULL)
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"WSL.network.generatehosts", result.gen_host],
|
||||
stdout=subprocess.DEVNULL)
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"WSL.network.generateresolvconf",
|
||||
result.gen_resolvconf],
|
||||
stdout=subprocess.DEVNULL)
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"WSL.interop.enabled",
|
||||
result.interop_enabled],
|
||||
stdout=subprocess.DEVNULL)
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"WSL.interop.appendwindowspath",
|
||||
result.interop_appendwindowspath],
|
||||
stdout=subprocess.DEVNULL)
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"ubuntu.GUI.followwintheme",
|
||||
result.gui_followwintheme],
|
||||
stdout=subprocess.DEVNULL)
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"ubuntu.GUI.theme", result.gui_theme],
|
||||
stdout=subprocess.DEVNULL)
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"ubuntu.Interop.guiintergration", result.legacy_gui],
|
||||
stdout=subprocess.DEVNULL)
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"ubuntu.Interop.audiointegration",
|
||||
result.legacy_audio],
|
||||
stdout=subprocess.DEVNULL)
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"ubuntu.Interop.advancedipdetection",
|
||||
result.adv_ip_detect],
|
||||
stdout=subprocess.DEVNULL)
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"ubuntu.Motd.wslnewsenabled", result.wsl_motd_news],
|
||||
stdout=subprocess.DEVNULL)
|
||||
|
||||
@property
|
||||
def wslconfadvanced(self):
|
||||
return self._wslconfadvanced
|
||||
|
||||
def __repr__(self):
|
||||
return "<WSL Conf Advanced: {}>".format(self.wslconfadvanced)
|
|
@ -0,0 +1,74 @@
|
|||
# Copyright 2015 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 subprocess
|
||||
import attr
|
||||
|
||||
from subiquitycore.utils import run_command
|
||||
|
||||
log = logging.getLogger('subiquity.models.wsl_configuration_base')
|
||||
|
||||
|
||||
@attr.s
|
||||
class WSLConfigurationBase(object):
|
||||
custom_path = attr.ib()
|
||||
custom_mount_opt = attr.ib()
|
||||
gen_host = attr.ib()
|
||||
gen_resolvconf = attr.ib()
|
||||
|
||||
|
||||
class WSLConfigurationBaseModel(object):
|
||||
""" Model representing basic wsl configuration
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._wslconfbase = None
|
||||
# TODO WSL: Load settings from system
|
||||
|
||||
def apply_settings(self, result, is_dry_run=False):
|
||||
d = {}
|
||||
d['custom_path'] = result.custom_path
|
||||
d['custom_mount_opt'] = result.custom_mount_opt
|
||||
d['gen_host'] = result.gen_host
|
||||
d['gen_resolvconf'] = result.gen_resolvconf
|
||||
self._wslconfbase = WSLConfigurationBase(**d)
|
||||
# TODO WSL: Drop all calls of ubuntuwsl here and ensure the data
|
||||
# are passed to the app model
|
||||
if not is_dry_run:
|
||||
# reset to keep everything as refreshed as new
|
||||
run_command(["/usr/bin/ubuntuwsl", "reset", "-y"],
|
||||
stdout=subprocess.DEVNULL)
|
||||
# set the settings
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"WSL.automount.root", result.custom_path],
|
||||
stdout=subprocess.DEVNULL)
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"WSL.automount.options", result.custom_mount_opt],
|
||||
stdout=subprocess.DEVNULL)
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"WSL.network.generatehosts", result.gen_host],
|
||||
stdout=subprocess.DEVNULL)
|
||||
run_command(["/usr/bin/ubuntuwsl", "update",
|
||||
"WSL.network.generateresolvconf",
|
||||
result.gen_resolvconf],
|
||||
stdout=subprocess.DEVNULL)
|
||||
|
||||
@property
|
||||
def wslconfbase(self):
|
||||
return self._wslconfbase
|
||||
|
||||
def __repr__(self):
|
||||
return "<WSL Conf Base: {}>".format(self.wslconfbase)
|
|
@ -0,0 +1,14 @@
|
|||
# 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/>.
|
|
@ -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/>.
|
||||
|
||||
from subiquity.server.controllers.cmdlist import (
|
||||
EarlyController,
|
||||
LateController,
|
||||
ErrorController,
|
||||
)
|
||||
from subiquity.server.controllers.locale import LocaleController
|
||||
from subiquity.server.controllers.reporting import ReportingController
|
||||
from subiquity.server.controllers.userdata import UserdataController
|
||||
from .identity import IdentityController
|
||||
from .wslconfbase import WSLConfigurationBaseController
|
||||
from .wslconfadvanced import WSLConfigurationAdvancedController
|
||||
from .configure import ConfigureController
|
||||
from .shutdown import SetupShutdownController
|
||||
|
||||
__all__ = [
|
||||
'EarlyController',
|
||||
'ErrorController',
|
||||
'IdentityController',
|
||||
'LateController',
|
||||
'LocaleController',
|
||||
'ReportingController',
|
||||
'SetupShutdownController',
|
||||
'UserdataController',
|
||||
'WSLConfigurationBaseController',
|
||||
'WSLConfigurationAdvancedController',
|
||||
'ConfigureController',
|
||||
]
|
|
@ -0,0 +1,80 @@
|
|||
# Copyright 2020 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 subiquitycore.context import with_context
|
||||
|
||||
from subiquity.common.errorreport import ErrorReportKind
|
||||
from subiquity.server.controller import (
|
||||
SubiquityController,
|
||||
)
|
||||
|
||||
from subiquity.common.types import (
|
||||
ApplicationState,
|
||||
)
|
||||
|
||||
log = logging.getLogger("subiquity.system_setup.controllers.configure")
|
||||
|
||||
|
||||
class ConfigureController(SubiquityController):
|
||||
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
self.model = app.base_model
|
||||
|
||||
def start(self):
|
||||
self.install_task = self.app.aio_loop.create_task(self.configure())
|
||||
|
||||
@with_context(
|
||||
description="final system configuration", level="INFO",
|
||||
childlevel="DEBUG")
|
||||
async def configure(self, *, context):
|
||||
context.set('is-install-context', True)
|
||||
try:
|
||||
|
||||
self.app.update_state(ApplicationState.WAITING)
|
||||
|
||||
await self.model.wait_install()
|
||||
|
||||
self.app.update_state(ApplicationState.NEEDS_CONFIRMATION)
|
||||
|
||||
self.app.update_state(ApplicationState.RUNNING)
|
||||
|
||||
await self.model.wait_postinstall()
|
||||
|
||||
self.app.update_state(ApplicationState.POST_WAIT)
|
||||
|
||||
# TODO WSL:
|
||||
# 1. Use self.model to get all data to commit
|
||||
# 2. Write directly (without wsl utilities) to wsl.conf and other
|
||||
# fstab files
|
||||
# 3. If not in reconfigure mode: create User, otherwise just write
|
||||
# wsl.conf files.
|
||||
# This must not use subprocesses.
|
||||
# If dry-run: write in .subiquity
|
||||
|
||||
self.app.update_state(ApplicationState.POST_RUNNING)
|
||||
|
||||
self.app.update_state(ApplicationState.DONE)
|
||||
except Exception:
|
||||
kw = {}
|
||||
self.app.make_apport_report(
|
||||
ErrorReportKind.INSTALL_FAIL, "configuration failed", **kw)
|
||||
raise
|
||||
|
||||
def stop_uu(self):
|
||||
# This is a no-op to allow Shutdown controller to depend on this one
|
||||
pass
|
|
@ -0,0 +1,60 @@
|
|||
# Copyright 2015 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 attr
|
||||
|
||||
from subiquity.common.types import IdentityData
|
||||
from subiquity.server.controllers.identity import IdentityController
|
||||
|
||||
log = logging.getLogger('system_setup.server.controllers.identity')
|
||||
|
||||
|
||||
class WSLIdentityController(IdentityController):
|
||||
|
||||
autoinstall_schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'realname': {'type': 'string'},
|
||||
'username': {'type': 'string'},
|
||||
'password': {'type': 'string'},
|
||||
},
|
||||
'required': ['username', 'password'],
|
||||
'additionalProperties': False,
|
||||
}
|
||||
|
||||
def load_autoinstall_data(self, data):
|
||||
if data is not None:
|
||||
identity_data = IdentityData(
|
||||
realname=data.get('realname', ''),
|
||||
username=data['username'],
|
||||
hostname='',
|
||||
crypted_password=data['password'],
|
||||
)
|
||||
self.model.add_user(identity_data)
|
||||
|
||||
def make_autoinstall(self):
|
||||
if self.model.user is None:
|
||||
return {}
|
||||
r = attr.asdict(self.model.user)
|
||||
return r
|
||||
|
||||
async def GET(self) -> IdentityData:
|
||||
data = IdentityData()
|
||||
if self.model.user is not None:
|
||||
data.username = self.model.user.username
|
||||
data.realname = self.model.user.realname
|
||||
return data
|
|
@ -0,0 +1,50 @@
|
|||
# Copyright 2020-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 subiquitycore.context import with_context
|
||||
from subiquity.common.types import ShutdownMode
|
||||
from subiquity.server.controllers import ShutdownController
|
||||
|
||||
log = logging.getLogger("system_setup.controllers.restart")
|
||||
|
||||
|
||||
class SetupShutdownController(ShutdownController):
|
||||
|
||||
def __init__(self, app):
|
||||
# This isn't the most beautiful way, but the shutdown controller
|
||||
# depends on Install, override with our configure one.
|
||||
super().__init__(app)
|
||||
self.app.controllers.Install = self.app.controllers.Configure
|
||||
|
||||
def start(self):
|
||||
# Do not copy logs to target
|
||||
self.server_reboot_event.set()
|
||||
self.app.aio_loop.create_task(self._run())
|
||||
|
||||
@with_context(description='mode={self.mode.name}')
|
||||
def shutdown(self, context):
|
||||
self.shuttingdown_event.set()
|
||||
if not self.opts.dry_run:
|
||||
if self.mode == ShutdownMode.REBOOT:
|
||||
# TODO WSL:
|
||||
# Implement a reboot that doesn't depend on systemd
|
||||
log.Warning("reboot command not implemented")
|
||||
elif self.mode == ShutdownMode.POWEROFF:
|
||||
# TODO WSL:
|
||||
# Implement a poweroff that doesn't depend on systemd
|
||||
log.Warning("poweroff command not implemented")
|
||||
self.app.exit()
|
|
@ -0,0 +1,190 @@
|
|||
# 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 attr
|
||||
from os import path
|
||||
import configparser
|
||||
from subiquitycore.context import with_context
|
||||
|
||||
from subiquity.common.apidef import API
|
||||
from subiquity.common.types import WSLConfigurationAdvanced
|
||||
from subiquity.server.controller import SubiquityController
|
||||
|
||||
log = logging.getLogger(
|
||||
'subiquity.server.controllers.wsl_configuration_advanced')
|
||||
|
||||
|
||||
# TODO WSL: remove all duplicates from WSL config base controller
|
||||
class WSLConfigurationAdvancedController(SubiquityController):
|
||||
|
||||
endpoint = API.wslconfadvanced
|
||||
|
||||
autoinstall_key = model_name = "wslconfadvanced"
|
||||
autoinstall_schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'custom_path': {'type': 'string'},
|
||||
'custom_mount_opt': {'type': 'string'},
|
||||
'gen_host': {'type': 'boolean'},
|
||||
'gen_resolvconf': {'type': 'boolean'},
|
||||
'interop_enabled': {'type': 'boolean'},
|
||||
'interop_appendwindowspath': {'type': 'boolean'},
|
||||
'gui_theme': {'type': 'string'},
|
||||
'gui_followwintheme': {'type': 'boolean'},
|
||||
'legacy_gui': {'type': 'boolean'},
|
||||
'legacy_audio': {'type': 'boolean'},
|
||||
'adv_ip_detect': {'type': 'boolean'},
|
||||
'wsl_motd_news': {'type': 'boolean'},
|
||||
'automount': {'type': 'boolean'},
|
||||
'mountfstab': {'type': 'boolean'}
|
||||
},
|
||||
'required': [],
|
||||
'additionalProperties': False,
|
||||
}
|
||||
|
||||
# this is a temporary simplified reference. The future complete reference
|
||||
# should use the default.json in `ubuntu-wsl-integration`.
|
||||
config_ref = {
|
||||
"wsl": {
|
||||
"automount": {
|
||||
"enabled": "automount",
|
||||
"mountfstab": "mountfstab",
|
||||
"root": "custom_path",
|
||||
"options": "custom_mount_opt",
|
||||
},
|
||||
"network": {
|
||||
"generatehosts": "gen_host",
|
||||
"generateresolvconf": "gen_resolvconf",
|
||||
},
|
||||
"interop": {
|
||||
"enabled": "interop_enabled",
|
||||
"appendwindowspath": "interop_appendwindowspath",
|
||||
}
|
||||
},
|
||||
"ubuntu": {
|
||||
"GUI": {
|
||||
"theme": "gui_theme",
|
||||
"followwintheme": "gui_followwintheme",
|
||||
},
|
||||
"Interop": {
|
||||
"guiintegration": "legacy_gui",
|
||||
"audiointegration": "legacy_audio",
|
||||
"advancedipdetection": "adv_ip_detect",
|
||||
},
|
||||
"Motd": {
|
||||
"wslnewsenabled": "wsl_motd_news",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
|
||||
# load the config file
|
||||
data = {}
|
||||
if path.exists('/etc/wsl.conf'):
|
||||
wslconfig = configparser.ConfigParser()
|
||||
wslconfig.read('/etc/wsl.conf')
|
||||
for a in wslconfig:
|
||||
if a in self.config_ref['wsl']:
|
||||
a_x = wslconfig[a]
|
||||
for b in a_x:
|
||||
if b in self.config_ref['wsl'][a]:
|
||||
data[self.config_ref['wsl'][a][b]] = a_x[b]
|
||||
if path.exists('/etc/ubuntu-wsl.conf'):
|
||||
ubuntuconfig = configparser.ConfigParser()
|
||||
ubuntuconfig.read('/etc/ubuntu-wsl.conf')
|
||||
for a in ubuntuconfig:
|
||||
if a in self.config_ref['ubuntu']:
|
||||
a_x = ubuntuconfig[a]
|
||||
for b in a_x:
|
||||
if b in self.config_ref['ubuntu'][a]:
|
||||
data[self.config_ref['ubuntu'][a][b]] = a_x[b]
|
||||
if data:
|
||||
def bool_converter(x):
|
||||
return x == 'true'
|
||||
reconf_data = WSLConfigurationAdvanced(
|
||||
custom_path=data['custom_path'],
|
||||
custom_mount_opt=data['custom_mount_opt'],
|
||||
gen_host=bool_converter(data['gen_host']),
|
||||
gen_resolvconf=bool_converter(data['gen_resolvconf']),
|
||||
interop_enabled=bool_converter(data['interop_enabled']),
|
||||
interop_appendwindowspath=bool_converter(
|
||||
data['interop_appendwindowspath']),
|
||||
gui_theme=data['gui_theme'],
|
||||
gui_followwintheme=bool_converter(data['gui_followwintheme']),
|
||||
legacy_gui=bool_converter(data['legacy_gui']),
|
||||
legacy_audio=bool_converter(data['legacy_audio']),
|
||||
adv_ip_detect=bool_converter(data['adv_ip_detect']),
|
||||
wsl_motd_news=bool_converter(data['wsl_motd_news']),
|
||||
automount=bool_converter(data['automount']),
|
||||
mountfstab=bool_converter(data['mountfstab']),
|
||||
)
|
||||
self.model.apply_settings(reconf_data, self.opts.dry_run)
|
||||
|
||||
def load_autoinstall_data(self, data):
|
||||
if data is not None:
|
||||
reconf_data = WSLConfigurationAdvanced(
|
||||
custom_path=data['custom_path'],
|
||||
custom_mount_opt=data['custom_mount_opt'],
|
||||
gen_host=data['gen_host'],
|
||||
gen_resolvconf=data['gen_resolvconf'],
|
||||
interop_enabled=data['interop_enabled'],
|
||||
interop_appendwindowspath=data['interop_appendwindowspath'],
|
||||
gui_theme=data['gui_theme'],
|
||||
gui_followwintheme=data['gui_followwintheme'],
|
||||
legacy_gui=data['legacy_gui'],
|
||||
legacy_audio=data['legacy_audio'],
|
||||
adv_ip_detect=data['adv_ip_detect'],
|
||||
wsl_motd_news=data['wsl_motd_news'],
|
||||
automount=data['automount'],
|
||||
mountfstab=data['mountfstab']
|
||||
)
|
||||
self.model.apply_settings(reconf_data, self.opts.dry_run)
|
||||
|
||||
@with_context()
|
||||
async def apply_autoinstall_config(self, context=None):
|
||||
pass
|
||||
|
||||
def make_autoinstall(self):
|
||||
r = attr.asdict(self.model.wslconfadvanced)
|
||||
return r
|
||||
|
||||
async def GET(self) -> WSLConfigurationAdvanced:
|
||||
data = WSLConfigurationAdvanced()
|
||||
if self.model.wslconfadvanced is not None:
|
||||
data.custom_path = self.model.wslconfadvanced.custom_path
|
||||
data.custom_mount_opt = self.model.wslconfadvanced.custom_mount_opt
|
||||
data.gen_host = self.model.wslconfadvanced.gen_host
|
||||
data.gen_resolvconf = self.model.wslconfadvanced.gen_resolvconf
|
||||
data.interop_enabled = self.model.wslconfadvanced.interop_enabled
|
||||
data.interop_appendwindowspath = \
|
||||
self.model.wslconfadvanced.interop_appendwindowspath
|
||||
data.gui_theme = self.model.wslconfadvanced.gui_theme
|
||||
data.gui_followwintheme = \
|
||||
self.model.wslconfadvanced.gui_followwintheme
|
||||
data.legacy_gui = self.model.wslconfadvanced.legacy_gui
|
||||
data.legacy_audio = self.model.wslconfadvanced.legacy_audio
|
||||
data.adv_ip_detect = self.model.wslconfadvanced.adv_ip_detect
|
||||
data.wsl_motd_news = self.model.wslconfadvanced.wsl_motd_news
|
||||
data.automount = self.model.wslconfadvanced.automount
|
||||
data.mountfstab = self.model.wslconfadvanced.mountfstab
|
||||
return data
|
||||
|
||||
async def POST(self, data: WSLConfigurationAdvanced):
|
||||
self.model.apply_settings(data, self.opts.dry_run)
|
||||
self.configured()
|
|
@ -0,0 +1,75 @@
|
|||
# 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 attr
|
||||
|
||||
from subiquitycore.context import with_context
|
||||
|
||||
from subiquity.common.apidef import API
|
||||
from subiquity.common.types import WSLConfigurationBase
|
||||
from subiquity.server.controller import SubiquityController
|
||||
|
||||
log = logging.getLogger('subiquity.server.controllers.wsl_configuration_base')
|
||||
|
||||
|
||||
class WSLConfigurationBaseController(SubiquityController):
|
||||
|
||||
endpoint = API.wslconfbase
|
||||
|
||||
autoinstall_key = model_name = "wslconfbase"
|
||||
autoinstall_schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'custom_path': {'type': 'string'},
|
||||
'custom_mount_opt': {'type': 'string'},
|
||||
'gen_host': {'type': 'boolean'},
|
||||
'gen_resolvconf': {'type': 'boolean'},
|
||||
},
|
||||
'required': [],
|
||||
'additionalProperties': False,
|
||||
}
|
||||
|
||||
def load_autoinstall_data(self, data):
|
||||
if data is not None:
|
||||
identity_data = WSLConfigurationBase(
|
||||
custom_path=data['custom_path'],
|
||||
custom_mount_opt=data['custom_mount_opt'],
|
||||
gen_host=data['gen_host'],
|
||||
gen_resolvconf=data['gen_resolvconf'],
|
||||
)
|
||||
self.model.apply_settings(identity_data, self.opts.dry_run)
|
||||
|
||||
@with_context()
|
||||
async def apply_autoinstall_config(self, context=None):
|
||||
pass
|
||||
|
||||
def make_autoinstall(self):
|
||||
r = attr.asdict(self.model.wslconfbase)
|
||||
return r
|
||||
|
||||
async def GET(self) -> WSLConfigurationBase:
|
||||
data = WSLConfigurationBase()
|
||||
if self.model.wslconfbase is not None:
|
||||
data.custom_path = self.model.wslconfbase.custom_path
|
||||
data.custom_mount_opt = self.model.wslconfbase.custom_mount_opt
|
||||
data.gen_host = self.model.wslconfbase.gen_host
|
||||
data.gen_resolvconf = self.model.wslconfbase.gen_resolvconf
|
||||
return data
|
||||
|
||||
async def POST(self, data: WSLConfigurationBase):
|
||||
self.model.apply_settings(data, self.opts.dry_run)
|
||||
self.configured()
|
|
@ -0,0 +1,59 @@
|
|||
# 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/>.
|
||||
|
||||
from subiquity.server.server import SubiquityServer
|
||||
from system_setup.models.system_setup import SystemSetupModel
|
||||
from subiquity.models.subiquity import ModelNames
|
||||
|
||||
import os
|
||||
|
||||
|
||||
INSTALL_MODEL_NAMES = ModelNames({
|
||||
"locale",
|
||||
"wslconfbase",
|
||||
},
|
||||
wsl_setup={
|
||||
"identity",
|
||||
},
|
||||
wsl_configuration={
|
||||
"wslconfadvanced",
|
||||
})
|
||||
|
||||
POSTINSTALL_MODEL_NAMES = ModelNames(set())
|
||||
|
||||
|
||||
class SystemSetupServer(SubiquityServer):
|
||||
|
||||
from system_setup.server import controllers as controllers_mod
|
||||
controllers = [
|
||||
"Reporting",
|
||||
"Error",
|
||||
"Locale",
|
||||
"Identity",
|
||||
"WSLConfigurationBase",
|
||||
"WSLConfigurationAdvanced",
|
||||
"Configure",
|
||||
"Late",
|
||||
"SetupShutdown",
|
||||
]
|
||||
|
||||
supported_variants = ["wsl_setup", "wsl_configuration"]
|
||||
|
||||
def make_model(self):
|
||||
root = '/'
|
||||
if self.opts.dry_run:
|
||||
root = os.path.abspath('.subiquity')
|
||||
return SystemSetupModel(root, INSTALL_MODEL_NAMES,
|
||||
POSTINSTALL_MODEL_NAMES)
|
|
@ -0,0 +1,14 @@
|
|||
# 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/>.
|
|
@ -0,0 +1,26 @@
|
|||
# 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/>.
|
||||
|
||||
from .identity import WSLIdentityView
|
||||
from .wslconfbase import WSLConfigurationBaseView
|
||||
from .wslconfadvanced import WSLConfigurationAdvancedView
|
||||
from .summary import SummaryView
|
||||
|
||||
__all__ = [
|
||||
'WSLIdentityView',
|
||||
'WSLConfigurationBaseView',
|
||||
'WSLConfigurationAdvancedView',
|
||||
'SummaryView',
|
||||
]
|
|
@ -0,0 +1,89 @@
|
|||
# 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 urwid import (
|
||||
connect_signal,
|
||||
)
|
||||
|
||||
|
||||
from subiquity.common.types import IdentityData
|
||||
from subiquity.ui.views.identity import (
|
||||
IdentityForm,
|
||||
IdentityView,
|
||||
setup_password_validation,
|
||||
)
|
||||
from subiquitycore.ui.utils import screen
|
||||
from subiquitycore.utils import crypt_password
|
||||
from subiquitycore.view import BaseView
|
||||
|
||||
from subiquity.common.resources import resource_path
|
||||
|
||||
|
||||
class WSLIdentityForm(IdentityForm):
|
||||
|
||||
realname = IdentityForm.realname
|
||||
username = IdentityForm.username
|
||||
username.help = \
|
||||
_("The username does not need to match your Windows username")
|
||||
password = IdentityForm.password
|
||||
confirm_password = IdentityForm.confirm_password
|
||||
|
||||
|
||||
class WSLIdentityView(BaseView):
|
||||
title = IdentityView.title
|
||||
excerpt = _("Please create a default UNIX user account. "
|
||||
"For more information visit: https://aka.ms/wslusers")
|
||||
|
||||
def __init__(self, controller, identity_data):
|
||||
self.controller = controller
|
||||
|
||||
reserved_usernames_path = resource_path('reserved-usernames')
|
||||
reserved_usernames = set()
|
||||
if os.path.exists(reserved_usernames_path):
|
||||
with open(reserved_usernames_path) as fp:
|
||||
for line in fp:
|
||||
line = line.strip()
|
||||
if line.startswith('#') or not line:
|
||||
continue
|
||||
reserved_usernames.add(line)
|
||||
else:
|
||||
reserved_usernames.add('root')
|
||||
|
||||
initial = {
|
||||
'realname': identity_data.realname,
|
||||
'username': identity_data.username,
|
||||
}
|
||||
|
||||
# This is the different form model with IdentityView
|
||||
# which prevents us from inheriting it
|
||||
self.form = WSLIdentityForm([], initial)
|
||||
|
||||
connect_signal(self.form, 'submit', self.done)
|
||||
setup_password_validation(self.form, _("passwords"))
|
||||
|
||||
super().__init__(
|
||||
screen(
|
||||
self.form.as_rows(),
|
||||
[self.form.done_btn],
|
||||
excerpt=_(self.excerpt),
|
||||
focus_buttons=False))
|
||||
|
||||
def done(self, result):
|
||||
self.controller.done(IdentityData(
|
||||
realname=self.form.realname.value,
|
||||
username=self.form.username.value,
|
||||
crypted_password=crypt_password(self.form.password.value),
|
||||
))
|
|
@ -0,0 +1,90 @@
|
|||
""" Summary
|
||||
|
||||
Summary provides user with the summary of all the current settings.
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from subiquitycore.ui.utils import button_pile, screen
|
||||
from subiquitycore.view import BaseView
|
||||
from subiquitycore.ui.form import Toggleable
|
||||
from subiquitycore.ui.buttons import (
|
||||
cancel_btn,
|
||||
ok_btn,
|
||||
)
|
||||
from subiquitycore.ui.width import widget_width
|
||||
from subiquity.common.types import ApplicationState
|
||||
|
||||
|
||||
log = logging.getLogger("ubuntu_wsl_oobe.ui.views.summary")
|
||||
|
||||
|
||||
class SummaryView(BaseView):
|
||||
title = _("Setup Complete")
|
||||
|
||||
def __init__(self, controller, real_name):
|
||||
self.controller = controller
|
||||
complete_text = _("Hi {real_name},\n\n"
|
||||
"You have completed the setup!\n\n"
|
||||
"It is suggested to run the following commands"
|
||||
" to update your Ubuntu to the latest version:"
|
||||
"\n\n\n"
|
||||
" $ sudo apt update\n $ sudo apt upgrade\n\n\n"
|
||||
"All settings will take effect after next "
|
||||
"restart of Ubuntu.").format(real_name=real_name)
|
||||
|
||||
self.reboot_btn = Toggleable(ok_btn(
|
||||
_("Reboot Now"), on_press=self.reboot))
|
||||
self.view_error_btn = cancel_btn(
|
||||
_("View error report"), on_press=self.view_error)
|
||||
|
||||
self.event_buttons = button_pile([])
|
||||
super().__init__(
|
||||
screen(
|
||||
rows=[],
|
||||
buttons=self.event_buttons,
|
||||
focus_buttons=True,
|
||||
excerpt=complete_text,
|
||||
)
|
||||
)
|
||||
|
||||
def update_for_state(self, state):
|
||||
btns = []
|
||||
if state == ApplicationState.DONE:
|
||||
btns = [self.reboot_btn]
|
||||
elif state == ApplicationState.ERROR:
|
||||
self.title = _('An error occurred during installation')
|
||||
self.reboot_btn.base_widget.set_label(_("Reboot Now"))
|
||||
self.reboot_btn.enabled = True
|
||||
btns = [
|
||||
self.view_error_btn,
|
||||
self.reboot_btn,
|
||||
]
|
||||
else:
|
||||
raise Exception(state)
|
||||
if self.controller.showing:
|
||||
self.controller.app.ui.set_header(self.title)
|
||||
self._set_buttons(btns)
|
||||
|
||||
def reboot(self, btn):
|
||||
log.debug('reboot clicked')
|
||||
self.reboot_btn.base_widget.set_label(_("Rebooting..."))
|
||||
self.reboot_btn.enabled = False
|
||||
self.event_buttons.original_widget._select_first_selectable()
|
||||
self.controller.click_reboot()
|
||||
self._set_button_width()
|
||||
|
||||
def view_error(self, btn):
|
||||
self.controller.app.show_error_report(self.controller.crash_report_ref)
|
||||
|
||||
def _set_button_width(self):
|
||||
w = 14
|
||||
for b, o in self.event_buttons.original_widget.contents:
|
||||
w = max(widget_width(b), w)
|
||||
self.event_buttons.width = self.event_buttons.min_width = w
|
||||
|
||||
def _set_buttons(self, buttons):
|
||||
p = self.event_buttons.original_widget
|
||||
p.contents[:] = [(b, p.options('pack')) for b in buttons]
|
||||
self._set_button_width()
|
|
@ -0,0 +1,200 @@
|
|||
""" WSLConfigurationAdvanced View
|
||||
|
||||
WSLConfigurationAdvanced provides user with options with additional settings
|
||||
for advanced configuration.
|
||||
|
||||
"""
|
||||
import re
|
||||
|
||||
from urwid import (
|
||||
connect_signal,
|
||||
)
|
||||
|
||||
from subiquitycore.ui.form import (
|
||||
Form,
|
||||
BooleanField,
|
||||
ChoiceField,
|
||||
simple_field,
|
||||
WantsToKnowFormField
|
||||
)
|
||||
from subiquitycore.ui.interactive import StringEditor
|
||||
from subiquitycore.ui.utils import screen
|
||||
from subiquitycore.view import BaseView
|
||||
from subiquity.common.types import WSLConfigurationAdvanced
|
||||
|
||||
|
||||
class MountEditor(StringEditor, WantsToKnowFormField):
|
||||
def keypress(self, size, key):
|
||||
''' restrict what chars we allow for mountpoints '''
|
||||
|
||||
mountpoint = r'[a-zA-Z0-9_/\.\-]'
|
||||
if re.match(mountpoint, key) is None:
|
||||
return False
|
||||
|
||||
return super().keypress(size, key)
|
||||
|
||||
|
||||
MountField = simple_field(MountEditor)
|
||||
StringField = simple_field(StringEditor)
|
||||
|
||||
|
||||
# TODO WSL: Advanced should not contain base configuration
|
||||
# (it must be in 2 pages).
|
||||
|
||||
class WSLConfigurationAdvancedForm(Form):
|
||||
def __init__(self, initial):
|
||||
super().__init__(initial=initial)
|
||||
|
||||
automount = BooleanField(_("Enable Auto-Mount"),
|
||||
help=_("Whether the Auto-Mount freature is"
|
||||
" enabled. This feature allows you "
|
||||
"to mount Windows drive in WSL"))
|
||||
mountfstab = BooleanField(_("Mount `/etc/fstab`"),
|
||||
help=_("Whether `/etc/fstab` will be mounted."
|
||||
" The configuration file `/etc/fstab` "
|
||||
"contains the necessary information to"
|
||||
" automate the process of mounting "
|
||||
"partitions. "))
|
||||
custom_path = MountField(_("Auto-Mount Location"),
|
||||
help=_("Location for the automount"))
|
||||
custom_mount_opt = StringField(_("Auto-Mount Option"),
|
||||
help=_("Mount option passed for "
|
||||
"the automount"))
|
||||
gen_host = BooleanField(_("Enable Host Generation"), help=_(
|
||||
"Selecting enables /etc/host re-generation at every start"))
|
||||
gen_resolvconf = BooleanField(_("Enable resolv.conf Generation"), help=_(
|
||||
"Selecting enables /etc/resolv.conf re-generation at every start"))
|
||||
interop_enabled = BooleanField(_("Enable Interop"),
|
||||
help=_("Whether the interoperability is"
|
||||
" enabled"))
|
||||
interop_appendwindowspath = BooleanField(_("Append Windows Path"),
|
||||
help=_("Whether Windows Path "
|
||||
"will be append in the"
|
||||
" PATH environment "
|
||||
"variable in WSL."))
|
||||
gui_theme = ChoiceField(_("GUI Theme"),
|
||||
help=_("This option changes the Ubuntu theme."),
|
||||
choices=["default", "light", "dark"])
|
||||
gui_followwintheme = BooleanField(_("Follow Windows Theme"),
|
||||
help=_("This option manages whether the"
|
||||
" Ubuntu theme follows the "
|
||||
"Windows theme; that is, when "
|
||||
"Windows uses dark theme, "
|
||||
"Ubuntu also uses dark theme."
|
||||
" Requires WSL interoperability"
|
||||
" enabled. "))
|
||||
legacy_gui = BooleanField(_("Legacy GUI Integration"),
|
||||
help=_("This option enables the Legacy GUI "
|
||||
"Integration on Windows 10. Requires"
|
||||
" a Third-party X Server."))
|
||||
legacy_audio = BooleanField(_("Legacy Audio Integration"),
|
||||
help=_("This option enables the Legacy "
|
||||
"Audio Integration on Windows 10. "
|
||||
"Requires PulseAudio for "
|
||||
"Windows Installed."))
|
||||
adv_ip_detect = BooleanField(_("Advanced IP Detection"),
|
||||
help=_("This option enables advanced "
|
||||
"detection of IP by Windows "
|
||||
"IPv4 Address which is more "
|
||||
"reliable to use with WSL2. "
|
||||
"Requires WSL interoperability"
|
||||
" enabled."))
|
||||
wsl_motd_news = BooleanField(_("Enable WSL News"),
|
||||
help=_("This options allows you to control"
|
||||
" your MOTD News. Toggling it on "
|
||||
"allows you to see the MOTD."))
|
||||
|
||||
def validate_custom_path(self):
|
||||
p = self.custom_path.value
|
||||
if p != "" and (re.fullmatch(r"(/[^/ ]*)+/?", p) is None):
|
||||
return _("Mount location must be a absolute UNIX path"
|
||||
" without space.")
|
||||
|
||||
def validate_custom_mount_opt(self):
|
||||
o = self.custom_mount_opt.value
|
||||
# filesystem independent mount option
|
||||
fsimo = [r"async", r"(no)?atime", r"(no)?auto",
|
||||
r"(fs|def|root)?context=\w+", r"(no)?dev", r"(no)?diratime",
|
||||
r"dirsync", r"(no)?exec", r"group", r"(no)?iversion",
|
||||
r"(no)?mand", r"_netdev", r"nofail", r"(no)?relatime",
|
||||
r"(no)?strictatime", r"(no)?suid", r"owner", r"remount",
|
||||
r"ro", r"rw", r"_rnetdev", r"sync", r"(no)?user", r"users"]
|
||||
# DrvFs filesystem mount option
|
||||
drvfsmo = r"case=(dir|force|off)|metadata|(u|g)id=\d+|(u|f|d)mask=\d+|"
|
||||
fso = "{0}{1}".format(drvfsmo, '|'.join(fsimo))
|
||||
|
||||
if o != "":
|
||||
e_t = ""
|
||||
p = o.split(',')
|
||||
x = True
|
||||
for i in p:
|
||||
if i == "":
|
||||
e_t += _("an empty entry detected; ")
|
||||
x = x and False
|
||||
elif re.fullmatch(fso, i) is not None:
|
||||
x = x and True
|
||||
else:
|
||||
e_t += _("{} is not a valid mount option; ").format(i)
|
||||
x = x and False
|
||||
if not x:
|
||||
return _("Invalid Input: {}Please check "
|
||||
"https://docs.microsoft.com/en-us/windows/wsl/"
|
||||
"wsl-config#mount-options "
|
||||
"for correct valid input").format(e_t)
|
||||
|
||||
|
||||
class WSLConfigurationAdvancedView(BaseView):
|
||||
title = _("WSL advanced options")
|
||||
excerpt = _("In this page, you can configure Ubuntu WSL"
|
||||
"advanced options your needs. \n")
|
||||
|
||||
def __init__(self, controller, configuration_data):
|
||||
self.controller = controller
|
||||
|
||||
initial = {
|
||||
'custom_path': configuration_data.custom_path,
|
||||
'custom_mount_opt': configuration_data.custom_mount_opt,
|
||||
'gen_host': configuration_data.gen_host,
|
||||
'gen_resolvconf': configuration_data.gen_resolvconf,
|
||||
'interop_enabled': configuration_data.interop_enabled,
|
||||
'interop_appendwindowspath':
|
||||
configuration_data.interop_appendwindowspath,
|
||||
'gui_theme': configuration_data.gui_theme,
|
||||
'gui_followwintheme': configuration_data.gui_followwintheme,
|
||||
'legacy_gui': configuration_data.legacy_gui,
|
||||
'legacy_audio': configuration_data.legacy_audio,
|
||||
'adv_ip_detect': configuration_data.adv_ip_detect,
|
||||
'wsl_motd_news': configuration_data.wsl_motd_news,
|
||||
'automount': configuration_data.automount,
|
||||
'mountfstab': configuration_data.mountfstab,
|
||||
}
|
||||
self.form = WSLConfigurationAdvancedForm(initial=initial)
|
||||
|
||||
connect_signal(self.form, 'submit', self.done)
|
||||
super().__init__(
|
||||
screen(
|
||||
self.form.as_rows(),
|
||||
[self.form.done_btn],
|
||||
focus_buttons=True,
|
||||
excerpt=self.excerpt,
|
||||
)
|
||||
)
|
||||
|
||||
def done(self, result):
|
||||
self.controller.done(WSLConfigurationAdvanced(
|
||||
custom_path=self.form.custom_path.value,
|
||||
custom_mount_opt=self.form.custom_mount_opt.value,
|
||||
gen_host=self.form.gen_host.value,
|
||||
gen_resolvconf=self.form.gen_resolvconf.value,
|
||||
interop_enabled=self.form.interop_enabled.value,
|
||||
interop_appendwindowspath=self.form
|
||||
.interop_appendwindowspath.value,
|
||||
gui_theme=self.form.gui_theme.value,
|
||||
gui_followwintheme=self.form.gui_followwintheme.value,
|
||||
legacy_gui=self.form.legacy_gui.value,
|
||||
legacy_audio=self.form.legacy_audio.value,
|
||||
adv_ip_detect=self.form.adv_ip_detect.value,
|
||||
wsl_motd_news=self.form.wsl_motd_news.value,
|
||||
automount=self.form.automount.value,
|
||||
mountfstab=self.form.mountfstab.value,
|
||||
))
|
|
@ -0,0 +1,125 @@
|
|||
""" WSLConfBase
|
||||
|
||||
WSLConfBase provides user with options to set up basic WSL configuration,
|
||||
requested on first setup.
|
||||
|
||||
"""
|
||||
import re
|
||||
|
||||
from urwid import (
|
||||
connect_signal,
|
||||
)
|
||||
|
||||
from subiquitycore.ui.form import (
|
||||
Form,
|
||||
BooleanField,
|
||||
simple_field,
|
||||
WantsToKnowFormField
|
||||
)
|
||||
from subiquitycore.ui.interactive import StringEditor
|
||||
from subiquitycore.ui.utils import screen
|
||||
from subiquitycore.view import BaseView
|
||||
from subiquity.common.types import WSLConfigurationBase
|
||||
|
||||
|
||||
class MountEditor(StringEditor, WantsToKnowFormField):
|
||||
def keypress(self, size, key):
|
||||
''' restrict what chars we allow for mountpoints '''
|
||||
|
||||
mountpoint = r'[a-zA-Z0-9_/\.\-]'
|
||||
if re.match(mountpoint, key) is None:
|
||||
return False
|
||||
|
||||
return super().keypress(size, key)
|
||||
|
||||
|
||||
MountField = simple_field(MountEditor)
|
||||
StringField = simple_field(StringEditor)
|
||||
|
||||
|
||||
class WSLConfBaseForm(Form):
|
||||
def __init__(self, initial):
|
||||
super().__init__(initial=initial)
|
||||
|
||||
custom_path = MountField(_("Mount Location"),
|
||||
help=_("Location for the automount"))
|
||||
custom_mount_opt = StringField(_("Mount Option"),
|
||||
help=_("Mount option passed "
|
||||
"for the automount"))
|
||||
gen_host = BooleanField(_("Enable Host Generation"), help=_(
|
||||
"Selecting enables /etc/host re-generation at every start"))
|
||||
gen_resolvconf = BooleanField(_("Enable resolv.conf Generation"), help=_(
|
||||
"Selecting enables /etc/resolv.conf re-generation at every start"))
|
||||
|
||||
def validate_custom_path(self):
|
||||
p = self.custom_path.value
|
||||
if p != "" and (re.fullmatch(r"(/[^/ ]*)+/?", p) is None):
|
||||
return _("Mount location must be a absolute UNIX path"
|
||||
" without space.")
|
||||
|
||||
def validate_custom_mount_opt(self):
|
||||
o = self.custom_mount_opt.value
|
||||
# filesystem independent mount option
|
||||
fsimo = [r"async", r"(no)?atime", r"(no)?auto",
|
||||
r"(fs|def|root)?context=\w+", r"(no)?dev", r"(no)?diratime",
|
||||
r"dirsync", r"(no)?exec", r"group", r"(no)?iversion",
|
||||
r"(no)?mand", r"_netdev", r"nofail", r"(no)?relatime",
|
||||
r"(no)?strictatime", r"(no)?suid", r"owner", r"remount",
|
||||
r"ro", r"rw", r"_rnetdev", r"sync", r"(no)?user", r"users"]
|
||||
# DrvFs filesystem mount option
|
||||
drvfsmo = r"case=(dir|force|off)|metadata|(u|g)id=\d+|(u|f|d)mask=\d+|"
|
||||
fso = "{0}{1}".format(drvfsmo, '|'.join(fsimo))
|
||||
|
||||
if o != "":
|
||||
e_t = ""
|
||||
p = o.split(',')
|
||||
x = True
|
||||
for i in p:
|
||||
if i == "":
|
||||
e_t += _("an empty entry detected; ")
|
||||
x = x and False
|
||||
elif re.fullmatch(fso, i) is not None:
|
||||
x = x and True
|
||||
else:
|
||||
e_t += _("{} is not a valid mount option; ").format(i)
|
||||
x = x and False
|
||||
if not x:
|
||||
return _("Invalid Input: {}Please check "
|
||||
"https://docs.microsoft.com/en-us/windows/wsl/"
|
||||
"wsl-config#mount-options "
|
||||
"for correct valid input").format(e_t)
|
||||
|
||||
|
||||
class WSLConfigurationBaseView(BaseView):
|
||||
title = _("WSL configuration options")
|
||||
excerpt = _(
|
||||
"In this page, you can configure Ubuntu WSL options to your needs.\n")
|
||||
|
||||
def __init__(self, controller, configuration_data):
|
||||
self.controller = controller
|
||||
|
||||
initial = {
|
||||
'custom_path': configuration_data.custom_path,
|
||||
'custom_mount_opt': configuration_data.custom_mount_opt,
|
||||
'gen_host': configuration_data.gen_host,
|
||||
'gen_resolvconf': configuration_data.gen_resolvconf,
|
||||
}
|
||||
self.form = WSLConfBaseForm(initial=initial)
|
||||
|
||||
connect_signal(self.form, 'submit', self.done)
|
||||
super().__init__(
|
||||
screen(
|
||||
self.form.as_rows(),
|
||||
[self.form.done_btn],
|
||||
focus_buttons=True,
|
||||
excerpt=self.excerpt,
|
||||
)
|
||||
)
|
||||
|
||||
def done(self, result):
|
||||
self.controller.done(WSLConfigurationBase(
|
||||
custom_path=self.form.custom_path.value,
|
||||
custom_mount_opt=self.form.custom_mount_opt.value,
|
||||
gen_host=self.form.gen_host.value,
|
||||
gen_resolvconf=self.form.gen_resolvconf.value
|
||||
))
|
Loading…
Reference in New Issue