diff --git a/.gitignore b/.gitignore
index 7c8764e0..1f3e3acc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -73,3 +73,6 @@ venv
# Runtime output
.subiquity
+
+# ignore local vscode config
+.vscode
diff --git a/Makefile b/Makefile
index 55db49d3..f82c5670 100644
--- a/Makefile
+++ b/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:
diff --git a/examples/answers-system-setup.yaml b/examples/answers-system-setup.yaml
new file mode 100644
index 00000000..4da0b03e
--- /dev/null
+++ b/examples/answers-system-setup.yaml
@@ -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
\ No newline at end of file
diff --git a/scripts/runtests.sh b/scripts/runtests.sh
index 96c51728..71f42c8b 100755
--- a/scripts/runtests.sh
+++ b/scripts/runtests.sh
@@ -5,16 +5,26 @@ testschema=.subiquity/test-autoinstall-schema.json
export PYTHONPATH=$PWD:$PWD/probert:$PWD/curtin
validate () {
- 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"
- exit 1
+ 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"
+ exit 1
+ fi
+ if grep passw0rd .subiquity/subiquity-client-debug.log .subiquity/subiquity-server-debug.log | grep -v "Loaded answers" | grep -v "answers_action"; then
+ echo "password leaked into log file"
+ 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
- if grep passw0rd .subiquity/subiquity-client-debug.log .subiquity/subiquity-server-debug.log | grep -v "Loaded answers" | grep -v "answers_action"; then
- echo "password leaked into log file"
- exit 1
- fi
- netplan generate --root .subiquity
}
clean () {
@@ -45,19 +55,24 @@ tty=$(tty) || tty=/dev/console
export SUBIQUITY_REPLAY_TIMESCALE=100
for answers in examples/answers*.yaml; do
clean
- config=$(sed -n 's/^#machine-config: \(.*\)/\1/p' $answers || true)
- if [ -z "$config" ]; then
- config=examples/simple.json
+ 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
+ fi
+ serial=$(sed -n 's/^#serial/x/p' $answers || true)
+ opts=''
+ if [ -n "$serial" ]; then
+ opts='--serial'
+ fi
+ # The --foreground is important to avoid subiquity getting SIGTTOU-ed.
+ 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
- serial=$(sed -n 's/^#serial/x/p' $answers || true)
- opts=''
- if [ -n "$serial" ]; then
- opts='--serial'
- fi
- # The --foreground is important to avoid subiquity getting SIGTTOU-ed.
- 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
done
clean
diff --git a/setup.py b/setup.py
index d635710b..1fadd085 100644
--- a/setup.py
+++ b/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'),
],
diff --git a/subiquity/client/client.py b/subiquity/client/client.py
index d513ce38..ef3f8111 100644
--- a/subiquity/client/client.py
+++ b/subiquity/client/client.py
@@ -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,14 +321,18 @@ class SubiquityClient(TuiApplication):
print(line)
return
await super().start()
- journald_listen(
- self.aio_loop,
- [status.event_syslog_id],
- self.controllers.Progress.event)
- journald_listen(
- self.aio_loop,
- [status.log_syslog_id],
- self.controllers.Progress.log_line)
+ # 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],
+ self.controllers.Progress.event)
+ journald_listen(
+ self.aio_loop,
+ [status.log_syslog_id],
+ self.controllers.Progress.log_line)
if not status.cloud_init_ok:
self.add_global_overlay(CloudInitFail(self))
self.error_reporter.load_reports()
@@ -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()
diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py
index 3bacac94..e8400bc4 100644
--- a/subiquity/common/apidef.py
+++ b/subiquity/common/apidef.py
@@ -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:
diff --git a/subiquity/common/types.py b/subiquity/common/types.py
index 8461b11d..4000e459 100644
--- a/subiquity/common/types.py
+++ b/subiquity/common/types.py
@@ -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)
diff --git a/subiquity/models/subiquity.py b/subiquity/models/subiquity.py
index 93c93470..99de443c 100644
--- a/subiquity/models/subiquity.py
+++ b/subiquity/models/subiquity.py
@@ -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)
diff --git a/subiquity/server/controllers/timezone.py b/subiquity/server/controllers/timezone.py
index b3403003..760e7657 100644
--- a/subiquity/server/controllers/timezone.py
+++ b/subiquity/server/controllers/timezone.py
@@ -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()
diff --git a/subiquity/server/server.py b/subiquity/server/server.py
index c4651e94..e833367d 100644
--- a/subiquity/server/server.py
+++ b/subiquity/server/server.py
@@ -74,6 +74,7 @@ from subiquitycore.snapd import (
SnapdConnection,
)
+NOPROBERARG = "NOPROBER"
log = logging.getLogger('subiquity.server.server')
@@ -112,14 +113,15 @@ 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 = []
- for dev in self.app.base_model.network.get_all_netdevs():
- ips.extend(map(str, dev.actual_global_ip_addresses))
+ 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:
return None
username = self.app.installer_user_name
@@ -225,6 +227,8 @@ class SubiquityServer(Application):
"Shutdown",
]
+ supported_variants = ["server", "desktop"]
+
def make_model(self):
root = '/'
if self.opts.dry_run:
@@ -253,7 +257,10 @@ class SubiquityServer(Application):
self.error_reporter = ErrorReporter(
self.context.child("ErrorReporter"), self.opts.dry_run, self.root)
- self.prober = Prober(opts.machine_config, self.debug_flags)
+ 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:
connection = FakeSnapdConnection(
@@ -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)
+ 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 = [
diff --git a/system_setup/__init__.py b/system_setup/__init__.py
new file mode 100644
index 00000000..eb0a6ef9
--- /dev/null
+++ b/system_setup/__init__.py
@@ -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 .
+
+""" Subiquity """
+
+from subiquitycore import i18n
+__all__ = [
+ 'i18n',
+]
+
+__version__ = "0.0.1"
diff --git a/system_setup/__main__.py b/system_setup/__main__.py
new file mode 100644
index 00000000..5b826b12
--- /dev/null
+++ b/system_setup/__main__.py
@@ -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 .
+
+import sys
+
+if __name__ == '__main__':
+ from system_setup.cmd.tui import main
+ sys.exit(main())
diff --git a/system_setup/client/__init__.py b/system_setup/client/__init__.py
new file mode 100644
index 00000000..8290406c
--- /dev/null
+++ b/system_setup/client/__init__.py
@@ -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 .
diff --git a/system_setup/client/client.py b/system_setup/client/client.py
new file mode 100644
index 00000000..2439f967
--- /dev/null
+++ b/system_setup/client/client.py
@@ -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 .
+
+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)
diff --git a/system_setup/client/controllers/__init__.py b/system_setup/client/controllers/__init__.py
new file mode 100644
index 00000000..9720388c
--- /dev/null
+++ b/system_setup/client/controllers/__init__.py
@@ -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 .
+
+
+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',
+]
diff --git a/system_setup/client/controllers/identity.py b/system_setup/client/controllers/identity.py
new file mode 100644
index 00000000..48364b53
--- /dev/null
+++ b/system_setup/client/controllers/identity.py
@@ -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 .
+
+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))
diff --git a/system_setup/client/controllers/summary.py b/system_setup/client/controllers/summary.py
new file mode 100644
index 00000000..7616af60
--- /dev/null
+++ b/system_setup/client/controllers/summary.py
@@ -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
diff --git a/system_setup/client/controllers/wslconfadvanced.py b/system_setup/client/controllers/wslconfadvanced.py
new file mode 100644
index 00000000..8b0d65cd
--- /dev/null
+++ b/system_setup/client/controllers/wslconfadvanced.py
@@ -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 .
+
+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()
diff --git a/system_setup/client/controllers/wslconfbase.py b/system_setup/client/controllers/wslconfbase.py
new file mode 100644
index 00000000..a83efc0d
--- /dev/null
+++ b/system_setup/client/controllers/wslconfbase.py
@@ -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()
diff --git a/system_setup/cmd/__init__.py b/system_setup/cmd/__init__.py
new file mode 100644
index 00000000..8290406c
--- /dev/null
+++ b/system_setup/cmd/__init__.py
@@ -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 .
diff --git a/system_setup/cmd/server.py b/system_setup/cmd/server.py
new file mode 100644
index 00000000..0e174b1b
--- /dev/null
+++ b/system_setup/cmd/server.py
@@ -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 .
+
+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())
diff --git a/system_setup/cmd/tui.py b/system_setup/cmd/tui.py
new file mode 100755
index 00000000..92fb8c57
--- /dev/null
+++ b/system_setup/cmd/tui.py
@@ -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 .
+
+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())
diff --git a/system_setup/models/__init__.py b/system_setup/models/__init__.py
new file mode 100644
index 00000000..8290406c
--- /dev/null
+++ b/system_setup/models/__init__.py
@@ -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 .
diff --git a/system_setup/models/system_setup.py b/system_setup/models/system_setup.py
new file mode 100644
index 00000000..3496b474
--- /dev/null
+++ b/system_setup/models/system_setup.py
@@ -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 .
+
+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()
diff --git a/system_setup/models/wslconfadvanced.py b/system_setup/models/wslconfadvanced.py
new file mode 100644
index 00000000..3b6262b2
--- /dev/null
+++ b/system_setup/models/wslconfadvanced.py
@@ -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 .
+
+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 "".format(self.wslconfadvanced)
diff --git a/system_setup/models/wslconfbase.py b/system_setup/models/wslconfbase.py
new file mode 100644
index 00000000..c2df10b5
--- /dev/null
+++ b/system_setup/models/wslconfbase.py
@@ -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 .
+
+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 "".format(self.wslconfbase)
diff --git a/system_setup/server/__init__.py b/system_setup/server/__init__.py
new file mode 100644
index 00000000..8290406c
--- /dev/null
+++ b/system_setup/server/__init__.py
@@ -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 .
diff --git a/system_setup/server/controllers/__init__.py b/system_setup/server/controllers/__init__.py
new file mode 100644
index 00000000..e75eec73
--- /dev/null
+++ b/system_setup/server/controllers/__init__.py
@@ -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 .
+
+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',
+]
diff --git a/system_setup/server/controllers/configure.py b/system_setup/server/controllers/configure.py
new file mode 100644
index 00000000..63957943
--- /dev/null
+++ b/system_setup/server/controllers/configure.py
@@ -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 .
+
+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
diff --git a/system_setup/server/controllers/identity.py b/system_setup/server/controllers/identity.py
new file mode 100644
index 00000000..16dd98cc
--- /dev/null
+++ b/system_setup/server/controllers/identity.py
@@ -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 .
+
+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
diff --git a/system_setup/server/controllers/shutdown.py b/system_setup/server/controllers/shutdown.py
new file mode 100644
index 00000000..17859cb8
--- /dev/null
+++ b/system_setup/server/controllers/shutdown.py
@@ -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 .
+
+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()
diff --git a/system_setup/server/controllers/wslconfadvanced.py b/system_setup/server/controllers/wslconfadvanced.py
new file mode 100644
index 00000000..3de13d2a
--- /dev/null
+++ b/system_setup/server/controllers/wslconfadvanced.py
@@ -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 .
+
+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()
diff --git a/system_setup/server/controllers/wslconfbase.py b/system_setup/server/controllers/wslconfbase.py
new file mode 100644
index 00000000..41f90350
--- /dev/null
+++ b/system_setup/server/controllers/wslconfbase.py
@@ -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 .
+
+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()
diff --git a/system_setup/server/server.py b/system_setup/server/server.py
new file mode 100644
index 00000000..b73acf8e
--- /dev/null
+++ b/system_setup/server/server.py
@@ -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 .
+
+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)
diff --git a/system_setup/ui/__init__.py b/system_setup/ui/__init__.py
new file mode 100644
index 00000000..8290406c
--- /dev/null
+++ b/system_setup/ui/__init__.py
@@ -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 .
diff --git a/system_setup/ui/views/__init__.py b/system_setup/ui/views/__init__.py
new file mode 100644
index 00000000..524b68f9
--- /dev/null
+++ b/system_setup/ui/views/__init__.py
@@ -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 .
+
+from .identity import WSLIdentityView
+from .wslconfbase import WSLConfigurationBaseView
+from .wslconfadvanced import WSLConfigurationAdvancedView
+from .summary import SummaryView
+
+__all__ = [
+ 'WSLIdentityView',
+ 'WSLConfigurationBaseView',
+ 'WSLConfigurationAdvancedView',
+ 'SummaryView',
+]
diff --git a/system_setup/ui/views/identity.py b/system_setup/ui/views/identity.py
new file mode 100644
index 00000000..4e5788f6
--- /dev/null
+++ b/system_setup/ui/views/identity.py
@@ -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 .
+
+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),
+ ))
diff --git a/system_setup/ui/views/summary.py b/system_setup/ui/views/summary.py
new file mode 100644
index 00000000..a65e4019
--- /dev/null
+++ b/system_setup/ui/views/summary.py
@@ -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()
diff --git a/system_setup/ui/views/wslconfadvanced.py b/system_setup/ui/views/wslconfadvanced.py
new file mode 100644
index 00000000..6dc51f27
--- /dev/null
+++ b/system_setup/ui/views/wslconfadvanced.py
@@ -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,
+ ))
diff --git a/system_setup/ui/views/wslconfbase.py b/system_setup/ui/views/wslconfbase.py
new file mode 100644
index 00000000..96086d1b
--- /dev/null
+++ b/system_setup/ui/views/wslconfbase.py
@@ -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
+ ))