Reconfigure mode: WIP

This commit is contained in:
Jinming Wu, Patrick 2021-07-23 00:49:12 +08:00 committed by Jean-Baptiste Lallement
parent 6cbe3e8a36
commit a97cc28ae6
11 changed files with 337 additions and 7 deletions

View File

@ -8,6 +8,7 @@ PROBERTDIR=./probert
PROBERT_REPO=https://github.com/canonical/probert PROBERT_REPO=https://github.com/canonical/probert
DRYRUN?=--dry-run --bootloader uefi --machine-config examples/simple.json DRYRUN?=--dry-run --bootloader uefi --machine-config examples/simple.json
SYSTEM_SETUP_DRYRUN?=--dry-run SYSTEM_SETUP_DRYRUN?=--dry-run
RECONFIG?=--reconfigure
export PYTHONPATH export PYTHONPATH
CWD := $(shell pwd) CWD := $(shell pwd)
@ -55,6 +56,12 @@ dryrun-system-setup:
dryrun-system-setup-server: dryrun-system-setup-server:
$(PYTHON) -m system_setup.cmd.server $(SYSTEM_SETUP_DRYRUN) $(PYTHON) -m system_setup.cmd.server $(SYSTEM_SETUP_DRYRUN)
dryrun-system-setup-recon:
$(PYTHON) -m system_setup.cmd.tui $(SYSTEM_SETUP_DRYRUN) $(RECONFIG)
dryrun-system-setup-server-recon:
$(PYTHON) -m system_setup.cmd.server $(SYSTEM_SETUP_DRYRUN) $(RECONFIG)
lint: flake8 lint: flake8
flake8: flake8:

View File

@ -24,6 +24,8 @@ log = logging.getLogger('system_setup.client.client')
class SystemSetupClient(SubiquityClient): class SystemSetupClient(SubiquityClient):
from system_setup.client import controllers as controllers_mod
snapd_socket_path = None snapd_socket_path = None
controllers = [ controllers = [
@ -34,8 +36,16 @@ class SystemSetupClient(SubiquityClient):
"Overview", "Overview",
"Progress", "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): def restart(self, remove_last_screen=True, restart_server=False):
log.debug(f"restart {remove_last_screen} {restart_server}") log.debug(f"restart {remove_last_screen} {restart_server}")

View File

@ -17,6 +17,7 @@
from .identity import WSLIdentityController from .identity import WSLIdentityController
from .integration import IntegrationController from .integration import IntegrationController
from .overview import OverviewController from .overview import OverviewController
from .reconfiguration import ReconfigurationController
from subiquity.client.controllers import (ProgressController, WelcomeController) from subiquity.client.controllers import (ProgressController, WelcomeController)
@ -27,5 +28,6 @@ __all__ = [
'ProgressController', 'ProgressController',
'IntegrationController', 'IntegrationController',
'OverviewController', 'OverviewController',
'ReconfigurationController',
] ]

View File

@ -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 <http://www.gnu.org/licenses/>.
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()

View File

@ -35,6 +35,7 @@ def make_server_args_parser():
help='menu-only, do not call installer function') help='menu-only, do not call installer function')
parser.add_argument('--socket') parser.add_argument('--socket')
parser.add_argument('--autoinstall', action='store') parser.add_argument('--autoinstall', action='store')
parser.add_argument('--reconfigure', action='store_true')
return parser return parser

View File

@ -70,6 +70,7 @@ def make_client_args_parser():
help='Synthesize a click on a button matching PAT') help='Synthesize a click on a button matching PAT')
parser.add_argument('--answers') parser.add_argument('--answers')
parser.add_argument('--server-pid') parser.add_argument('--server-pid')
parser.add_argument('--reconfigure', action='store_true')
return parser return parser
@ -90,6 +91,8 @@ def main():
sock_path = '.subiquity/socket' sock_path = '.subiquity/socket'
opts.socket = sock_path opts.socket = sock_path
server_args = ['--dry-run', '--socket=' + sock_path] + unknown server_args = ['--dry-run', '--socket=' + sock_path] + unknown
if '--reconfigure' in args:
server_args.append('--reconfigure')
server_parser = make_server_args_parser() server_parser = make_server_args_parser()
server_parser.parse_args(server_args) # just to check server_parser.parse_args(server_args) # just to check
server_output = open('.subiquity/server-output', 'w') server_output = open('.subiquity/server-output', 'w')

View File

@ -50,6 +50,7 @@ class WSLConfiguration2Model(object):
def apply_settings(self, result, is_dry_run=False): def apply_settings(self, result, is_dry_run=False):
d = {} d = {}
#TODO: placholder settings; should be dynamically assgined using ubuntu-wsl-integration
d['custom_path'] = result.custom_path d['custom_path'] = result.custom_path
d['custom_mount_opt'] = result.custom_mount_opt d['custom_mount_opt'] = result.custom_mount_opt
d['gen_host'] = result.gen_host d['gen_host'] = result.gen_host
@ -70,6 +71,7 @@ class WSLConfiguration2Model(object):
run_command(["/usr/bin/ubuntuwsl", "reset", "-y"], run_command(["/usr/bin/ubuntuwsl", "reset", "-y"],
stdout=subprocess.DEVNULL) stdout=subprocess.DEVNULL)
# set the settings # set the settings
#TODO: placholder settings; should be dynamically generated using ubuntu-wsl-integration
run_command(["/usr/bin/ubuntuwsl", "update", run_command(["/usr/bin/ubuntuwsl", "update",
"WSL.automount.enabled", result.automount], "WSL.automount.enabled", result.automount],
stdout=subprocess.DEVNULL) stdout=subprocess.DEVNULL)

View File

@ -16,7 +16,8 @@
import logging import logging
import attr import attr
from os import path
import configparser
from subiquitycore.context import with_context from subiquitycore.context import with_context
from subiquity.common.apidef import API from subiquity.common.apidef import API
@ -53,9 +54,87 @@ class WSLConfiguration2Controller(SubiquityController):
'additionalProperties': False, '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): def load_autoinstall_data(self, data):
if data is not None: if data is not None:
identity_data = WSLConfiguration2Data( reconf_data = WSLConfiguration2Data(
custom_path=data['custom_path'], custom_path=data['custom_path'],
custom_mount_opt=data['custom_mount_opt'], custom_mount_opt=data['custom_mount_opt'],
gen_host=data['gen_host'], gen_host=data['gen_host'],
@ -71,7 +150,7 @@ class WSLConfiguration2Controller(SubiquityController):
automount=data['automount'], automount=data['automount'],
mountfstab=data['mountfstab'] 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() @with_context()
async def apply_autoinstall_config(self, context=None): async def apply_autoinstall_config(self, context=None):

View File

@ -28,9 +28,18 @@ class SystemSetupServer(SubiquityServer):
"Locale", "Locale",
"Identity", "Identity",
"WSLConfiguration1", "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): def make_model(self):
root = '/' root = '/'
if self.opts.dry_run: if self.opts.dry_run:

View File

@ -16,9 +16,11 @@
from .identity import WSLIdentityView from .identity import WSLIdentityView
from .integration import IntegrationView from .integration import IntegrationView
from .overview import OverviewView from .overview import OverviewView
from .reconfiguration import ReconfigurationView
__all__ = [ __all__ = [
'WSLIdentityView', 'WSLIdentityView',
'IntegrationView', 'IntegrationView',
'OverviewView', 'OverviewView',
'ReconfigurationView',
] ]

View File

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