diff --git a/Makefile b/Makefile index c18c726d..f1e3a55f 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ 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) @@ -55,6 +56,12 @@ dryrun-system-setup: 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/system_setup/client/client.py b/system_setup/client/client.py index c683e7db..fd488d73 100644 --- a/system_setup/client/client.py +++ b/system_setup/client/client.py @@ -24,6 +24,8 @@ log = logging.getLogger('system_setup.client.client') class SystemSetupClient(SubiquityClient): + from system_setup.client import controllers as controllers_mod + snapd_socket_path = None controllers = [ @@ -34,8 +36,16 @@ class SystemSetupClient(SubiquityClient): "Overview", "Progress", ] + def __init__(self, opts): + if opts.reconfigure: + self.controllers = [ + "Welcome", + "Reconfiguration", + "Progress", + ] + super().__init__(opts) - from system_setup.client import controllers as controllers_mod + def restart(self, remove_last_screen=True, restart_server=False): log.debug(f"restart {remove_last_screen} {restart_server}") diff --git a/system_setup/client/controllers/__init__.py b/system_setup/client/controllers/__init__.py index 2636dbf6..ad2307ab 100644 --- a/system_setup/client/controllers/__init__.py +++ b/system_setup/client/controllers/__init__.py @@ -17,6 +17,7 @@ from .identity import WSLIdentityController from .integration import IntegrationController from .overview import OverviewController +from .reconfiguration import ReconfigurationController from subiquity.client.controllers import (ProgressController, WelcomeController) @@ -27,5 +28,6 @@ __all__ = [ 'ProgressController', 'IntegrationController', 'OverviewController', + 'ReconfigurationController', ] diff --git a/system_setup/client/controllers/reconfiguration.py b/system_setup/client/controllers/reconfiguration.py new file mode 100644 index 00000000..8631645e --- /dev/null +++ b/system_setup/client/controllers/reconfiguration.py @@ -0,0 +1,64 @@ +# 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.client.controller import SubiquityTuiController +from subiquity.common.types import WSLConfiguration2Data +from system_setup.ui.views.reconfiguration import ReconfigurationView + +log = logging.getLogger('system_setup.client.controllers.reconfiguration') + + +class ReconfigurationController(SubiquityTuiController): + endpoint_name = 'wslconf2' + + async def make_ui(self): + data = await self.endpoint.GET() + return ReconfigurationView(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 = WSLConfiguration2Data( + 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( + "ConfigurationController.done next_screen user_spec=%s", + reconf_data) + self.app.next_screen(self.endpoint.POST(reconf_data)) + + def cancel(self): + self.app.prev_screen() \ No newline at end of file diff --git a/system_setup/cmd/server.py b/system_setup/cmd/server.py index b659186f..09d4a5b3 100644 --- a/system_setup/cmd/server.py +++ b/system_setup/cmd/server.py @@ -35,6 +35,7 @@ def make_server_args_parser(): help='menu-only, do not call installer function') parser.add_argument('--socket') parser.add_argument('--autoinstall', action='store') + parser.add_argument('--reconfigure', action='store_true') return parser diff --git a/system_setup/cmd/tui.py b/system_setup/cmd/tui.py index 9be6d7cf..0b143181 100755 --- a/system_setup/cmd/tui.py +++ b/system_setup/cmd/tui.py @@ -70,6 +70,7 @@ def make_client_args_parser(): help='Synthesize a click on a button matching PAT') parser.add_argument('--answers') parser.add_argument('--server-pid') + parser.add_argument('--reconfigure', action='store_true') return parser @@ -90,6 +91,8 @@ def main(): sock_path = '.subiquity/socket' opts.socket = sock_path server_args = ['--dry-run', '--socket=' + sock_path] + unknown + if '--reconfigure' in args: + server_args.append('--reconfigure') server_parser = make_server_args_parser() server_parser.parse_args(server_args) # just to check server_output = open('.subiquity/server-output', 'w') diff --git a/system_setup/models/wslconf2.py b/system_setup/models/wslconf2.py index ed248cda..5efee78f 100644 --- a/system_setup/models/wslconf2.py +++ b/system_setup/models/wslconf2.py @@ -50,6 +50,7 @@ class WSLConfiguration2Model(object): 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 @@ -70,6 +71,7 @@ class WSLConfiguration2Model(object): 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) diff --git a/system_setup/server/controllers/wslconf2.py b/system_setup/server/controllers/wslconf2.py index f157e646..4b474c9e 100644 --- a/system_setup/server/controllers/wslconf2.py +++ b/system_setup/server/controllers/wslconf2.py @@ -16,7 +16,8 @@ import logging import attr - +from os import path +import configparser from subiquitycore.context import with_context from subiquity.common.apidef import API @@ -48,14 +49,92 @@ class WSLConfiguration2Controller(SubiquityController): '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: + yes_no_converter = lambda x: x == 'true' + reconf_data = WSLConfiguration2Data( + custom_path=data['custom_path'], + custom_mount_opt=data['custom_mount_opt'], + gen_host=yes_no_converter(data['gen_host']), + gen_resolvconf=yes_no_converter(data['gen_resolvconf']), + interop_enabled=yes_no_converter(data['interop_enabled']), + interop_appendwindowspath=yes_no_converter(data['interop_appendwindowspath']), + gui_theme=data['gui_theme'], + gui_followwintheme=yes_no_converter(data['gui_followwintheme']), + legacy_gui=yes_no_converter(data['legacy_gui']), + legacy_audio=yes_no_converter(data['legacy_audio']), + adv_ip_detect=yes_no_converter(data['adv_ip_detect']), + wsl_motd_news=yes_no_converter(data['wsl_motd_news']), + automount=yes_no_converter(data['automount']), + mountfstab=yes_no_converter(data['mountfstab']), + ) + self.model.apply_settings(reconf_data, self.opts.dry_run) def load_autoinstall_data(self, data): if data is not None: - identity_data = WSLConfiguration2Data( + reconf_data = WSLConfiguration2Data( custom_path=data['custom_path'], custom_mount_opt=data['custom_mount_opt'], gen_host=data['gen_host'], @@ -71,7 +150,7 @@ class WSLConfiguration2Controller(SubiquityController): automount=data['automount'], mountfstab=data['mountfstab'] ) - self.model.apply_settings(identity_data, self.opts.dry_run) + self.model.apply_settings(reconf_data, self.opts.dry_run) @with_context() async def apply_autoinstall_config(self, context=None): diff --git a/system_setup/server/server.py b/system_setup/server/server.py index 5736224a..6d6486d2 100644 --- a/system_setup/server/server.py +++ b/system_setup/server/server.py @@ -28,8 +28,17 @@ class SystemSetupServer(SubiquityServer): "Locale", "Identity", "WSLConfiguration1", - "WSLConfiguration2" - ] + ] + + def __init__(self, opts, block_log_dir): + if opts.reconfigure: + self.controllers = [ + "Reporting", + "Error", + "Locale", + "WSLConfiguration2", + ] + super().__init__(opts, block_log_dir) def make_model(self): root = '/' diff --git a/system_setup/ui/views/__init__.py b/system_setup/ui/views/__init__.py index 7aff96f6..8ec74019 100644 --- a/system_setup/ui/views/__init__.py +++ b/system_setup/ui/views/__init__.py @@ -16,9 +16,11 @@ from .identity import WSLIdentityView from .integration import IntegrationView from .overview import OverviewView +from .reconfiguration import ReconfigurationView __all__ = [ 'WSLIdentityView', 'IntegrationView', 'OverviewView', + 'ReconfigurationView', ] \ No newline at end of file diff --git a/system_setup/ui/views/reconfiguration.py b/system_setup/ui/views/reconfiguration.py new file mode 100644 index 00000000..8a834c53 --- /dev/null +++ b/system_setup/ui/views/reconfiguration.py @@ -0,0 +1,151 @@ +""" Reconfiguration View + +Integration provides user with options to set up integration configurations. + +""" +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 WSLConfiguration2Data + + +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 ReconfigurationForm(Form): + def __init__(self, initial): + super().__init__(initial=initial) + + #TODO: placholder settings UI; should be dynamically generated using ubuntu-wsl-integration + 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 ReconfigurationView(BaseView): + title = _("Configuration") + excerpt = _("In this page, you can tweak Ubuntu WSL to your needs. \n" + ) + + def __init__(self, controller, integration_data): + self.controller = controller + + initial = { + 'custom_path': integration_data.custom_path, + 'custom_mount_opt':integration_data.custom_mount_opt, + 'gen_host': integration_data.gen_host, + 'gen_resolvconf': integration_data.gen_resolvconf, + 'interop_enabled': integration_data.interop_enabled, + 'interop_appendwindowspath': integration_data.interop_appendwindowspath, + 'gui_theme': integration_data.gui_theme, + 'gui_followwintheme': integration_data.gui_followwintheme, + 'legacy_gui': integration_data.legacy_gui, + 'legacy_audio': integration_data.legacy_audio, + 'adv_ip_detect': integration_data.adv_ip_detect, + 'wsl_motd_news': integration_data.wsl_motd_news, + 'automount': integration_data.automount, + 'mountfstab': integration_data.mountfstab, + } + self.form = ReconfigurationForm(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(WSLConfiguration2Data( + 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, + ))