Merge pull request #1038 from canonical/wsl_oobe

OOBE and configuration for WSL
This commit is contained in:
Michael Hudson-Doyle 2021-09-06 20:23:14 +12:00 committed by GitHub
commit 221f7f98f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 2209 additions and 39 deletions

3
.gitignore vendored
View File

@ -73,3 +73,6 @@ venv
# Runtime output # Runtime output
.subiquity .subiquity
# ignore local vscode config
.vscode

View File

@ -7,10 +7,12 @@ PYTHONPATH=$(shell pwd):$(shell pwd)/probert:$(shell pwd)/curtin
PROBERTDIR=./probert PROBERTDIR=./probert
PROBERT_REPO=https://github.com/canonical/probert PROBERT_REPO=https://github.com/canonical/probert
DRYRUN?=--dry-run --bootloader uefi --machine-config examples/simple.json DRYRUN?=--dry-run --bootloader uefi --machine-config examples/simple.json
SYSTEM_SETUP_DRYRUN?=--dry-run
RECONFIG?=--reconfigure
export PYTHONPATH export PYTHONPATH
CWD := $(shell pwd) CWD := $(shell pwd)
CHECK_DIRS := console_conf/ subiquity/ subiquitycore/ CHECK_DIRS := console_conf/ subiquity/ subiquitycore/ system_setup/
PYTHON := python3 PYTHON := python3
ifneq (,$(MACHINE)) ifneq (,$(MACHINE))
@ -48,6 +50,18 @@ dryrun-serial ui-view-serial:
dryrun-server: dryrun-server:
$(PYTHON) -m subiquity.cmd.server $(DRYRUN) $(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 lint: flake8
flake8: flake8:

View File

@ -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

View File

@ -5,6 +5,10 @@ testschema=.subiquity/test-autoinstall-schema.json
export PYTHONPATH=$PWD:$PWD/probert:$PWD/curtin export PYTHONPATH=$PWD:$PWD/probert:$PWD/curtin
validate () { validate () {
mode="install"
[ $# -gt 0 ] && mode="$1"
if [ "${mode}" = "install" ]; then
python3 scripts/validate-yaml.py .subiquity/subiquity-curtin-install.conf python3 scripts/validate-yaml.py .subiquity/subiquity-curtin-install.conf
if [ ! -e .subiquity/subiquity-client-debug.log ] || [ ! -e .subiquity/subiquity-server-debug.log ]; then if [ ! -e .subiquity/subiquity-client-debug.log ] || [ ! -e .subiquity/subiquity-server-debug.log ]; then
echo "log file not created" echo "log file not created"
@ -15,6 +19,12 @@ validate () {
exit 1 exit 1
fi fi
netplan generate --root .subiquity 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 () { clean () {
@ -45,6 +55,7 @@ tty=$(tty) || tty=/dev/console
export SUBIQUITY_REPLAY_TIMESCALE=100 export SUBIQUITY_REPLAY_TIMESCALE=100
for answers in examples/answers*.yaml; do for answers in examples/answers*.yaml; do
clean clean
if echo $answers|grep -vq system-setup; then
config=$(sed -n 's/^#machine-config: \(.*\)/\1/p' $answers || true) config=$(sed -n 's/^#machine-config: \(.*\)/\1/p' $answers || true)
if [ -z "$config" ]; then if [ -z "$config" ]; then
config=examples/simple.json 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 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 validate
grep -q 'finish: subiquity/Install/install/postinstall/run_unattended_upgrades: SUCCESS: downloading and installing security updates' .subiquity/subiquity-server-debug.log 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 done
clean clean

View File

@ -117,6 +117,8 @@ setup(name='subiquity',
'subiquity-server = subiquity.cmd.server:main', 'subiquity-server = subiquity.cmd.server:main',
'subiquity-tui = subiquity.cmd.tui:main', 'subiquity-tui = subiquity.cmd.tui:main',
'console-conf-tui = console_conf.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-write-login-details = '
'console_conf.cmd.write_login_details:main'), 'console_conf.cmd.write_login_details:main'),
], ],

View File

@ -83,6 +83,10 @@ class SubiquityClient(TuiApplication):
snapd_socket_path = '/run/snapd.socket' 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 from subiquity.client import controllers as controllers_mod
project = "subiquity" project = "subiquity"
@ -171,10 +175,10 @@ class SubiquityClient(TuiApplication):
return return
if self.urwid_loop is not None: if self.urwid_loop is not None:
self.urwid_loop.stop() self.urwid_loop.stop()
cmdline = ['snap', 'run', 'subiquity'] cmdline = self.cmdline
if self.opts.dry_run: if self.opts.dry_run:
cmdline = [ cmdline = [
sys.executable, '-m', 'subiquity.cmd.tui', sys.executable, '-m', self.dryrun_cmdline_module,
] + sys.argv[1:] + ['--socket', self.opts.socket] ] + sys.argv[1:] + ['--socket', self.opts.socket]
if self.opts.server_pid is not None: if self.opts.server_pid is not None:
cmdline.extend(['--server-pid', self.opts.server_pid]) cmdline.extend(['--server-pid', self.opts.server_pid])
@ -317,6 +321,10 @@ class SubiquityClient(TuiApplication):
print(line) print(line)
return return
await super().start() 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( journald_listen(
self.aio_loop, self.aio_loop,
[status.event_syslog_id], [status.event_syslog_id],
@ -435,7 +443,7 @@ class SubiquityClient(TuiApplication):
endpoint_names.append(c.endpoint_name) endpoint_names.append(c.endpoint_name)
if endpoint_names: if endpoint_names:
await self.client.meta.mark_configured.POST(endpoint_names) await self.client.meta.mark_configured.POST(endpoint_names)
await self.client.meta.client_variant.POST('server') await self.client.meta.client_variant.POST(self.variant)
self.controllers.index = index - 1 self.controllers.index = index - 1
self.next_screen() self.next_screen()

View File

@ -47,6 +47,8 @@ from subiquity.common.types import (
TimeZoneInfo, TimeZoneInfo,
WLANSupportInstallState, WLANSupportInstallState,
ZdevInfo, ZdevInfo,
WSLConfigurationBase,
WSLConfigurationAdvanced,
) )
@ -59,6 +61,8 @@ class API:
proxy = simple_endpoint(str) proxy = simple_endpoint(str)
ssh = simple_endpoint(SSHData) ssh = simple_endpoint(SSHData)
updates = simple_endpoint(str) updates = simple_endpoint(str)
wslconfbase = simple_endpoint(WSLConfigurationBase)
wslconfadvanced = simple_endpoint(WSLConfigurationAdvanced)
class meta: class meta:
class status: class status:

View File

@ -335,3 +335,30 @@ class TimeZoneInfo:
class ShutdownMode(enum.Enum): class ShutdownMode(enum.Enum):
REBOOT = enum.auto() REBOOT = enum.auto()
POWEROFF = 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)

View File

@ -146,6 +146,13 @@ class SubiquityModel:
self._confirmation_task.cancel() self._confirmation_task.cancel()
else: else:
self._install_event.set() 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): def configured(self, model_name):
self._configured_names.add(model_name) self._configured_names.add(model_name)

View File

@ -19,12 +19,20 @@ import subprocess
from subiquity.common.apidef import API from subiquity.common.apidef import API
from subiquity.common.types import TimeZoneInfo from subiquity.common.types import TimeZoneInfo
from subiquity.server.controller import SubiquityController from subiquity.server.controller import SubiquityController
from shutil import which
import os
log = logging.getLogger('subiquity.server.controllers.timezone') log = logging.getLogger('subiquity.server.controllers.timezone')
def active_timedatectl():
return which('timedatectl') and os.path.exists('/run/systemd/system')
def generate_possible_tzs(): def generate_possible_tzs():
special_keys = ['', 'geoip'] special_keys = ['', 'geoip']
if not active_timedatectl():
return special_keys
tzcmd = ['timedatectl', 'list-timezones'] tzcmd = ['timedatectl', 'list-timezones']
list_tz_out = subprocess.check_output(tzcmd, universal_newlines=True) list_tz_out = subprocess.check_output(tzcmd, universal_newlines=True)
real_tzs = list_tz_out.splitlines() real_tzs = list_tz_out.splitlines()

View File

@ -74,6 +74,7 @@ from subiquitycore.snapd import (
SnapdConnection, SnapdConnection,
) )
NOPROBERARG = "NOPROBER"
log = logging.getLogger('subiquity.server.server') log = logging.getLogger('subiquity.server.server')
@ -112,12 +113,13 @@ class MetaController:
controller.configured() controller.configured()
async def client_variant_POST(self, variant: str) -> None: 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}') raise ValueError(f'unrecognized client variant {variant}')
self.app.base_model.set_source_variant(variant) self.app.base_model.set_source_variant(variant)
async def ssh_info_GET(self) -> Optional[LiveSessionSSHInfo]: async def ssh_info_GET(self) -> Optional[LiveSessionSSHInfo]:
ips = [] ips = []
if self.app.base_model.network:
for dev in self.app.base_model.network.get_all_netdevs(): for dev in self.app.base_model.network.get_all_netdevs():
ips.extend(map(str, dev.actual_global_ip_addresses)) ips.extend(map(str, dev.actual_global_ip_addresses))
if not ips: if not ips:
@ -225,6 +227,8 @@ class SubiquityServer(Application):
"Shutdown", "Shutdown",
] ]
supported_variants = ["server", "desktop"]
def make_model(self): def make_model(self):
root = '/' root = '/'
if self.opts.dry_run: if self.opts.dry_run:
@ -253,6 +257,9 @@ class SubiquityServer(Application):
self.error_reporter = ErrorReporter( self.error_reporter = ErrorReporter(
self.context.child("ErrorReporter"), self.opts.dry_run, self.root) 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.prober = Prober(opts.machine_config, self.debug_flags)
self.kernel_cmdline = shlex.split(opts.kernel_cmdline) self.kernel_cmdline = shlex.split(opts.kernel_cmdline)
if opts.snaps_from_examples: if opts.snaps_from_examples:
@ -263,9 +270,13 @@ class SubiquityServer(Application):
os.path.dirname(__file__))), os.path.dirname(__file__))),
"examples", "snaps"), "examples", "snaps"),
self.scale_factor) self.scale_factor)
else: self.snapd = AsyncSnapd(connection)
elif os.path.exists(self.snapd_socket_path):
connection = SnapdConnection(self.root, self.snapd_socket_path) connection = SnapdConnection(self.root, self.snapd_socket_path)
self.snapd = AsyncSnapd(connection) 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.note_data_for_apport("SnapUpdated", str(self.updated))
self.event_listeners = [] self.event_listeners = []
self.autoinstall_config = None self.autoinstall_config = None
@ -567,14 +578,20 @@ class SubiquityServer(Application):
await self.apply_autoinstall_config() await self.apply_autoinstall_config()
def _network_change(self): def _network_change(self):
if not self.snapd:
return
self.hub.broadcast('snapd-network-change') self.hub.broadcast('snapd-network-change')
async def _proxy_set(self): async def _proxy_set(self):
if not self.snapd:
return
await run_in_thread( await run_in_thread(
self.snapd.connection.configure_proxy, self.base_model.proxy) self.snapd.connection.configure_proxy, self.base_model.proxy)
self.hub.broadcast('snapd-network-change') self.hub.broadcast('snapd-network-change')
def restart(self): def restart(self):
if not self.snapd:
return
cmdline = ['snap', 'run', 'subiquity.subiquity-server'] cmdline = ['snap', 'run', 'subiquity.subiquity-server']
if self.opts.dry_run: if self.opts.dry_run:
cmdline = [ cmdline = [

23
system_setup/__init__.py Normal file
View File

@ -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"

20
system_setup/__main__.py Normal file
View File

@ -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())

View File

@ -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/>.

View File

@ -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)

View File

@ -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',
]

View File

@ -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))

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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/>.

View File

@ -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())

153
system_setup/cmd/tui.py Executable file
View File

@ -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())

View File

@ -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/>.

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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/>.

View File

@ -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',
]

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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/>.

View File

@ -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',
]

View File

@ -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),
))

View File

@ -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()

View File

@ -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,
))

View File

@ -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
))