From d75bcbda04bc113c939a8aaab795c1a885d76e83 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Wed, 25 Aug 2021 10:17:38 +0800 Subject: [PATCH 01/69] Include definitions and utils 1. Inlcude api definitions and types; 2. Inlcude is_wsl utility. --- subiquity/common/apidef.py | 4 ++++ subiquity/common/types.py | 30 ++++++++++++++++++++++++++++++ subiquitycore/utils.py | 6 ++++++ 3 files changed, 40 insertions(+) diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py index 3bacac94..e9f1fd4d 100644 --- a/subiquity/common/apidef.py +++ b/subiquity/common/apidef.py @@ -47,6 +47,8 @@ from subiquity.common.types import ( TimeZoneInfo, WLANSupportInstallState, ZdevInfo, + WSLConfiguration1Data, + WSLConfiguration2Data, ) @@ -59,6 +61,8 @@ class API: proxy = simple_endpoint(str) ssh = simple_endpoint(SSHData) updates = simple_endpoint(str) + wslconf1 = simple_endpoint(WSLConfiguration1Data) + wslconf2 = simple_endpoint(WSLConfiguration2Data) class meta: class status: diff --git a/subiquity/common/types.py b/subiquity/common/types.py index 8461b11d..4c396d22 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -335,3 +335,33 @@ class TimeZoneInfo: class ShutdownMode(enum.Enum): REBOOT = enum.auto() POWEROFF = enum.auto() + +class WSLConfiguration1Data: + 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) + + +class LookType(enum.Enum): + Default = "default" + Light = "light" + Dark = "dark" + + +@attr.s(auto_attribs=True) +class WSLConfiguration2Data: + gui_theme: LookType = attr.ib(default=LookType.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) + 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/subiquitycore/utils.py b/subiquitycore/utils.py index aacc80fd..ab17df08 100644 --- a/subiquitycore/utils.py +++ b/subiquitycore/utils.py @@ -19,6 +19,7 @@ import logging import os import random import subprocess +import pathlib log = logging.getLogger("subiquitycore.utils") @@ -143,3 +144,8 @@ def disable_subiquity(): "snap.subiquity.subiquity-service.service", "serial-subiquity@*.service"]) return + +"""""" +def is_wsl(): + """ Returns True if we are on a WSL system """ + return pathlib.Path("/proc/sys/fs/binfmt_misc/WSLInterop").is_file() \ No newline at end of file From b9f6ed2c81939c000f0ec648adde1d59f1fdfded Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Fri, 2 Jul 2021 16:25:11 +0800 Subject: [PATCH 02/69] subiquity/server/server.py: provide no_snapd support --- subiquity/server/server.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/subiquity/server/server.py b/subiquity/server/server.py index c4651e94..e00fde86 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -263,9 +263,15 @@ 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.controllers.remove("Refresh") + self.controllers.remove("SnapList") + self.snapd = None self.note_data_for_apport("SnapUpdated", str(self.updated)) self.event_listeners = [] self.autoinstall_config = None @@ -567,14 +573,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 = [ From 339e66a2c3ed80e620048f65b06be290bb4dce8b Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Fri, 2 Jul 2021 16:26:21 +0800 Subject: [PATCH 03/69] system_setup: initial setup --- system_setup/__init__.py | 23 +++ system_setup/__main__.py | 5 + system_setup/cmd/__init__.py | 0 system_setup/cmd/server.py | 88 ++++++++++++ system_setup/cmd/tui.py | 146 ++++++++++++++++++++ system_setup/models/__init__.py | 0 system_setup/models/system_server.py | 101 ++++++++++++++ system_setup/models/wslconf1.py | 71 ++++++++++ system_setup/server/__init__.py | 0 system_setup/server/controllers/__init__.py | 17 +++ system_setup/server/controllers/wslconf1.py | 75 ++++++++++ system_setup/server/server.py | 37 +++++ 12 files changed, 563 insertions(+) create mode 100644 system_setup/__init__.py create mode 100644 system_setup/__main__.py create mode 100644 system_setup/cmd/__init__.py create mode 100644 system_setup/cmd/server.py create mode 100644 system_setup/cmd/tui.py create mode 100644 system_setup/models/__init__.py create mode 100644 system_setup/models/system_server.py create mode 100644 system_setup/models/wslconf1.py create mode 100644 system_setup/server/__init__.py create mode 100644 system_setup/server/controllers/__init__.py create mode 100644 system_setup/server/controllers/wslconf1.py create mode 100644 system_setup/server/server.py 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..f7a09694 --- /dev/null +++ b/system_setup/__main__.py @@ -0,0 +1,5 @@ +import sys + +if __name__ == '__main__': + from system_setup.cmd.tui import main + sys.exit(main()) diff --git a/system_setup/cmd/__init__.py b/system_setup/cmd/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/system_setup/cmd/server.py b/system_setup/cmd/server.py new file mode 100644 index 00000000..b659186f --- /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') + 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 100644 index 00000000..52805988 --- /dev/null +++ b/system_setup/cmd/tui.py @@ -0,0 +1,146 @@ +#!/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.common import ( + LOGDIR, + setup_environment, + ) +from .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(): + 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') + 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') + 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 subiquity.client.client import SubiquityClient + parser = make_client_args_parser() + args = sys.argv[1:] + 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', 'subiquity.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='subiquity-client') + + logger = logging.getLogger('subiquity') + version = os.environ.get("SNAP_REVISION", "unknown") + logger.info("Starting Subiquity 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 = SubiquityClient(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..e69de29b diff --git a/system_setup/models/system_server.py b/system_setup/models/system_server.py new file mode 100644 index 00000000..c16469fb --- /dev/null +++ b/system_setup/models/system_server.py @@ -0,0 +1,101 @@ +# 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 subiquitycore.utils import run_command, is_wsl + + +from subiquity.models.locale import LocaleModel +from subiquity.models.identity import IdentityModel +from .wslconf1 import WSLConfiguration1Model + + +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 +""" + +# Models that will be used in WSL system setup +ALL_MODEL_NAMES = [ + "identity", + "locale", + "wslconf1", +] + + +class SystemSetupModel(SubiquityModel): + """The overall model for subiquity.""" + + target = '/' + + def __init__(self, root): + # Parent class init is not called to not load models we don't need. + self.root = root + self.is_wsl = is_wsl() + + self.debconf_selections = None + self.filesystem = None + self.kernel = None + self.keyboard = None + self.mirror = None + self.network = None + self.proxy = None + self.snaplist = None + self.ssh = None + self.updates = None + + self.packages = [] + self.userdata = {} + self.locale = LocaleModel() + self.identity = IdentityModel() + self.wslconf1 = WSLConfiguration1Model() + + self.confirmation = asyncio.Event() + + self._events = { + name: asyncio.Event() for name in ALL_MODEL_NAMES + } + self.postinstall_events = { + self._events[name] for name in ALL_MODEL_NAMES + } + + def configured(self, model_name): + # We need to override the parent class as *_MODEL_NAMES are global variables + # in server.py + if model_name not in ALL_MODEL_NAMES: + return + self._events[model_name].set() + stage = 'wslinstall' + unconfigured = { + mn for mn in ALL_MODEL_NAMES + if not self._events[mn].is_set() + } + log.debug( + "model %s for %s is configured, to go %s", + model_name, stage, unconfigured) + diff --git a/system_setup/models/wslconf1.py b/system_setup/models/wslconf1.py new file mode 100644 index 00000000..78ed7709 --- /dev/null +++ b/system_setup/models/wslconf1.py @@ -0,0 +1,71 @@ +# 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_integration_1') + + +@attr.s +class WSLConfiguration1(object): + custom_path = attr.ib() + custom_mount_opt = attr.ib() + gen_host = attr.ib() + gen_resolvconf = attr.ib() + + +class WSLConfiguration1Model(object): + """ Model representing integration + """ + + def __init__(self): + self._wslconf1 = None + + 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._wslconf1 = WSLConfiguration1(**d) + 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 wslconf1(self): + return self._wslconf1 + + def __repr__(self): + return "".format(self.wslconf1) diff --git a/system_setup/server/__init__.py b/system_setup/server/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/system_setup/server/controllers/__init__.py b/system_setup/server/controllers/__init__.py new file mode 100644 index 00000000..0e51296a --- /dev/null +++ b/system_setup/server/controllers/__init__.py @@ -0,0 +1,17 @@ +from subiquity.server.controllers.cmdlist import EarlyController, LateController, ErrorController +from subiquity.server.controllers.identity import IdentityController +from subiquity.server.controllers.locale import LocaleController +from subiquity.server.controllers.reporting import ReportingController +from subiquity.server.controllers.userdata import UserdataController +from .wslconf1 import WSLConfiguration1Controller + +__all__ = [ + 'EarlyController', + 'ErrorController', + 'IdentityController', + 'LateController', + 'LocaleController', + 'ReportingController', + 'UserdataController', + "WSLConfiguration1Controller", +] \ No newline at end of file diff --git a/system_setup/server/controllers/wslconf1.py b/system_setup/server/controllers/wslconf1.py new file mode 100644 index 00000000..4529cbcf --- /dev/null +++ b/system_setup/server/controllers/wslconf1.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 WSLConfiguration1Data +from subiquity.server.controller import SubiquityController + +log = logging.getLogger('subiquity.server.controllers.wsl_integration_1') + + +class WSLConfiguration1Controller(SubiquityController): + + endpoint = API.wslconf1 + + autoinstall_key = model_name = "wslconf1" + 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 = WSLConfiguration1Data( + 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.wslconf1) + return r + + async def GET(self) -> WSLConfiguration1Data: + data = WSLConfiguration1Data() + if self.model.wslconf1 is not None: + data.custom_path = self.model.wslconf1.custom_path + data.custom_mount_opt = self.model.wslconf1.custom_mount_opt + data.gen_host = self.model.wslconf1.gen_host + data.gen_resolvconf = self.model.wslconf1.gen_resolvconf + return data + + async def POST(self, data: WSLConfiguration1Data): + 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..37e9ba56 --- /dev/null +++ b/system_setup/server/server.py @@ -0,0 +1,37 @@ +# 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_server import SystemSetupModel +import os + + +class SystemSetupServer(SubiquityServer): + + from system_setup.server import controllers as controllers_mod + controllers = [ + "Reporting", + "Error", + "Userdata", + "Locale", + "Identity", + "WSLConfiguration1" + ] + + def make_model(self): + root = '/' + if self.opts.dry_run: + root = os.path.abspath('.subiquity') + return SystemSetupModel(root) From 91fb547fe87f46fd411c1f38101abae0c940b96c Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Tue, 6 Jul 2021 15:26:37 +0800 Subject: [PATCH 04/69] Makefile & setup.py: include corr. build cmds --- Makefile | 6 ++++++ setup.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/Makefile b/Makefile index 55db49d3..4215ee6d 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,12 @@ dryrun-serial ui-view-serial: dryrun-server: $(PYTHON) -m subiquity.cmd.server $(DRYRUN) +dryrun-system-setup: + $(PYTHON) -m system_setup.cmd.tui $(DRYRUN) + +dryrun-system-setup-server: + $(PYTHON) -m system_setup.cmd.server $(DRYRUN) + lint: flake8 flake8: 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'), ], From 653d015d2db83cac72325dd61fb06f874fd97a94 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Wed, 7 Jul 2021 21:35:48 +0800 Subject: [PATCH 05/69] subiquity/server/server.py: NOPROBER setup --- subiquity/server/server.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/subiquity/server/server.py b/subiquity/server/server.py index e00fde86..9059aa1f 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') @@ -253,7 +254,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( From 486e05eeb10ae23746d4e3d1d79e5f07b93ea322 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Wed, 7 Jul 2021 21:38:43 +0800 Subject: [PATCH 06/69] Makefile: System_setup specific drtrun param --- Makefile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 4215ee6d..c18c726d 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ 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 export PYTHONPATH CWD := $(shell pwd) @@ -49,10 +50,10 @@ dryrun-server: $(PYTHON) -m subiquity.cmd.server $(DRYRUN) dryrun-system-setup: - $(PYTHON) -m system_setup.cmd.tui $(DRYRUN) + $(PYTHON) -m system_setup.cmd.tui $(SYSTEM_SETUP_DRYRUN) dryrun-system-setup-server: - $(PYTHON) -m system_setup.cmd.server $(DRYRUN) + $(PYTHON) -m system_setup.cmd.server $(SYSTEM_SETUP_DRYRUN) lint: flake8 From 57ee7cd8e03fc87c83fe43bb8d01a5e1844075a5 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Wed, 7 Jul 2021 21:39:32 +0800 Subject: [PATCH 07/69] .gitignore: ignore .vscode --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) 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 From ef2be951b247832b47dace257a9dae636ae035f9 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Tue, 13 Jul 2021 14:15:09 +0800 Subject: [PATCH 08/69] WSLConfiguration2 API complete --- subiquity/common/types.py | 8 +- system_setup/models/system_server.py | 3 + system_setup/models/wslconf2.py | 125 ++++++++++++++++++++ system_setup/server/controllers/__init__.py | 2 + system_setup/server/controllers/wslconf2.py | 105 ++++++++++++++++ system_setup/server/server.py | 3 +- 6 files changed, 238 insertions(+), 8 deletions(-) create mode 100644 system_setup/models/wslconf2.py create mode 100644 system_setup/server/controllers/wslconf2.py diff --git a/subiquity/common/types.py b/subiquity/common/types.py index 4c396d22..271fb65c 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -343,15 +343,9 @@ class WSLConfiguration1Data: gen_resolvconf: bool = attr.ib(default=True) -class LookType(enum.Enum): - Default = "default" - Light = "light" - Dark = "dark" - - @attr.s(auto_attribs=True) class WSLConfiguration2Data: - gui_theme: LookType = attr.ib(default=LookType.Default) + 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) diff --git a/system_setup/models/system_server.py b/system_setup/models/system_server.py index c16469fb..8e956d93 100644 --- a/system_setup/models/system_server.py +++ b/system_setup/models/system_server.py @@ -24,6 +24,7 @@ from subiquitycore.utils import run_command, is_wsl from subiquity.models.locale import LocaleModel from subiquity.models.identity import IdentityModel from .wslconf1 import WSLConfiguration1Model +from .wslconf2 import WSLConfiguration2Model log = logging.getLogger('system_setup.models.system_server') @@ -45,6 +46,7 @@ ALL_MODEL_NAMES = [ "identity", "locale", "wslconf1", + "wslconf2", ] @@ -74,6 +76,7 @@ class SystemSetupModel(SubiquityModel): self.locale = LocaleModel() self.identity = IdentityModel() self.wslconf1 = WSLConfiguration1Model() + self.wslconf2 = WSLConfiguration2Model() self.confirmation = asyncio.Event() diff --git a/system_setup/models/wslconf2.py b/system_setup/models/wslconf2.py new file mode 100644 index 00000000..ed248cda --- /dev/null +++ b/system_setup/models/wslconf2.py @@ -0,0 +1,125 @@ +# 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 +import json + +from subiquitycore.utils import run_command + +log = logging.getLogger('subiquity.models.wsl_integration_2') + + +@attr.s +class WSLConfiguration2(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 WSLConfiguration2Model(object): + """ Model representing integration + """ + + def __init__(self): + self._wslconf2 = None + + 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 + 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._wslconf2 = WSLConfiguration2(**d) + 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.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 wslconf2(self): + return self._wslconf2 + + def __repr__(self): + return "".format(self.wslconf2) diff --git a/system_setup/server/controllers/__init__.py b/system_setup/server/controllers/__init__.py index 0e51296a..39bd1fde 100644 --- a/system_setup/server/controllers/__init__.py +++ b/system_setup/server/controllers/__init__.py @@ -4,6 +4,7 @@ from subiquity.server.controllers.locale import LocaleController from subiquity.server.controllers.reporting import ReportingController from subiquity.server.controllers.userdata import UserdataController from .wslconf1 import WSLConfiguration1Controller +from .wslconf2 import WSLConfiguration2Controller __all__ = [ 'EarlyController', @@ -14,4 +15,5 @@ __all__ = [ 'ReportingController', 'UserdataController', "WSLConfiguration1Controller", + "WSLConfiguration2Controller", ] \ No newline at end of file diff --git a/system_setup/server/controllers/wslconf2.py b/system_setup/server/controllers/wslconf2.py new file mode 100644 index 00000000..f157e646 --- /dev/null +++ b/system_setup/server/controllers/wslconf2.py @@ -0,0 +1,105 @@ +# 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 WSLConfiguration2Data +from subiquity.server.controller import SubiquityController + +log = logging.getLogger('subiquity.server.controllers.wsl_integration_2') + + +class WSLConfiguration2Controller(SubiquityController): + + endpoint = API.wslconf2 + + autoinstall_key = model_name = "wslconf2" + 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, + } + + def load_autoinstall_data(self, data): + if data is not None: + identity_data = WSLConfiguration2Data( + 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(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.wslconf2) + return r + + async def GET(self) -> WSLConfiguration2Data: + data = WSLConfiguration2Data() + if self.model.wslconf2 is not None: + data.custom_path = self.model.wslconf2.custom_path + data.custom_mount_opt = self.model.wslconf2.custom_mount_opt + data.gen_host = self.model.wslconf2.gen_host + data.gen_resolvconf = self.model.wslconf2.gen_resolvconf + data.interop_enabled = self.model.wslconf2.interop_enabled + data.interop_appendwindowspath = self.model.wslconf2.interop_appendwindowspath + data.gui_theme = self.model.wslconf2.gui_theme + data.gui_followwintheme = self.model.wslconf2.gui_followwintheme + data.legacy_gui = self.model.wslconf2.legacy_gui + data.legacy_audio = self.model.wslconf2.legacy_audio + data.adv_ip_detect = self.model.wslconf2.adv_ip_detect + data.wsl_motd_news = self.model.wslconf2.wsl_motd_news + data.automount = self.model.wslconf2.automount + data.mountfstab = self.model.wslconf2.mountfstab + return data + + async def POST(self, data: WSLConfiguration2Data): + 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 index 37e9ba56..5736224a 100644 --- a/system_setup/server/server.py +++ b/system_setup/server/server.py @@ -27,7 +27,8 @@ class SystemSetupServer(SubiquityServer): "Userdata", "Locale", "Identity", - "WSLConfiguration1" + "WSLConfiguration1", + "WSLConfiguration2" ] def make_model(self): From 84774c5dc42152af5d09e40deec0162c97fcc8de Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Tue, 13 Jul 2021 17:18:44 +0800 Subject: [PATCH 09/69] WSL User API reimplementation --- system_setup/server/controllers/__init__.py | 2 +- system_setup/server/controllers/identity.py | 63 +++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 system_setup/server/controllers/identity.py diff --git a/system_setup/server/controllers/__init__.py b/system_setup/server/controllers/__init__.py index 39bd1fde..e3e6f184 100644 --- a/system_setup/server/controllers/__init__.py +++ b/system_setup/server/controllers/__init__.py @@ -1,8 +1,8 @@ from subiquity.server.controllers.cmdlist import EarlyController, LateController, ErrorController -from subiquity.server.controllers.identity import IdentityController 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 .wslconf1 import WSLConfiguration1Controller from .wslconf2 import WSLConfiguration2Controller diff --git a/system_setup/server/controllers/identity.py b/system_setup/server/controllers/identity.py new file mode 100644 index 00000000..a6769b0c --- /dev/null +++ b/system_setup/server/controllers/identity.py @@ -0,0 +1,63 @@ +# 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 subiquitycore.context import with_context + +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'}, + 'hostname': {'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 From 73e1ab79011c8e8b2b0eec248de403158d746261 Mon Sep 17 00:00:00 2001 From: Didier Roche Date: Tue, 13 Jul 2021 14:24:06 +0200 Subject: [PATCH 10/69] Make network optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As we don’t have a network object in WSL and inherit from the base class, ensure it can be None. --- subiquity/server/server.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/subiquity/server/server.py b/subiquity/server/server.py index 9059aa1f..26c58b7f 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -119,8 +119,9 @@ class MetaController: 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 From d8e0026df5888af8e7bdf4a6ae5c96c2b5e4d787 Mon Sep 17 00:00:00 2001 From: Didier Roche Date: Tue, 13 Jul 2021 14:35:40 +0200 Subject: [PATCH 11/69] WSL identity controller and view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This controller and view inherits from the subiquity identity, but remove the "hostname" field. We are using composition over inheritance for identityController as the UI builder is basing on the order of declared elements as class variable and we can’t override that in the metaclass when inheriting. --- system_setup/client/__init__.py | 14 ++++ system_setup/client/controllers/identity.py | 38 ++++++++++ system_setup/ui/__init__.py | 14 ++++ system_setup/ui/views/__init__.py | 20 +++++ system_setup/ui/views/identity.py | 82 +++++++++++++++++++++ 5 files changed, 168 insertions(+) create mode 100644 system_setup/client/__init__.py create mode 100644 system_setup/client/controllers/identity.py create mode 100644 system_setup/ui/__init__.py create mode 100644 system_setup/ui/views/__init__.py create mode 100644 system_setup/ui/views/identity.py 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/controllers/identity.py b/system_setup/client/controllers/identity.py new file mode 100644 index 00000000..0680792a --- /dev/null +++ b/system_setup/client/controllers/identity.py @@ -0,0 +1,38 @@ +# 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.controllers import IdentityController +from subiquity.common.types import IdentityData +from system_setup.ui.views import WSLIdentityView + +log = logging.getLogger('system_setup.client.controllers.identity') + + +class WSLIdentityController(IdentityController): + + 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) 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..45ffae82 --- /dev/null +++ b/system_setup/ui/views/__init__.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 . + +from .identity import WSLIdentityView + +__all__ = [ + 'WSLIdentityView', +] \ No newline at end of file diff --git a/system_setup/ui/views/identity.py b/system_setup/ui/views/identity.py new file mode 100644 index 00000000..979b05ec --- /dev/null +++ b/system_setup/ui/views/identity.py @@ -0,0 +1,82 @@ +# 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 urwid import ( + connect_signal, + ) + + +from subiquity.common.types import IdentityData +from subiquity.ui.views.identity import IdentityForm, IdentityView, setup_password_validation +from subiquitycore.ui.form import Form +from subiquitycore.ui.utils import screen +from subiquitycore.utils import crypt_password +from subiquitycore.view import BaseView + + +class WSLIdentityForm(Form): + + realname = IdentityForm.realname + username = IdentityForm.username + password = IdentityForm.password + confirm_password = IdentityForm.confirm_password + + def __init__(self, initial): + self.identityForm = IdentityForm([], initial) + super().__init__(initial=initial) + + def validate_realname(self): + self.identityForm.validate_realname() + + def validate_username(self): + self.identityForm.validate_username() + + def validate_password(self): + self.identityForm.validate_password() + + def validate_confirm_password(self): + self.identityForm.validate_confirm_password() + + +class WSLIdentityView(BaseView): + title = IdentityView.title + excerpt = _("Enter the username and password you will use to log in.") + + def __init__(self, controller, identity_data): + self.controller = controller + + initial = { + 'realname': identity_data.realname, + 'username': identity_data.username, + } + + 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), + )) From 87002bddebb6aecd15ba4f07713642b1d124aa41 Mon Sep 17 00:00:00 2001 From: Didier Roche Date: Tue, 13 Jul 2021 14:39:44 +0200 Subject: [PATCH 12/69] SystemSetupClient to control system setup tui This class declares the controllers we want to use and consequently, which views in which orders we present to the user. Reuse the Welcome controller from subiquity by reimporting it in system_setup.client.controllers. Redefine the restart functionality to not depends on snap. --- system_setup/client/client.py | 66 +++++++++++++++++++++ system_setup/client/controllers/__init__.py | 27 +++++++++ system_setup/cmd/tui.py | 16 ++--- 3 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 system_setup/client/client.py create mode 100644 system_setup/client/controllers/__init__.py mode change 100644 => 100755 system_setup/cmd/tui.py diff --git a/system_setup/client/client.py b/system_setup/client/client.py new file mode 100644 index 00000000..907f1fb4 --- /dev/null +++ b/system_setup/client/client.py @@ -0,0 +1,66 @@ +# 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 os +import sys + +from subiquity.client.client import SubiquityClient + +log = logging.getLogger('system_setup.client.client') + + +class SystemSetupClient(SubiquityClient): + + snapd_socket_path = None + + controllers = [ + #"Serial", + "Welcome", + "WSLIdentity", + "Progress", + ] + + 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}") + if self.fg_proc is not None: + log.debug( + "killing foreground process %s before restarting", + self.fg_proc) + self.restarting = True + self.aio_loop.create_task( + self._kill_fg_proc(remove_last_screen, restart_server)) + return + if remove_last_screen: + self._remove_last_screen() + if restart_server: + self.restarting = True + self.ui.block_input = True + self.aio_loop.create_task(self._restart_server()) + return + if self.urwid_loop is not None: + self.urwid_loop.stop() + cmdline = sys.argv + if self.opts.dry_run: + cmdline = [ + sys.executable, '-m', 'system_setup.cmd.tui', + ] + sys.argv[1:] + ['--socket', self.opts.socket] + if self.opts.server_pid is not None: + cmdline.extend(['--server-pid', self.opts.server_pid]) + log.debug("restarting %r", cmdline) + + os.execvp(cmdline[0], cmdline) diff --git a/system_setup/client/controllers/__init__.py b/system_setup/client/controllers/__init__.py new file mode 100644 index 00000000..7c3efcc0 --- /dev/null +++ b/system_setup/client/controllers/__init__.py @@ -0,0 +1,27 @@ +# 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 subiquity.client.controllers import (ProgressController, WelcomeController) + + +__all__ = [ + 'WelcomeController', + 'WSLIdentityController', + 'ProgressController', +] + diff --git a/system_setup/cmd/tui.py b/system_setup/cmd/tui.py old mode 100644 new mode 100755 index 52805988..ae70b66e --- a/system_setup/cmd/tui.py +++ b/system_setup/cmd/tui.py @@ -23,11 +23,11 @@ import sys from subiquitycore.log import setup_logger -from subiquity.common import ( +from subiquity.cmd.common import ( LOGDIR, setup_environment, ) -from .server import make_server_args_parser +from system_setup.cmd.server import make_server_args_parser class ClickAction(argparse.Action): @@ -80,7 +80,7 @@ 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 subiquity.client.client import SubiquityClient + from system_setup.client.client import SystemSetupClient parser = make_client_args_parser() args = sys.argv[1:] if '--dry-run' in args: @@ -93,7 +93,7 @@ def main(): 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', 'subiquity.cmd.server'] + \ + server_cmd = [sys.executable, '-m', 'system_setup.cmd.server'] + \ server_args server_proc = subprocess.Popen( server_cmd, stdout=server_output, stderr=subprocess.STDOUT) @@ -111,11 +111,11 @@ def main(): logdir = LOGDIR if opts.dry_run: logdir = ".subiquity" - logfiles = setup_logger(dir=logdir, base='subiquity-client') + logfiles = setup_logger(dir=logdir, base='system_setup-client') logger = logging.getLogger('subiquity') - version = os.environ.get("SNAP_REVISION", "unknown") - logger.info("Starting Subiquity revision {}".format(version)) + 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): @@ -132,7 +132,7 @@ def main(): opts.answers.close() opts.answers = None - subiquity_interface = SubiquityClient(opts) + subiquity_interface = SystemSetupClient(opts) subiquity_interface.note_file_for_apport( "InstallerLog", logfiles['debug']) From f00d1d659ea5d9dde94e40dcddcd68da97b5eb37 Mon Sep 17 00:00:00 2001 From: Didier Roche Date: Tue, 13 Jul 2021 14:42:28 +0200 Subject: [PATCH 13/69] Add missing copyrights --- system_setup/__main__.py | 15 +++++++++++++++ system_setup/cmd/__init__.py | 14 ++++++++++++++ system_setup/models/__init__.py | 14 ++++++++++++++ system_setup/server/__init__.py | 14 ++++++++++++++ system_setup/server/controllers/__init__.py | 15 +++++++++++++++ 5 files changed, 72 insertions(+) diff --git a/system_setup/__main__.py b/system_setup/__main__.py index f7a09694..5b826b12 100644 --- a/system_setup/__main__.py +++ b/system_setup/__main__.py @@ -1,3 +1,18 @@ +# 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__': diff --git a/system_setup/cmd/__init__.py b/system_setup/cmd/__init__.py index e69de29b..8290406c 100644 --- a/system_setup/cmd/__init__.py +++ 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/models/__init__.py b/system_setup/models/__init__.py index e69de29b..8290406c 100644 --- a/system_setup/models/__init__.py +++ 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/server/__init__.py b/system_setup/server/__init__.py index e69de29b..8290406c 100644 --- a/system_setup/server/__init__.py +++ 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 index e3e6f184..d30820b7 100644 --- a/system_setup/server/controllers/__init__.py +++ b/system_setup/server/controllers/__init__.py @@ -1,3 +1,18 @@ +# 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 4672a1b772001fa706e5bd753dc07fba082aba97 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Thu, 15 Jul 2021 21:20:33 +0800 Subject: [PATCH 14/69] TUI: Intergration Page --- system_setup/client/client.py | 1 + system_setup/client/controllers/__init__.py | 2 + .../client/controllers/integration.py | 34 +++++ system_setup/ui/views/__init__.py | 2 + system_setup/ui/views/integration.py | 118 ++++++++++++++++++ 5 files changed, 157 insertions(+) create mode 100644 system_setup/client/controllers/integration.py create mode 100644 system_setup/ui/views/integration.py diff --git a/system_setup/client/client.py b/system_setup/client/client.py index 907f1fb4..54fb9c0a 100644 --- a/system_setup/client/client.py +++ b/system_setup/client/client.py @@ -30,6 +30,7 @@ class SystemSetupClient(SubiquityClient): #"Serial", "Welcome", "WSLIdentity", + "Integration", "Progress", ] diff --git a/system_setup/client/controllers/__init__.py b/system_setup/client/controllers/__init__.py index 7c3efcc0..1d7fc015 100644 --- a/system_setup/client/controllers/__init__.py +++ b/system_setup/client/controllers/__init__.py @@ -15,6 +15,7 @@ from .identity import WSLIdentityController +from .integration import IntegrationController from subiquity.client.controllers import (ProgressController, WelcomeController) @@ -23,5 +24,6 @@ __all__ = [ 'WelcomeController', 'WSLIdentityController', 'ProgressController', + 'IntegrationController', ] diff --git a/system_setup/client/controllers/integration.py b/system_setup/client/controllers/integration.py new file mode 100644 index 00000000..a1477f04 --- /dev/null +++ b/system_setup/client/controllers/integration.py @@ -0,0 +1,34 @@ +import logging + +from subiquity.client.controller import SubiquityTuiController +from subiquity.common.types import WSLConfiguration1Data +from system_setup.ui.views.integration import IntegrationView + +log = logging.getLogger('ubuntu_wsl_oobe.controllers.integration') + + +class IntegrationController(SubiquityTuiController): + endpoint_name = 'wslconf1' + + async def make_ui(self): + data = await self.endpoint.GET() + return IntegrationView(self, data) + + def run_answers(self): + if all(elem in self.answers for elem in + ['custom_path', 'custom_mount_opt', 'gen_host', 'gen_resolvconf']): + integration = WSLConfiguration1Data( + 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(integration) + + def done(self, integration_data): + log.debug( + "IntegrationController.done next_screen user_spec=%s", + integration_data) + self.app.next_screen(self.endpoint.POST(integration_data)) + + def cancel(self): + self.app.prev_screen() diff --git a/system_setup/ui/views/__init__.py b/system_setup/ui/views/__init__.py index 45ffae82..bc201ccc 100644 --- a/system_setup/ui/views/__init__.py +++ b/system_setup/ui/views/__init__.py @@ -14,7 +14,9 @@ # along with this program. If not, see . from .identity import WSLIdentityView +from .integration import IntegrationView __all__ = [ 'WSLIdentityView', + 'IntegrationView', ] \ No newline at end of file diff --git a/system_setup/ui/views/integration.py b/system_setup/ui/views/integration.py new file mode 100644 index 00000000..3123a15e --- /dev/null +++ b/system_setup/ui/views/integration.py @@ -0,0 +1,118 @@ +""" Integration + +Integration provides user with options to set up integration configurations. + +""" +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 WSLConfiguration1Data + + +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 IntegrationForm(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 IntegrationView(BaseView): + title = _("Tweaks") + 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, + } + self.form = IntegrationForm(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(WSLConfiguration1Data( + 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 + )) From 6e922180062f893d7b4c70d4b1b22c71a05eaa51 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Sat, 17 Jul 2021 00:19:11 +0800 Subject: [PATCH 15/69] TUI: Add initial Overview Support --- system_setup/client/client.py | 1 + system_setup/client/controllers/__init__.py | 2 + system_setup/client/controllers/overview.py | 28 +++++++++++++ system_setup/ui/views/__init__.py | 2 + system_setup/ui/views/overview.py | 46 +++++++++++++++++++++ 5 files changed, 79 insertions(+) create mode 100644 system_setup/client/controllers/overview.py create mode 100644 system_setup/ui/views/overview.py diff --git a/system_setup/client/client.py b/system_setup/client/client.py index 54fb9c0a..c683e7db 100644 --- a/system_setup/client/client.py +++ b/system_setup/client/client.py @@ -31,6 +31,7 @@ class SystemSetupClient(SubiquityClient): "Welcome", "WSLIdentity", "Integration", + "Overview", "Progress", ] diff --git a/system_setup/client/controllers/__init__.py b/system_setup/client/controllers/__init__.py index 1d7fc015..2636dbf6 100644 --- a/system_setup/client/controllers/__init__.py +++ b/system_setup/client/controllers/__init__.py @@ -16,6 +16,7 @@ from .identity import WSLIdentityController from .integration import IntegrationController +from .overview import OverviewController from subiquity.client.controllers import (ProgressController, WelcomeController) @@ -25,5 +26,6 @@ __all__ = [ 'WSLIdentityController', 'ProgressController', 'IntegrationController', + 'OverviewController', ] diff --git a/system_setup/client/controllers/overview.py b/system_setup/client/controllers/overview.py new file mode 100644 index 00000000..d0fb7208 --- /dev/null +++ b/system_setup/client/controllers/overview.py @@ -0,0 +1,28 @@ +import logging +from subiquity.client.controller import SubiquityTuiController + +from subiquitycore.utils import run_command +from system_setup.ui.views.overview import OverviewView + +log = logging.getLogger('ubuntu_wsl_oobe.controllers.identity') + + +def disable_ubuntu_wsl_oobe(): + """ Stop running ubuntu_wsl_oobe and remove the package """ + log.info('disabling ubuntu-wsl-oobe service') + #run_command(["apt", "remove", "-y", "ubuntu-wsl-oobe", "ubuntu-wsl-oobe-subiquitycore"]) + return + + +class OverviewController(SubiquityTuiController): + + async def make_ui(self): + return OverviewView(self) + + def cancel(self): + self.app.cancel() + + def done(self, result): + if not self.opts.dry_run: + disable_ubuntu_wsl_oobe() + self.app.exit() diff --git a/system_setup/ui/views/__init__.py b/system_setup/ui/views/__init__.py index bc201ccc..7aff96f6 100644 --- a/system_setup/ui/views/__init__.py +++ b/system_setup/ui/views/__init__.py @@ -15,8 +15,10 @@ from .identity import WSLIdentityView from .integration import IntegrationView +from .overview import OverviewView __all__ = [ 'WSLIdentityView', 'IntegrationView', + 'OverviewView', ] \ No newline at end of file diff --git a/system_setup/ui/views/overview.py b/system_setup/ui/views/overview.py new file mode 100644 index 00000000..43a34f2d --- /dev/null +++ b/system_setup/ui/views/overview.py @@ -0,0 +1,46 @@ +""" Overview + +Overview provides user with the overview of all the current settings. + +""" + + + +import logging + +from subiquitycore.ui.buttons import done_btn +from subiquitycore.ui.utils import button_pile, screen +from subiquitycore.view import BaseView + +log = logging.getLogger("ubuntu_wsl_oobe.ui.views.overview") + + +class OverviewView(BaseView): + title = _("Setup Complete") + + def __init__(self, controller): + self.controller = controller + user_name = "test" + #with open('/var/lib/ubuntu-wsl/assigned_account', 'r') as f: + # user_name = f.read() + complete_text = _("Hi {username},\n" + "You have complete the setup!\n\n" + "It is suggested to run the following command to update your Ubuntu " + "to the latest version:\n\n\n" + " $ sudo apt update\n $ sudo apt upgrade\n\n\n" + "You can use the builtin `ubuntuwsl` command to manage your WSL settings:\n\n\n" + " $ sudo ubuntuwsl ...\n\n\n" + "* All settings will take effect after first restart of Ubuntu.").format(username=user_name) + + super().__init__( + screen( + rows=[], + buttons=button_pile( + [done_btn(_("Done"), on_press=self.confirm), ]), + focus_buttons=True, + excerpt=complete_text, + ) + ) + + def confirm(self, result): + self.controller.done(result) From 6cbe3e8a369cd9a84a5ae209c6c5e1f0c9735858 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Wed, 21 Jul 2021 17:07:40 +0800 Subject: [PATCH 16/69] Generic Fixes --- system_setup/client/controllers/integration.py | 2 +- system_setup/client/controllers/overview.py | 10 +--------- system_setup/cmd/tui.py | 2 +- system_setup/ui/views/identity.py | 4 +++- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/system_setup/client/controllers/integration.py b/system_setup/client/controllers/integration.py index a1477f04..07d2d1e8 100644 --- a/system_setup/client/controllers/integration.py +++ b/system_setup/client/controllers/integration.py @@ -4,7 +4,7 @@ from subiquity.client.controller import SubiquityTuiController from subiquity.common.types import WSLConfiguration1Data from system_setup.ui.views.integration import IntegrationView -log = logging.getLogger('ubuntu_wsl_oobe.controllers.integration') +log = logging.getLogger('system_setup.client.controllers.integration') class IntegrationController(SubiquityTuiController): diff --git a/system_setup/client/controllers/overview.py b/system_setup/client/controllers/overview.py index d0fb7208..13acb1ec 100644 --- a/system_setup/client/controllers/overview.py +++ b/system_setup/client/controllers/overview.py @@ -7,12 +7,6 @@ from system_setup.ui.views.overview import OverviewView log = logging.getLogger('ubuntu_wsl_oobe.controllers.identity') -def disable_ubuntu_wsl_oobe(): - """ Stop running ubuntu_wsl_oobe and remove the package """ - log.info('disabling ubuntu-wsl-oobe service') - #run_command(["apt", "remove", "-y", "ubuntu-wsl-oobe", "ubuntu-wsl-oobe-subiquitycore"]) - return - class OverviewController(SubiquityTuiController): @@ -23,6 +17,4 @@ class OverviewController(SubiquityTuiController): self.app.cancel() def done(self, result): - if not self.opts.dry_run: - disable_ubuntu_wsl_oobe() - self.app.exit() + self.app.next_screen() diff --git a/system_setup/cmd/tui.py b/system_setup/cmd/tui.py index ae70b66e..9be6d7cf 100755 --- a/system_setup/cmd/tui.py +++ b/system_setup/cmd/tui.py @@ -111,7 +111,7 @@ def main(): logdir = LOGDIR if opts.dry_run: logdir = ".subiquity" - logfiles = setup_logger(dir=logdir, base='system_setup-client') + logfiles = setup_logger(dir=logdir, base='systemsetup-client') logger = logging.getLogger('subiquity') version = "unknown" diff --git a/system_setup/ui/views/identity.py b/system_setup/ui/views/identity.py index 979b05ec..cb883c12 100644 --- a/system_setup/ui/views/identity.py +++ b/system_setup/ui/views/identity.py @@ -30,6 +30,7 @@ class WSLIdentityForm(Form): 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 @@ -52,7 +53,8 @@ class WSLIdentityForm(Form): class WSLIdentityView(BaseView): title = IdentityView.title - excerpt = _("Enter the username and password you will use to log in.") + 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 From a97cc28ae6a35a6a12972bf46b05d1df6a4ad4f3 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Fri, 23 Jul 2021 00:49:12 +0800 Subject: [PATCH 17/69] Reconfigure mode: WIP --- Makefile | 7 + system_setup/client/client.py | 12 +- system_setup/client/controllers/__init__.py | 2 + .../client/controllers/reconfiguration.py | 64 ++++++++ system_setup/cmd/server.py | 1 + system_setup/cmd/tui.py | 3 + system_setup/models/wslconf2.py | 2 + system_setup/server/controllers/wslconf2.py | 87 +++++++++- system_setup/server/server.py | 13 +- system_setup/ui/views/__init__.py | 2 + system_setup/ui/views/reconfiguration.py | 151 ++++++++++++++++++ 11 files changed, 337 insertions(+), 7 deletions(-) create mode 100644 system_setup/client/controllers/reconfiguration.py create mode 100644 system_setup/ui/views/reconfiguration.py 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, + )) From cc7a1993719ef8e1b6dc0257f177e9476569bbe8 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Sat, 7 Aug 2021 01:58:09 +0800 Subject: [PATCH 18/69] Reconfigure mode: WIP --- system_setup/ui/views/reconfigure.py | 118 +++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 system_setup/ui/views/reconfigure.py diff --git a/system_setup/ui/views/reconfigure.py b/system_setup/ui/views/reconfigure.py new file mode 100644 index 00000000..8b0ce09a --- /dev/null +++ b/system_setup/ui/views/reconfigure.py @@ -0,0 +1,118 @@ +""" Integration + +Integration provides user with options to set up integration configurations. + +""" +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 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) + + 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 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, + } + self.form = IntegrationForm(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(WSLConfiguration1Data( + 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 + )) From f4c7dc68dd071a4cd58ec60d053aa6a983790670 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Tue, 3 Aug 2021 20:39:39 +0800 Subject: [PATCH 19/69] linting fixes --- subiquitycore/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/subiquitycore/utils.py b/subiquitycore/utils.py index ab17df08..067ceeee 100644 --- a/subiquitycore/utils.py +++ b/subiquitycore/utils.py @@ -145,7 +145,7 @@ def disable_subiquity(): "serial-subiquity@*.service"]) return -"""""" + def is_wsl(): """ Returns True if we are on a WSL system """ - return pathlib.Path("/proc/sys/fs/binfmt_misc/WSLInterop").is_file() \ No newline at end of file + return pathlib.Path("/proc/sys/fs/binfmt_misc/WSLInterop").is_file() From 3536f0517a20f4286d5693d3dc8b486c4389f252 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Sat, 7 Aug 2021 01:14:26 +0800 Subject: [PATCH 20/69] system_setup: attempt to fix the bug --- system_setup/cmd/server.py | 2 +- system_setup/models/system_server.py | 40 +++++++++++++++------------- system_setup/server/server.py | 5 ++-- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/system_setup/cmd/server.py b/system_setup/cmd/server.py index 09d4a5b3..4aa82574 100644 --- a/system_setup/cmd/server.py +++ b/system_setup/cmd/server.py @@ -70,7 +70,7 @@ def main(): logfiles = setup_logger(dir=logdir, base='systemsetup-server') - logger = logging.getLogger('systemsetup') + logger = logging.getLogger('systemsetup-server') version = "unknown" logger.info("Starting System Setup server revision {}".format(version)) logger.info("Arguments passed: {}".format(sys.argv)) diff --git a/system_setup/models/system_server.py b/system_setup/models/system_server.py index 8e956d93..8568df0a 100644 --- a/system_setup/models/system_server.py +++ b/system_setup/models/system_server.py @@ -41,21 +41,24 @@ ff02::1 ip6-allnodes ff02::2 ip6-allrouters """ -# Models that will be used in WSL system setup -ALL_MODEL_NAMES = [ - "identity", - "locale", - "wslconf1", - "wslconf2", -] - - class SystemSetupModel(SubiquityModel): """The overall model for subiquity.""" target = '/' - def __init__(self, root): + # Models that will be used in WSL system setup + ALL_MODEL_NAMES = [ + "identity", + "locale", + "wslconf1", + ] + + def __init__(self, root, reconfigure=False): + if reconfigure: + self.ALL_MODEL_NAMES = [ + "locale", + "wslconf2", + ] # Parent class init is not called to not load models we don't need. self.root = root self.is_wsl = is_wsl() @@ -81,24 +84,23 @@ class SystemSetupModel(SubiquityModel): self.confirmation = asyncio.Event() self._events = { - name: asyncio.Event() for name in ALL_MODEL_NAMES - } + name: asyncio.Event() for name in self.ALL_MODEL_NAMES + } self.postinstall_events = { - self._events[name] for name in ALL_MODEL_NAMES - } + self._events[name] for name in self.ALL_MODEL_NAMES + } def configured(self, model_name): # We need to override the parent class as *_MODEL_NAMES are global variables # in server.py - if model_name not in ALL_MODEL_NAMES: + if model_name not in self.ALL_MODEL_NAMES: return self._events[model_name].set() - stage = 'wslinstall' + stage = 'install' unconfigured = { - mn for mn in ALL_MODEL_NAMES + mn for mn in self.ALL_MODEL_NAMES if not self._events[mn].is_set() - } + } log.debug( "model %s for %s is configured, to go %s", model_name, stage, unconfigured) - diff --git a/system_setup/server/server.py b/system_setup/server/server.py index 6d6486d2..7c37759f 100644 --- a/system_setup/server/server.py +++ b/system_setup/server/server.py @@ -31,7 +31,8 @@ class SystemSetupServer(SubiquityServer): ] def __init__(self, opts, block_log_dir): - if opts.reconfigure: + self.is_reconfig = opts.reconfigure + if self.is_reconfig: self.controllers = [ "Reporting", "Error", @@ -44,4 +45,4 @@ class SystemSetupServer(SubiquityServer): root = '/' if self.opts.dry_run: root = os.path.abspath('.subiquity') - return SystemSetupModel(root) + return SystemSetupModel(root, self.is_reconfig) From 6bb86645ea179b30313e50386272b80e08fe50d7 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Mon, 9 Aug 2021 23:20:25 +0800 Subject: [PATCH 21/69] system_setup: dryrun username --- system_setup/client/controllers/identity.py | 12 ++++++++++++ system_setup/ui/views/overview.py | 9 +++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/system_setup/client/controllers/identity.py b/system_setup/client/controllers/identity.py index 0680792a..d05c5065 100644 --- a/system_setup/client/controllers/identity.py +++ b/system_setup/client/controllers/identity.py @@ -36,3 +36,15 @@ class WSLIdentityController(IdentityController): username=self.answers['username'], crypted_password=self.answers['password']) self.done(identity) + + def done(self, identity_data): + log.debug( + "IdentityController.done next_screen user_spec=%s", + identity_data) + if self.opts.dry_run: + username = "dryrun_user" + else: + username = identity_data.username + with open('/var/run/ubuntu_wsl_oobe_assigned_account', 'w') as f: + f.write(username) + self.app.next_screen(self.endpoint.POST(identity_data)) diff --git a/system_setup/ui/views/overview.py b/system_setup/ui/views/overview.py index 43a34f2d..1234e661 100644 --- a/system_setup/ui/views/overview.py +++ b/system_setup/ui/views/overview.py @@ -5,7 +5,7 @@ Overview provides user with the overview of all the current settings. """ - +import os import logging from subiquitycore.ui.buttons import done_btn @@ -20,9 +20,10 @@ class OverviewView(BaseView): def __init__(self, controller): self.controller = controller - user_name = "test" - #with open('/var/lib/ubuntu-wsl/assigned_account', 'r') as f: - # user_name = f.read() + user_name = "" + with open('/var/run/ubuntu_wsl_oobe_assigned_account', 'r') as f: + user_name = f.read() + os.remove('/var/run/ubuntu_wsl_oobe_assigned_account') complete_text = _("Hi {username},\n" "You have complete the setup!\n\n" "It is suggested to run the following command to update your Ubuntu " From 9745fcc05f22e35ec3ab5aa6f6585d3ce37b67ef Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Tue, 10 Aug 2021 00:05:10 +0800 Subject: [PATCH 22/69] system_setup: identity tui validation fix --- system_setup/ui/views/identity.py | 82 +++++++++++++++++++++++++------ 1 file changed, 68 insertions(+), 14 deletions(-) diff --git a/system_setup/ui/views/identity.py b/system_setup/ui/views/identity.py index cb883c12..484a1ebf 100644 --- a/system_setup/ui/views/identity.py +++ b/system_setup/ui/views/identity.py @@ -13,42 +13,84 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import os +import re +from subiquity.common.resources import resource_path from urwid import ( connect_signal, ) from subiquity.common.types import IdentityData -from subiquity.ui.views.identity import IdentityForm, IdentityView, setup_password_validation +from subiquity.ui.views.identity import IdentityForm, IdentityView, PasswordField, RealnameField, UsernameField, setup_password_validation from subiquitycore.ui.form import Form from subiquitycore.ui.utils import screen from subiquitycore.utils import crypt_password from subiquitycore.view import BaseView +HOSTNAME_MAXLEN = 64 +HOSTNAME_REGEX = r'[a-z0-9_][a-z0-9_-]*' +REALNAME_MAXLEN = 160 +SSH_IMPORT_MAXLEN = 256 + 3 # account for lp: or gh: +USERNAME_MAXLEN = 32 +USERNAME_REGEX = r'[a-z_][a-z0-9_-]*' class WSLIdentityForm(Form): - 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 - - def __init__(self, initial): - self.identityForm = IdentityForm([], initial) + def __init__(self, reserved_usernames, initial): + self.reserved_usernames = reserved_usernames super().__init__(initial=initial) + realname = RealnameField(_("Your name:")) + username = UsernameField(_("Pick a username:"), help=_("The username does not need to match your Windows username")) + password = PasswordField(_("Choose a password:")) + confirm_password = PasswordField(_("Confirm your password:")) + def validate_realname(self): - self.identityForm.validate_realname() + if len(self.realname.value) > REALNAME_MAXLEN: + return _( + "Name too long, must be less than {limit}" + ).format(limit=REALNAME_MAXLEN) + + def validate_hostname(self): + if len(self.hostname.value) < 1: + return _("Server name must not be empty") + + if len(self.hostname.value) > HOSTNAME_MAXLEN: + return _( + "Server name too long, must be less than {limit}" + ).format(limit=HOSTNAME_MAXLEN) + + if not re.match(HOSTNAME_REGEX, self.hostname.value): + return _( + "Hostname must match HOSTNAME_REGEX: " + HOSTNAME_REGEX) def validate_username(self): - self.identityForm.validate_username() + username = self.username.value + if len(username) < 1: + return _("Username missing") + + if len(username) > USERNAME_MAXLEN: + return _( + "Username too long, must be less than {limit}" + ).format(limit=USERNAME_MAXLEN) + + if not re.match(r'[a-z_][a-z0-9_-]*', username): + return _( + "Username must match USERNAME_REGEX: " + USERNAME_REGEX) + + if username in self.reserved_usernames: + return _( + 'The username "{username}" is reserved for use by the system.' + ).format(username=username) def validate_password(self): - self.identityForm.validate_password() + if len(self.password.value) < 1: + return _("Password must be set") def validate_confirm_password(self): - self.identityForm.validate_confirm_password() + if self.password.value != self.confirm_password.value: + return _("Passwords do not match") class WSLIdentityView(BaseView): @@ -59,12 +101,24 @@ class WSLIdentityView(BaseView): 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, } - self.form = WSLIdentityForm(initial) + self.form = WSLIdentityForm(reserved_usernames, initial) connect_signal(self.form, 'submit', self.done) setup_password_validation(self.form, _("passwords")) From 90bb1561417a5cb66cecbc4e4c10e57a9f9649d1 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Wed, 25 Aug 2021 10:19:02 +0800 Subject: [PATCH 23/69] subiquity/common/types.py: WSLConf1 type fix --- subiquity/common/types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/subiquity/common/types.py b/subiquity/common/types.py index 271fb65c..e0818680 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -336,6 +336,8 @@ class ShutdownMode(enum.Enum): REBOOT = enum.auto() POWEROFF = enum.auto() + +@attr.s(auto_attribs=True) class WSLConfiguration1Data: custom_path: str = attr.ib(default='/mnt/') custom_mount_opt: str = '' From 55fdfc1df0cb988bbb7bb581ec963d3dd165c980 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Tue, 10 Aug 2021 15:26:07 +0800 Subject: [PATCH 24/69] system_setup: fix a small bug in api calling --- system_setup/server/server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system_setup/server/server.py b/system_setup/server/server.py index 7c37759f..571d0e6d 100644 --- a/system_setup/server/server.py +++ b/system_setup/server/server.py @@ -28,11 +28,12 @@ class SystemSetupServer(SubiquityServer): "Locale", "Identity", "WSLConfiguration1", + "WSLConfiguration2", ] def __init__(self, opts, block_log_dir): self.is_reconfig = opts.reconfigure - if self.is_reconfig: + if self.is_reconfig and not opts.dry_run: self.controllers = [ "Reporting", "Error", From 74e5c47c252a8301f7d13894abb8d43b8779e594 Mon Sep 17 00:00:00 2001 From: Didier Roche Date: Tue, 10 Aug 2021 14:49:07 +0200 Subject: [PATCH 25/69] Revert "system_setup: identity tui validation fix" This reverts commit 2f3dc90b7e565e9592fc27f9748e2ca1ae7c65f5. --- system_setup/ui/views/identity.py | 82 ++++++------------------------- 1 file changed, 14 insertions(+), 68 deletions(-) diff --git a/system_setup/ui/views/identity.py b/system_setup/ui/views/identity.py index 484a1ebf..cb883c12 100644 --- a/system_setup/ui/views/identity.py +++ b/system_setup/ui/views/identity.py @@ -13,84 +13,42 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import os -import re -from subiquity.common.resources import resource_path from urwid import ( connect_signal, ) from subiquity.common.types import IdentityData -from subiquity.ui.views.identity import IdentityForm, IdentityView, PasswordField, RealnameField, UsernameField, setup_password_validation +from subiquity.ui.views.identity import IdentityForm, IdentityView, setup_password_validation from subiquitycore.ui.form import Form from subiquitycore.ui.utils import screen from subiquitycore.utils import crypt_password from subiquitycore.view import BaseView -HOSTNAME_MAXLEN = 64 -HOSTNAME_REGEX = r'[a-z0-9_][a-z0-9_-]*' -REALNAME_MAXLEN = 160 -SSH_IMPORT_MAXLEN = 256 + 3 # account for lp: or gh: -USERNAME_MAXLEN = 32 -USERNAME_REGEX = r'[a-z_][a-z0-9_-]*' class WSLIdentityForm(Form): - def __init__(self, reserved_usernames, initial): - self.reserved_usernames = reserved_usernames + 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 + + def __init__(self, initial): + self.identityForm = IdentityForm([], initial) super().__init__(initial=initial) - realname = RealnameField(_("Your name:")) - username = UsernameField(_("Pick a username:"), help=_("The username does not need to match your Windows username")) - password = PasswordField(_("Choose a password:")) - confirm_password = PasswordField(_("Confirm your password:")) - def validate_realname(self): - if len(self.realname.value) > REALNAME_MAXLEN: - return _( - "Name too long, must be less than {limit}" - ).format(limit=REALNAME_MAXLEN) - - def validate_hostname(self): - if len(self.hostname.value) < 1: - return _("Server name must not be empty") - - if len(self.hostname.value) > HOSTNAME_MAXLEN: - return _( - "Server name too long, must be less than {limit}" - ).format(limit=HOSTNAME_MAXLEN) - - if not re.match(HOSTNAME_REGEX, self.hostname.value): - return _( - "Hostname must match HOSTNAME_REGEX: " + HOSTNAME_REGEX) + self.identityForm.validate_realname() def validate_username(self): - username = self.username.value - if len(username) < 1: - return _("Username missing") - - if len(username) > USERNAME_MAXLEN: - return _( - "Username too long, must be less than {limit}" - ).format(limit=USERNAME_MAXLEN) - - if not re.match(r'[a-z_][a-z0-9_-]*', username): - return _( - "Username must match USERNAME_REGEX: " + USERNAME_REGEX) - - if username in self.reserved_usernames: - return _( - 'The username "{username}" is reserved for use by the system.' - ).format(username=username) + self.identityForm.validate_username() def validate_password(self): - if len(self.password.value) < 1: - return _("Password must be set") + self.identityForm.validate_password() def validate_confirm_password(self): - if self.password.value != self.confirm_password.value: - return _("Passwords do not match") + self.identityForm.validate_confirm_password() class WSLIdentityView(BaseView): @@ -101,24 +59,12 @@ class WSLIdentityView(BaseView): 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, } - self.form = WSLIdentityForm(reserved_usernames, initial) + self.form = WSLIdentityForm(initial) connect_signal(self.form, 'submit', self.done) setup_password_validation(self.form, _("passwords")) From 1e52ab6374658872a097c7d431f5a347382fa6b2 Mon Sep 17 00:00:00 2001 From: Didier Roche Date: Tue, 10 Aug 2021 16:07:43 +0200 Subject: [PATCH 26/69] Use inheritance for IdentityForm The previous composition version had some flaws due to multiple instances of a given Field with different addresses when reinstalled in the parent class. This caused the field validation validate_ to not work as it was not taken the expected referenced values. Instead of duplicating the code, we use an inherited version, taking and overriding only our desired fields. We still need to derive WSLIdentityView from the base class instead of IdentityView, as this is what is instantiating the form. (WSLIdentityForm instead of IdentityForm). Co-authored-by: Jean-Baptiste Lallement --- system_setup/ui/views/identity.py | 38 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/system_setup/ui/views/identity.py b/system_setup/ui/views/identity.py index cb883c12..e4965a2e 100644 --- a/system_setup/ui/views/identity.py +++ b/system_setup/ui/views/identity.py @@ -13,6 +13,7 @@ # 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, ) @@ -20,13 +21,14 @@ from urwid import ( from subiquity.common.types import IdentityData from subiquity.ui.views.identity import IdentityForm, IdentityView, setup_password_validation -from subiquitycore.ui.form import Form 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(Form): + +class WSLIdentityForm(IdentityForm): realname = IdentityForm.realname username = IdentityForm.username @@ -34,23 +36,6 @@ class WSLIdentityForm(Form): password = IdentityForm.password confirm_password = IdentityForm.confirm_password - def __init__(self, initial): - self.identityForm = IdentityForm([], initial) - super().__init__(initial=initial) - - def validate_realname(self): - self.identityForm.validate_realname() - - def validate_username(self): - self.identityForm.validate_username() - - def validate_password(self): - self.identityForm.validate_password() - - def validate_confirm_password(self): - self.identityForm.validate_confirm_password() - - class WSLIdentityView(BaseView): title = IdentityView.title excerpt = _("Please create a default UNIX user account. " @@ -59,12 +44,25 @@ class WSLIdentityView(BaseView): 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, } - self.form = WSLIdentityForm(initial) + # 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")) From 300161e0eab15ef92671f548e5f81fd78c0e1c37 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Thu, 12 Aug 2021 20:01:44 +0800 Subject: [PATCH 27/69] system_setup: Pre fixes --- Makefile | 2 +- system_setup/ui/views/reconfigure.py | 118 --------------------------- 2 files changed, 1 insertion(+), 119 deletions(-) delete mode 100644 system_setup/ui/views/reconfigure.py diff --git a/Makefile b/Makefile index f1e3a55f..f82c5670 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ 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)) diff --git a/system_setup/ui/views/reconfigure.py b/system_setup/ui/views/reconfigure.py deleted file mode 100644 index 8b0ce09a..00000000 --- a/system_setup/ui/views/reconfigure.py +++ /dev/null @@ -1,118 +0,0 @@ -""" Integration - -Integration provides user with options to set up integration configurations. - -""" -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 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) - - 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 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, - } - self.form = IntegrationForm(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(WSLConfiguration1Data( - 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 - )) From aff93e5cdaa2c16604f48ddb393d6b2bc7a1edc0 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Thu, 12 Aug 2021 22:31:27 +0800 Subject: [PATCH 28/69] system_setup: PEP8 linting fixes --- system_setup/client/client.py | 5 +- system_setup/client/controllers/__init__.py | 4 +- .../client/controllers/integration.py | 3 +- system_setup/client/controllers/overview.py | 2 - .../client/controllers/reconfiguration.py | 16 +-- system_setup/models/system_server.py | 7 +- system_setup/models/wslconf2.py | 19 ++-- system_setup/server/controllers/__init__.py | 8 +- system_setup/server/controllers/identity.py | 2 - system_setup/server/controllers/wslconf2.py | 29 ++--- system_setup/ui/views/__init__.py | 2 +- system_setup/ui/views/identity.py | 13 ++- system_setup/ui/views/integration.py | 31 +++--- system_setup/ui/views/overview.py | 11 +- system_setup/ui/views/reconfiguration.py | 100 +++++++++++++----- 15 files changed, 161 insertions(+), 91 deletions(-) diff --git a/system_setup/client/client.py b/system_setup/client/client.py index fd488d73..39d324da 100644 --- a/system_setup/client/client.py +++ b/system_setup/client/client.py @@ -29,13 +29,14 @@ class SystemSetupClient(SubiquityClient): snapd_socket_path = None controllers = [ - #"Serial", + # "Serial", "Welcome", "WSLIdentity", "Integration", "Overview", "Progress", ] + def __init__(self, opts): if opts.reconfigure: self.controllers = [ @@ -45,8 +46,6 @@ class SystemSetupClient(SubiquityClient): ] super().__init__(opts) - - def restart(self, remove_last_screen=True, restart_server=False): log.debug(f"restart {remove_last_screen} {restart_server}") if self.fg_proc is not None: diff --git a/system_setup/client/controllers/__init__.py b/system_setup/client/controllers/__init__.py index ad2307ab..79abf691 100644 --- a/system_setup/client/controllers/__init__.py +++ b/system_setup/client/controllers/__init__.py @@ -19,7 +19,8 @@ from .integration import IntegrationController from .overview import OverviewController from .reconfiguration import ReconfigurationController -from subiquity.client.controllers import (ProgressController, WelcomeController) +from subiquity.client.controllers import (ProgressController, + WelcomeController) __all__ = [ @@ -30,4 +31,3 @@ __all__ = [ 'OverviewController', 'ReconfigurationController', ] - diff --git a/system_setup/client/controllers/integration.py b/system_setup/client/controllers/integration.py index 07d2d1e8..223f7ae5 100644 --- a/system_setup/client/controllers/integration.py +++ b/system_setup/client/controllers/integration.py @@ -16,7 +16,8 @@ class IntegrationController(SubiquityTuiController): def run_answers(self): if all(elem in self.answers for elem in - ['custom_path', 'custom_mount_opt', 'gen_host', 'gen_resolvconf']): + ['custom_path', 'custom_mount_opt', + 'gen_host', 'gen_resolvconf']): integration = WSLConfiguration1Data( custom_path=self.answers['custom_path'], custom_mount_opt=self.answers['custom_mount_opt'], diff --git a/system_setup/client/controllers/overview.py b/system_setup/client/controllers/overview.py index 13acb1ec..029b95dd 100644 --- a/system_setup/client/controllers/overview.py +++ b/system_setup/client/controllers/overview.py @@ -1,13 +1,11 @@ import logging from subiquity.client.controller import SubiquityTuiController -from subiquitycore.utils import run_command from system_setup.ui.views.overview import OverviewView log = logging.getLogger('ubuntu_wsl_oobe.controllers.identity') - class OverviewController(SubiquityTuiController): async def make_ui(self): diff --git a/system_setup/client/controllers/reconfiguration.py b/system_setup/client/controllers/reconfiguration.py index 8631645e..8fba4021 100644 --- a/system_setup/client/controllers/reconfiguration.py +++ b/system_setup/client/controllers/reconfiguration.py @@ -15,10 +15,6 @@ 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 @@ -35,14 +31,20 @@ class ReconfigurationController(SubiquityTuiController): 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']): + ['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'], + interop_appendwindowspath=self + .answers['interop_appendwindowspath'], gui_theme=self.answers['gui_theme'], gui_followwintheme=self.answers['gui_followwintheme'], legacy_gui=self.answers['legacy_gui'], @@ -61,4 +63,4 @@ class ReconfigurationController(SubiquityTuiController): self.app.next_screen(self.endpoint.POST(reconf_data)) def cancel(self): - self.app.prev_screen() \ No newline at end of file + self.app.prev_screen() diff --git a/system_setup/models/system_server.py b/system_setup/models/system_server.py index 8568df0a..1e2b571f 100644 --- a/system_setup/models/system_server.py +++ b/system_setup/models/system_server.py @@ -18,7 +18,7 @@ import logging from subiquity.models.subiquity import SubiquityModel -from subiquitycore.utils import run_command, is_wsl +from subiquitycore.utils import is_wsl from subiquity.models.locale import LocaleModel @@ -41,6 +41,7 @@ ff02::1 ip6-allnodes ff02::2 ip6-allrouters """ + class SystemSetupModel(SubiquityModel): """The overall model for subiquity.""" @@ -91,8 +92,8 @@ class SystemSetupModel(SubiquityModel): } def configured(self, model_name): - # We need to override the parent class as *_MODEL_NAMES are global variables - # in server.py + # We need to override the parent class as + # *_MODEL_NAMES are global variables in server.py if model_name not in self.ALL_MODEL_NAMES: return self._events[model_name].set() diff --git a/system_setup/models/wslconf2.py b/system_setup/models/wslconf2.py index 5efee78f..3f846047 100644 --- a/system_setup/models/wslconf2.py +++ b/system_setup/models/wslconf2.py @@ -16,7 +16,6 @@ import logging import subprocess import attr -import json from subiquitycore.utils import run_command @@ -50,7 +49,8 @@ class WSLConfiguration2Model(object): def apply_settings(self, result, is_dry_run=False): d = {} - #TODO: placholder settings; should be dynamically assgined using ubuntu-wsl-integration + # 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 @@ -71,7 +71,8 @@ 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 + # 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) @@ -100,7 +101,8 @@ class WSLConfiguration2Model(object): result.interop_appendwindowspath], stdout=subprocess.DEVNULL) run_command(["/usr/bin/ubuntuwsl", "update", - "ubuntu.GUI.followwintheme", result.gui_followwintheme], + "ubuntu.GUI.followwintheme", + result.gui_followwintheme], stdout=subprocess.DEVNULL) run_command(["/usr/bin/ubuntuwsl", "update", "ubuntu.GUI.theme", result.gui_theme], @@ -109,15 +111,16 @@ class WSLConfiguration2Model(object): "ubuntu.Interop.guiintergration", result.legacy_gui], stdout=subprocess.DEVNULL) run_command(["/usr/bin/ubuntuwsl", "update", - "ubuntu.Interop.audiointegration", result.legacy_audio], + "ubuntu.Interop.audiointegration", + result.legacy_audio], stdout=subprocess.DEVNULL) run_command(["/usr/bin/ubuntuwsl", "update", - "ubuntu.Interop.advancedipdetection", result.adv_ip_detect], + "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) - + stdout=subprocess.DEVNULL) @property def wslconf2(self): diff --git a/system_setup/server/controllers/__init__.py b/system_setup/server/controllers/__init__.py index d30820b7..3c52b268 100644 --- a/system_setup/server/controllers/__init__.py +++ b/system_setup/server/controllers/__init__.py @@ -13,7 +13,11 @@ # 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.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 @@ -31,4 +35,4 @@ __all__ = [ 'UserdataController', "WSLConfiguration1Controller", "WSLConfiguration2Controller", -] \ No newline at end of file +] diff --git a/system_setup/server/controllers/identity.py b/system_setup/server/controllers/identity.py index a6769b0c..2935170b 100644 --- a/system_setup/server/controllers/identity.py +++ b/system_setup/server/controllers/identity.py @@ -17,8 +17,6 @@ import logging import attr -from subiquitycore.context import with_context - from subiquity.common.types import IdentityData from subiquity.server.controllers.identity import IdentityController diff --git a/system_setup/server/controllers/wslconf2.py b/system_setup/server/controllers/wslconf2.py index 4b474c9e..ce65f39b 100644 --- a/system_setup/server/controllers/wslconf2.py +++ b/system_setup/server/controllers/wslconf2.py @@ -113,22 +113,24 @@ class WSLConfiguration2Controller(SubiquityController): 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' + def bool_converter(x): + return 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']), + 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=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']), + 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) @@ -168,7 +170,8 @@ class WSLConfiguration2Controller(SubiquityController): data.gen_host = self.model.wslconf2.gen_host data.gen_resolvconf = self.model.wslconf2.gen_resolvconf data.interop_enabled = self.model.wslconf2.interop_enabled - data.interop_appendwindowspath = self.model.wslconf2.interop_appendwindowspath + data.interop_appendwindowspath = \ + self.model.wslconf2.interop_appendwindowspath data.gui_theme = self.model.wslconf2.gui_theme data.gui_followwintheme = self.model.wslconf2.gui_followwintheme data.legacy_gui = self.model.wslconf2.legacy_gui diff --git a/system_setup/ui/views/__init__.py b/system_setup/ui/views/__init__.py index 8ec74019..81db478e 100644 --- a/system_setup/ui/views/__init__.py +++ b/system_setup/ui/views/__init__.py @@ -23,4 +23,4 @@ __all__ = [ 'IntegrationView', 'OverviewView', 'ReconfigurationView', -] \ No newline at end of file +] diff --git a/system_setup/ui/views/identity.py b/system_setup/ui/views/identity.py index e4965a2e..4e5788f6 100644 --- a/system_setup/ui/views/identity.py +++ b/system_setup/ui/views/identity.py @@ -20,7 +20,11 @@ from urwid import ( from subiquity.common.types import IdentityData -from subiquity.ui.views.identity import IdentityForm, IdentityView, setup_password_validation +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 @@ -32,10 +36,12 @@ class WSLIdentityForm(IdentityForm): realname = IdentityForm.realname username = IdentityForm.username - username.help = _("The username does not need to match your Windows 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. " @@ -61,7 +67,8 @@ class WSLIdentityView(BaseView): 'username': identity_data.username, } - # This is the different form model with IdentityView which prevents us from inheriting it + # 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) diff --git a/system_setup/ui/views/integration.py b/system_setup/ui/views/integration.py index 3123a15e..aacb2af8 100644 --- a/system_setup/ui/views/integration.py +++ b/system_setup/ui/views/integration.py @@ -40,8 +40,11 @@ class IntegrationForm(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")) + 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=_( @@ -50,15 +53,18 @@ class IntegrationForm(Form): 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.") + 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"] + 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)) @@ -78,26 +84,25 @@ class IntegrationForm(Form): 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) + "https://docs.microsoft.com/en-us/windows/wsl/" + "wsl-config#mount-options " + "for correct valid input").format(e_t) class IntegrationView(BaseView): title = _("Tweaks") - excerpt = _("In this page, you can tweak Ubuntu WSL to your needs. \n" - ) + 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, + 'custom_mount_opt': integration_data.custom_mount_opt, 'gen_host': integration_data.gen_host, 'gen_resolvconf': integration_data.gen_resolvconf, } self.form = IntegrationForm(initial=initial) - connect_signal(self.form, 'submit', self.done) super().__init__( diff --git a/system_setup/ui/views/overview.py b/system_setup/ui/views/overview.py index 1234e661..5343360a 100644 --- a/system_setup/ui/views/overview.py +++ b/system_setup/ui/views/overview.py @@ -26,12 +26,15 @@ class OverviewView(BaseView): os.remove('/var/run/ubuntu_wsl_oobe_assigned_account') complete_text = _("Hi {username},\n" "You have complete the setup!\n\n" - "It is suggested to run the following command to update your Ubuntu " - "to the latest version:\n\n\n" + "It is suggested to run the following command" + " to update your Ubuntu to the latest version:" + "\n\n\n" " $ sudo apt update\n $ sudo apt upgrade\n\n\n" - "You can use the builtin `ubuntuwsl` command to manage your WSL settings:\n\n\n" + "You can use the builtin `ubuntuwsl` command to " + "manage your WSL settings:\n\n\n" " $ sudo ubuntuwsl ...\n\n\n" - "* All settings will take effect after first restart of Ubuntu.").format(username=user_name) + "* All settings will take effect after first " + "restart of Ubuntu.").format(username=user_name) super().__init__( screen( diff --git a/system_setup/ui/views/reconfiguration.py b/system_setup/ui/views/reconfiguration.py index 8a834c53..a85320b0 100644 --- a/system_setup/ui/views/reconfiguration.py +++ b/system_setup/ui/views/reconfiguration.py @@ -41,38 +41,82 @@ 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")) + # 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")) + 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.")) + 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.") + 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"] + 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)) @@ -92,24 +136,26 @@ class ReconfigurationForm(Form): 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) + "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" - ) + 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, + '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, + 'interop_appendwindowspath': + integration_data.interop_appendwindowspath, 'gui_theme': integration_data.gui_theme, 'gui_followwintheme': integration_data.gui_followwintheme, 'legacy_gui': integration_data.legacy_gui, @@ -120,7 +166,6 @@ class ReconfigurationView(BaseView): 'mountfstab': integration_data.mountfstab, } self.form = ReconfigurationForm(initial=initial) - connect_signal(self.form, 'submit', self.done) super().__init__( @@ -139,7 +184,8 @@ class ReconfigurationView(BaseView): 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, + 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, From 3dccdddf395607c793a8e4745a1e0e4d30703613 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Fri, 13 Aug 2021 22:42:34 +0800 Subject: [PATCH 29/69] system_setup: no longer require sudo for dryrun --- system_setup/client/controllers/identity.py | 8 +++----- system_setup/ui/views/overview.py | 11 +++++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/system_setup/client/controllers/identity.py b/system_setup/client/controllers/identity.py index d05c5065..c4231189 100644 --- a/system_setup/client/controllers/identity.py +++ b/system_setup/client/controllers/identity.py @@ -41,10 +41,8 @@ class WSLIdentityController(IdentityController): log.debug( "IdentityController.done next_screen user_spec=%s", identity_data) - if self.opts.dry_run: - username = "dryrun_user" - else: + if not self.opts.dry_run: username = identity_data.username - with open('/var/run/ubuntu_wsl_oobe_assigned_account', 'w') as f: - f.write(username) + with open('/var/run/ubuntu_wsl_oobe_assigned_account', 'w') as f: + f.write(username) self.app.next_screen(self.endpoint.POST(identity_data)) diff --git a/system_setup/ui/views/overview.py b/system_setup/ui/views/overview.py index 5343360a..b978c9ca 100644 --- a/system_setup/ui/views/overview.py +++ b/system_setup/ui/views/overview.py @@ -14,16 +14,19 @@ from subiquitycore.view import BaseView log = logging.getLogger("ubuntu_wsl_oobe.ui.views.overview") +WSL_USERNAME_PATH = "/var/run/ubuntu_wsl_oobe_assigned_account" + class OverviewView(BaseView): title = _("Setup Complete") def __init__(self, controller): self.controller = controller - user_name = "" - with open('/var/run/ubuntu_wsl_oobe_assigned_account', 'r') as f: - user_name = f.read() - os.remove('/var/run/ubuntu_wsl_oobe_assigned_account') + user_name = "dryrun_user" + if os.path.isfile(WSL_USERNAME_PATH): + with open(WSL_USERNAME_PATH, 'r') as f: + user_name = f.read() + os.remove(WSL_USERNAME_PATH) complete_text = _("Hi {username},\n" "You have complete the setup!\n\n" "It is suggested to run the following command" From 0ad51520695393984c28f3ba76f5387debcd31ac Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Fri, 13 Aug 2021 23:05:38 +0800 Subject: [PATCH 30/69] system_setup: initial Progress fix --- system_setup/models/system_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system_setup/models/system_server.py b/system_setup/models/system_server.py index 1e2b571f..05aecea3 100644 --- a/system_setup/models/system_server.py +++ b/system_setup/models/system_server.py @@ -49,8 +49,8 @@ class SystemSetupModel(SubiquityModel): # Models that will be used in WSL system setup ALL_MODEL_NAMES = [ - "identity", "locale", + "identity", "wslconf1", ] @@ -87,7 +87,7 @@ class SystemSetupModel(SubiquityModel): self._events = { name: asyncio.Event() for name in self.ALL_MODEL_NAMES } - self.postinstall_events = { + self.install_events = { self._events[name] for name in self.ALL_MODEL_NAMES } From d89d9c3f7d6f17546b9a920d5287cb79f2fa3a6f Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Thu, 19 Aug 2021 23:42:06 +0800 Subject: [PATCH 31/69] system_setup: prevent loading unnecessary library --- system_setup/server/server.py | 385 +++++++++++++++++++++++++++++++++- 1 file changed, 382 insertions(+), 3 deletions(-) diff --git a/system_setup/server/server.py b/system_setup/server/server.py index 571d0e6d..77221757 100644 --- a/system_setup/server/server.py +++ b/system_setup/server/server.py @@ -13,13 +13,65 @@ # 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_server import SystemSetupModel +import asyncio +import logging import os +import shlex +import sys +import time + +import jsonschema +import yaml +from aiohttp import web +from cloudinit import atomic_helper, safeyaml, stages +from cloudinit.config.cc_set_passwords import rand_user_password +from cloudinit.distros import ug_util +from subiquity.common.api.server import bind, controller_for_request +from subiquity.common.apidef import API +from subiquity.common.errorreport import ErrorReporter +from subiquity.common.serialize import to_json +from subiquity.common.types import (ApplicationState, ErrorReportKind, + ErrorReportRef, PasswordKind) +from subiquity.server.controller import SubiquityController +from subiquity.server.errors import ErrorController +from subiquity.server.server import (MetaController, + get_installer_password_from_cloudinit_log) +from subiquitycore.async_helpers import run_in_thread +from subiquitycore.context import with_context +from subiquitycore.core import Application +from subiquitycore.ssh import user_key_fingerprints +from subiquitycore.utils import arun_command, run_command +from system_setup.models.system_server import SystemSetupModel + +log = logging.getLogger('subiquity.server.server') + +NOPROBERARG = "NOPROBER" -class SystemSetupServer(SubiquityServer): +class SystemSetupServer(Application): + ''' + Server for the System Setup. + No longer using old method because it keep inheriting incompatible + classes subiquity.server, which is not what we want for System Setup. + ''' + + snapd_socket_path = '/run/snapd.socket' + + base_schema = { + 'type': 'object', + 'properties': { + 'version': { + 'type': 'integer', + 'minimum': 1, + 'maximum': 1, + }, + }, + 'required': ['version'], + 'additionalProperties': True, + } + + project = "subiquity" from system_setup.server import controllers as controllers_mod controllers = [ "Reporting", @@ -32,6 +84,37 @@ class SystemSetupServer(SubiquityServer): ] def __init__(self, opts, block_log_dir): + super().__init__(opts) + self.block_log_dir = block_log_dir + self.cloud = None + self.cloud_init_ok = None + self._state = ApplicationState.STARTING_UP + self.state_event = asyncio.Event() + self.interactive = None + self.confirming_tty = '' + self.fatal_error = None + self.running_error_commands = False + self.installer_user_name = None + self.installer_user_passwd_kind = PasswordKind.NONE + self.installer_user_passwd = None + + self.echo_syslog_id = 'subiquity_echo.{}'.format(os.getpid()) + self.event_syslog_id = 'subiquity_event.{}'.format(os.getpid()) + self.log_syslog_id = 'subiquity_log.{}'.format(os.getpid()) + + self.error_reporter = ErrorReporter( + self.context.child("ErrorReporter"), self.opts.dry_run, self.root) + self.prober = None + self.kernel_cmdline = shlex.split(opts.kernel_cmdline) + self.controllers.remove("Refresh") + self.controllers.remove("SnapList") + self.snapd = None + self.note_data_for_apport("SnapUpdated", str(self.updated)) + self.event_listeners = [] + self.autoinstall_config = None + self.hub.subscribe('network-up', self._network_change) + self.hub.subscribe('network-proxy-set', self._proxy_set) + self.geoip = None self.is_reconfig = opts.reconfigure if self.is_reconfig and not opts.dry_run: self.controllers = [ @@ -47,3 +130,299 @@ class SystemSetupServer(SubiquityServer): if self.opts.dry_run: root = os.path.abspath('.subiquity') return SystemSetupModel(root, self.is_reconfig) + + def load_serialized_state(self): + for controller in self.controllers.instances: + controller.load_state() + + def add_event_listener(self, listener): + self.event_listeners.append(listener) + + def _maybe_push_to_journal(self, event_type, context, description): + pass + + def report_start_event(self, context, description): + for listener in self.event_listeners: + listener.report_start_event(context, description) + self._maybe_push_to_journal('start', context, description) + + def report_finish_event(self, context, description, status): + for listener in self.event_listeners: + listener.report_finish_event(context, description, status) + self._maybe_push_to_journal('finish', context, description) + + @property + def state(self): + return self._state + + def update_state(self, state): + self._state = state + self.state_event.set() + self.state_event.clear() + + def note_file_for_apport(self, key, path): + self.error_reporter.note_file_for_apport(key, path) + + def note_data_for_apport(self, key, value): + self.error_reporter.note_data_for_apport(key, value) + + def make_apport_report(self, kind, thing, *, wait=False, **kw): + return self.error_reporter.make_apport_report( + kind, thing, wait=wait, **kw) + + async def _run_error_cmds(self, report): + await report._info_task + Error = getattr(self.controllers, "Error", None) + if Error is not None and Error.cmds: + try: + await Error.run() + except Exception: + log.exception("running error-commands failed") + if not self.interactive: + self.update_state(ApplicationState.ERROR) + + def _exception_handler(self, loop, context): + exc = context.get('exception') + if exc is None: + super()._exception_handler(loop, context) + return + report = self.error_reporter.report_for_exc(exc) + log.error("top level error", exc_info=exc) + if not report: + report = self.make_apport_report( + ErrorReportKind.UNKNOWN, "unknown error", + exc=exc) + self.fatal_error = report + if self.interactive: + self.update_state(ApplicationState.ERROR) + if not self.running_error_commands: + self.running_error_commands = True + self.aio_loop.create_task(self._run_error_cmds(report)) + + @web.middleware + async def middleware(self, request, handler): + override_status = None + controller = await controller_for_request(request) + if isinstance(controller, SubiquityController): + if not controller.interactive(): + override_status = 'skip' + elif self.state == ApplicationState.NEEDS_CONFIRMATION: + if self.base_model.is_postinstall_only(controller.model_name): + override_status = 'confirm' + if override_status is not None: + resp = web.Response(headers={'x-status': override_status}) + else: + resp = await handler(request) + if self.updated: + resp.headers['x-updated'] = 'yes' + else: + resp.headers['x-updated'] = 'no' + if resp.get('exception'): + exc = resp['exception'] + log.debug( + 'request to {} crashed'.format(request.raw_path), exc_info=exc) + report = self.make_apport_report( + ErrorReportKind.SERVER_REQUEST_FAIL, + "request to {}".format(request.raw_path), + exc=exc) + resp.headers['x-error-report'] = to_json( + ErrorReportRef, report.ref()) + return resp + + @with_context() + async def apply_autoinstall_config(self, context): + for controller in self.controllers.instances: + if controller.interactive(): + log.debug( + "apply_autoinstall_config: skipping %s as interactive", + controller.name) + continue + await controller.apply_autoinstall_config() + controller.configured() + + def load_autoinstall_config(self, *, only_early): + log.debug("load_autoinstall_config only_early %s", only_early) + if self.opts.autoinstall is None: + return + with open(self.opts.autoinstall) as fp: + self.autoinstall_config = yaml.safe_load(fp) + if only_early: + self.controllers.Reporting.setup_autoinstall() + self.controllers.Reporting.start() + self.controllers.Error.setup_autoinstall() + with self.context.child("core_validation", level="INFO"): + jsonschema.validate(self.autoinstall_config, self.base_schema) + self.controllers.Early.setup_autoinstall() + else: + for controller in self.controllers.instances: + controller.setup_autoinstall() + + async def start_api_server(self): + app = web.Application(middlewares=[self.middleware]) + bind(app.router, API.meta, MetaController(self)) + bind(app.router, API.errors, ErrorController(self)) + if self.opts.dry_run: + from subiquity.server.dryrun import DryRunController + bind(app.router, API.dry_run, DryRunController(self)) + for controller in self.controllers.instances: + controller.add_routes(app) + runner = web.AppRunner(app, keepalive_timeout=0xffffffff) + await runner.setup() + site = web.UnixSite(runner, self.opts.socket) + await site.start() + # It is intended that a non-root client can connect. + os.chmod(self.opts.socket, 0o666) + + async def wait_for_cloudinit(self): + if self.opts.dry_run: + self.cloud_init_ok = True + return + ci_start = time.time() + status_coro = arun_command(["cloud-init", "status", "--wait"]) + try: + status_cp = await asyncio.wait_for(status_coro, 600) + except asyncio.CancelledError: + status_txt = '' + self.cloud_init_ok = False + else: + status_txt = status_cp.stdout + self.cloud_init_ok = True + log.debug("waited %ss for cloud-init", time.time() - ci_start) + if "status: done" in status_txt: + log.debug("loading cloud config") + init = stages.Init() + init.read_cfg() + init.fetch(existing="trust") + self.cloud = init.cloudify() + autoinstall_path = '/autoinstall.yaml' + if 'autoinstall' in self.cloud.cfg: + if not os.path.exists(autoinstall_path): + atomic_helper.write_file( + autoinstall_path, + safeyaml.dumps( + self.cloud.cfg['autoinstall']).encode('utf-8'), + mode=0o600) + if os.path.exists(autoinstall_path): + self.opts.autoinstall = autoinstall_path + else: + log.debug( + "cloud-init status: %r, assumed disabled", + status_txt) + + def _user_has_password(self, username): + with open('/etc/shadow') as fp: + for line in fp: + if line.startswith(username + ":$"): + return True + return False + + def set_installer_password(self): + if self.cloud is None: + return + + passfile = self.state_path("installer-user-passwd") + + if os.path.exists(passfile): + with open(passfile) as fp: + contents = fp.read() + self.installer_user_passwd_kind = PasswordKind.KNOWN + self.installer_user_name, self.installer_user_passwd = \ + contents.split(':', 1) + return + + def use_passwd(passwd): + self.installer_user_passwd = passwd + self.installer_user_passwd_kind = PasswordKind.KNOWN + with open(passfile, 'w') as fp: + fp.write(self.installer_user_name + ':' + passwd) + + if self.opts.dry_run: + self.installer_user_name = os.environ['USER'] + use_passwd(rand_user_password()) + return + + (users, _groups) = ug_util.normalize_users_groups( + self.cloud.cfg, self.cloud.distro) + (username, _user_config) = ug_util.extract_default(users) + + self.installer_user_name = username + + if self._user_has_password(username): + # Was the password set to a random password by a version of + # cloud-init that records the username in the log? (This is the + # case we hit on upgrading the subiquity snap) + passwd = get_installer_password_from_cloudinit_log() + if passwd: + use_passwd(passwd) + else: + self.installer_user_passwd_kind = PasswordKind.UNKNOWN + elif not user_key_fingerprints(username): + passwd = rand_user_password() + cp = run_command('chpasswd', input=username + ':'+passwd+'\n') + if cp.returncode == 0: + use_passwd(passwd) + else: + log.info("setting installer password failed %s", cp) + self.installer_user_passwd_kind = PasswordKind.NONE + else: + self.installer_user_passwd_kind = PasswordKind.NONE + + async def start(self): + self.controllers.load_all() + await self.start_api_server() + self.update_state(ApplicationState.CLOUD_INIT_WAIT) + await self.wait_for_cloudinit() + self.set_installer_password() + self.load_autoinstall_config(only_early=True) + if self.autoinstall_config and self.controllers.Early.cmds: + stamp_file = self.state_path("early-commands") + if not os.path.exists(stamp_file): + self.update_state(ApplicationState.EARLY_COMMANDS) + # Just wait a second for any clients to get ready to print + # output. + await asyncio.sleep(1) + await self.controllers.Early.run() + open(stamp_file, 'w').close() + await asyncio.sleep(1) + self.load_autoinstall_config(only_early=False) + if self.autoinstall_config: + self.interactive = bool( + self.autoinstall_config.get('interactive-sections')) + else: + self.interactive = True + if not self.interactive and not self.opts.dry_run: + open('/run/casper-no-prompt', 'w').close() + self.load_serialized_state() + self.update_state(ApplicationState.WAITING) + await super().start() + 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 = [ + sys.executable, '-m', 'subiquity.cmd.server', + ] + sys.argv[1:] + os.execvp(cmdline[0], cmdline) + + def make_autoinstall(self): + config = {'version': 1} + for controller in self.controllers.instances: + controller_conf = controller.make_autoinstall() + if controller_conf: + config[controller.autoinstall_key] = controller_conf + return config From f42f1d133e64d95db1b92d07a3593b2dc41df1c4 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Sat, 7 Aug 2021 01:58:09 +0800 Subject: [PATCH 32/69] Reconfigure mode: WIP --- system_setup/ui/views/reconfigure.py | 118 +++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 system_setup/ui/views/reconfigure.py diff --git a/system_setup/ui/views/reconfigure.py b/system_setup/ui/views/reconfigure.py new file mode 100644 index 00000000..8b0ce09a --- /dev/null +++ b/system_setup/ui/views/reconfigure.py @@ -0,0 +1,118 @@ +""" Integration + +Integration provides user with options to set up integration configurations. + +""" +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 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) + + 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 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, + } + self.form = IntegrationForm(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(WSLConfiguration1Data( + 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 + )) From d217db86a20fb62c54614e64d55770e34c8a95c1 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Wed, 25 Aug 2021 10:52:22 +0800 Subject: [PATCH 33/69] Revert "system_setup: identity tui validation fix" This reverts commit 2f3dc90b7e565e9592fc27f9748e2ca1ae7c65f5. --- system_setup/ui/views/identity.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/system_setup/ui/views/identity.py b/system_setup/ui/views/identity.py index 4e5788f6..cb26adcb 100644 --- a/system_setup/ui/views/identity.py +++ b/system_setup/ui/views/identity.py @@ -50,18 +50,6 @@ class WSLIdentityView(BaseView): 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, From 6ba608b8f72ba69496291d15b8003761b8dfd7ab Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Wed, 25 Aug 2021 10:54:42 +0800 Subject: [PATCH 34/69] Use inheritance for IdentityForm The previous composition version had some flaws due to multiple instances of a given Field with different addresses when reinstalled in the parent class. This caused the field validation validate_ to not work as it was not taken the expected referenced values. Instead of duplicating the code, we use an inherited version, taking and overriding only our desired fields. We still need to derive WSLIdentityView from the base class instead of IdentityView, as this is what is instantiating the form. (WSLIdentityForm instead of IdentityForm). Co-authored-by: Jean-Baptiste Lallement --- system_setup/ui/views/identity.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/system_setup/ui/views/identity.py b/system_setup/ui/views/identity.py index cb26adcb..4e5788f6 100644 --- a/system_setup/ui/views/identity.py +++ b/system_setup/ui/views/identity.py @@ -50,6 +50,18 @@ class WSLIdentityView(BaseView): 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, From eec135af26c639033ab443f56387a454f232ffc1 Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Thu, 12 Aug 2021 20:01:44 +0800 Subject: [PATCH 35/69] system_setup: Pre fixes --- system_setup/ui/views/reconfigure.py | 118 --------------------------- 1 file changed, 118 deletions(-) delete mode 100644 system_setup/ui/views/reconfigure.py diff --git a/system_setup/ui/views/reconfigure.py b/system_setup/ui/views/reconfigure.py deleted file mode 100644 index 8b0ce09a..00000000 --- a/system_setup/ui/views/reconfigure.py +++ /dev/null @@ -1,118 +0,0 @@ -""" Integration - -Integration provides user with options to set up integration configurations. - -""" -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 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) - - 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 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, - } - self.form = IntegrationForm(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(WSLConfiguration1Data( - 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 - )) From 09e8bbb6dcf79671553dbbc2b84f4d33237c8528 Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Tue, 17 Aug 2021 21:50:29 +1200 Subject: [PATCH 36/69] update stuff for my last merge --- system_setup/models/system_server.py | 52 +++++----------------------- 1 file changed, 8 insertions(+), 44 deletions(-) diff --git a/system_setup/models/system_server.py b/system_setup/models/system_server.py index 05aecea3..3990edda 100644 --- a/system_setup/models/system_server.py +++ b/system_setup/models/system_server.py @@ -16,7 +16,9 @@ import asyncio import logging -from subiquity.models.subiquity import SubiquityModel +from subiquity.models.subiquity import ( + SubiquityModel, + ) from subiquitycore.utils import is_wsl @@ -47,34 +49,11 @@ class SystemSetupModel(SubiquityModel): target = '/' - # Models that will be used in WSL system setup - ALL_MODEL_NAMES = [ - "locale", - "identity", - "wslconf1", - ] - def __init__(self, root, reconfigure=False): - if reconfigure: - self.ALL_MODEL_NAMES = [ - "locale", - "wslconf2", - ] # Parent class init is not called to not load models we don't need. self.root = root self.is_wsl = is_wsl() - self.debconf_selections = None - self.filesystem = None - self.kernel = None - self.keyboard = None - self.mirror = None - self.network = None - self.proxy = None - self.snaplist = None - self.ssh = None - self.updates = None - self.packages = [] self.userdata = {} self.locale = LocaleModel() @@ -84,24 +63,9 @@ class SystemSetupModel(SubiquityModel): self.confirmation = asyncio.Event() - self._events = { - name: asyncio.Event() for name in self.ALL_MODEL_NAMES - } - self.install_events = { - self._events[name] for name in self.ALL_MODEL_NAMES - } + self._configured_names = set() + self._cur_install_model_names = set() + self._cur_postinstall_model_names = set() - def configured(self, model_name): - # We need to override the parent class as - # *_MODEL_NAMES are global variables in server.py - if model_name not in self.ALL_MODEL_NAMES: - return - self._events[model_name].set() - stage = 'install' - unconfigured = { - mn for mn in self.ALL_MODEL_NAMES - if not self._events[mn].is_set() - } - log.debug( - "model %s for %s is configured, to go %s", - model_name, stage, unconfigured) + def set_source_variant(self, variant): + pass From eae46aa50ae9579ac0274c78e0bbcec49ce361cf Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Wed, 25 Aug 2021 10:56:07 +0800 Subject: [PATCH 37/69] system_setup: fixes for merge (WIP) --- subiquity/server/controllers/timezone.py | 13 ++++-- system_setup/client/client.py | 1 - system_setup/models/system_server.py | 57 +++++++++++++++++++++--- system_setup/server/server.py | 5 +-- 4 files changed, 61 insertions(+), 15 deletions(-) diff --git a/subiquity/server/controllers/timezone.py b/subiquity/server/controllers/timezone.py index b3403003..9fc44784 100644 --- a/subiquity/server/controllers/timezone.py +++ b/subiquity/server/controllers/timezone.py @@ -19,20 +19,27 @@ 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') +TIMEDATECTLCMD = which('timedatectl') +SD_BOOTED = os.path.exists('/run/systemd/system') + def generate_possible_tzs(): special_keys = ['', 'geoip'] - tzcmd = ['timedatectl', 'list-timezones'] + if not TIMEDATECTLCMD or not SD_BOOTED: + return special_keys + tzcmd = [TIMEDATECTLCMD, 'list-timezones'] list_tz_out = subprocess.check_output(tzcmd, universal_newlines=True) real_tzs = list_tz_out.splitlines() return special_keys + real_tzs def timedatectl_settz(app, tz): - tzcmd = ['timedatectl', 'set-timezone', tz] + tzcmd = [TIMEDATECTLCMD, 'set-timezone', tz] if app.opts.dry_run: tzcmd = ['sleep', str(1/app.scale_factor)] @@ -44,7 +51,7 @@ def timedatectl_settz(app, tz): def timedatectl_gettz(): # timedatectl show would be easier, but isn't on bionic - tzcmd = ['timedatectl', 'status'] + tzcmd = [TIMEDATECTLCMD, 'status'] env = {'LC_ALL': 'C'} # ... # Time zone: America/Denver (MDT, -0600) diff --git a/system_setup/client/client.py b/system_setup/client/client.py index 39d324da..3143c2a2 100644 --- a/system_setup/client/client.py +++ b/system_setup/client/client.py @@ -29,7 +29,6 @@ class SystemSetupClient(SubiquityClient): snapd_socket_path = None controllers = [ - # "Serial", "Welcome", "WSLIdentity", "Integration", diff --git a/system_setup/models/system_server.py b/system_setup/models/system_server.py index 3990edda..21ca3d7a 100644 --- a/system_setup/models/system_server.py +++ b/system_setup/models/system_server.py @@ -16,9 +16,7 @@ import asyncio import logging -from subiquity.models.subiquity import ( - SubiquityModel, - ) +from subiquity.models.subiquity import ModelNames, SubiquityModel from subiquitycore.utils import is_wsl @@ -49,7 +47,19 @@ class SystemSetupModel(SubiquityModel): target = '/' + # Models that will be used in WSL system setup + INSTALL_MODEL_NAMES = ModelNames({ + "locale", + "identity", + "wslconf1", + }) + def __init__(self, root, reconfigure=False): + if reconfigure: + self.INSTALL_MODEL_NAMES = ModelNames({ + "locale", + "wslconf2", + }) # Parent class init is not called to not load models we don't need. self.root = root self.is_wsl = is_wsl() @@ -61,11 +71,44 @@ class SystemSetupModel(SubiquityModel): self.wslconf1 = WSLConfiguration1Model() self.wslconf2 = WSLConfiguration2Model() - self.confirmation = asyncio.Event() + self._confirmation = asyncio.Event() + self._confirmation_task = None self._configured_names = set() - self._cur_install_model_names = set() - self._cur_postinstall_model_names = set() + self._install_model_names = self.INSTALL_MODEL_NAMES + self._postinstall_model_names = None + self._cur_install_model_names = self.INSTALL_MODEL_NAMES.default_names + self._cur_postinstall_model_names = None + self._install_event = asyncio.Event() + self._postinstall_event = asyncio.Event() def set_source_variant(self, variant): - pass + self._cur_install_model_names = \ + self._install_model_names.for_variant(variant) + if self._cur_postinstall_model_names is not None: + self._cur_postinstall_model_names = \ + self._postinstall_model_names.for_variant(variant) + unconfigured_install_model_names = \ + self._cur_install_model_names - self._configured_names + if unconfigured_install_model_names: + if self._install_event.is_set(): + self._install_event = asyncio.Event() + if self._confirmation_task is not None: + self._confirmation_task.cancel() + else: + self._install_event.set() + + def configured(self, model_name): + self._configured_names.add(model_name) + if model_name in self._cur_install_model_names: + stage = 'install' + names = self._cur_install_model_names + event = self._install_event + else: + return + unconfigured = names - self._configured_names + log.debug( + "model %s for %s stage is configured, to go %s", + model_name, stage, unconfigured) + if not unconfigured: + event.set() diff --git a/system_setup/server/server.py b/system_setup/server/server.py index 77221757..07acbef8 100644 --- a/system_setup/server/server.py +++ b/system_setup/server/server.py @@ -43,7 +43,7 @@ from subiquitycore.ssh import user_key_fingerprints from subiquitycore.utils import arun_command, run_command from system_setup.models.system_server import SystemSetupModel -log = logging.getLogger('subiquity.server.server') +log = logging.getLogger('system_setup.server.server') NOPROBERARG = "NOPROBER" @@ -106,8 +106,6 @@ class SystemSetupServer(Application): self.context.child("ErrorReporter"), self.opts.dry_run, self.root) self.prober = None self.kernel_cmdline = shlex.split(opts.kernel_cmdline) - self.controllers.remove("Refresh") - self.controllers.remove("SnapList") self.snapd = None self.note_data_for_apport("SnapUpdated", str(self.updated)) self.event_listeners = [] @@ -123,7 +121,6 @@ class SystemSetupServer(Application): "Locale", "WSLConfiguration2", ] - super().__init__(opts, block_log_dir) def make_model(self): root = '/' From 8c3f0bc26467e89ee45381a19bbf5f3b2d68aa46 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Tue, 31 Aug 2021 12:03:01 +0200 Subject: [PATCH 38/69] Inherit from base server Inherit from the base server instead of reimplementing it completely and override the methods needed for WSL. This removes lots of code duplication. --- system_setup/server/server.py | 384 +--------------------------------- 1 file changed, 4 insertions(+), 380 deletions(-) diff --git a/system_setup/server/server.py b/system_setup/server/server.py index 07acbef8..571d0e6d 100644 --- a/system_setup/server/server.py +++ b/system_setup/server/server.py @@ -13,65 +13,13 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import asyncio -import logging -import os -import shlex -import sys -import time - -import jsonschema -import yaml -from aiohttp import web -from cloudinit import atomic_helper, safeyaml, stages -from cloudinit.config.cc_set_passwords import rand_user_password -from cloudinit.distros import ug_util -from subiquity.common.api.server import bind, controller_for_request -from subiquity.common.apidef import API -from subiquity.common.errorreport import ErrorReporter -from subiquity.common.serialize import to_json -from subiquity.common.types import (ApplicationState, ErrorReportKind, - ErrorReportRef, PasswordKind) -from subiquity.server.controller import SubiquityController -from subiquity.server.errors import ErrorController -from subiquity.server.server import (MetaController, - get_installer_password_from_cloudinit_log) -from subiquitycore.async_helpers import run_in_thread -from subiquitycore.context import with_context -from subiquitycore.core import Application -from subiquitycore.ssh import user_key_fingerprints -from subiquitycore.utils import arun_command, run_command +from subiquity.server.server import SubiquityServer from system_setup.models.system_server import SystemSetupModel - -log = logging.getLogger('system_setup.server.server') - -NOPROBERARG = "NOPROBER" +import os -class SystemSetupServer(Application): - ''' - Server for the System Setup. +class SystemSetupServer(SubiquityServer): - No longer using old method because it keep inheriting incompatible - classes subiquity.server, which is not what we want for System Setup. - ''' - - snapd_socket_path = '/run/snapd.socket' - - base_schema = { - 'type': 'object', - 'properties': { - 'version': { - 'type': 'integer', - 'minimum': 1, - 'maximum': 1, - }, - }, - 'required': ['version'], - 'additionalProperties': True, - } - - project = "subiquity" from system_setup.server import controllers as controllers_mod controllers = [ "Reporting", @@ -84,35 +32,6 @@ class SystemSetupServer(Application): ] def __init__(self, opts, block_log_dir): - super().__init__(opts) - self.block_log_dir = block_log_dir - self.cloud = None - self.cloud_init_ok = None - self._state = ApplicationState.STARTING_UP - self.state_event = asyncio.Event() - self.interactive = None - self.confirming_tty = '' - self.fatal_error = None - self.running_error_commands = False - self.installer_user_name = None - self.installer_user_passwd_kind = PasswordKind.NONE - self.installer_user_passwd = None - - self.echo_syslog_id = 'subiquity_echo.{}'.format(os.getpid()) - self.event_syslog_id = 'subiquity_event.{}'.format(os.getpid()) - self.log_syslog_id = 'subiquity_log.{}'.format(os.getpid()) - - self.error_reporter = ErrorReporter( - self.context.child("ErrorReporter"), self.opts.dry_run, self.root) - self.prober = None - self.kernel_cmdline = shlex.split(opts.kernel_cmdline) - self.snapd = None - self.note_data_for_apport("SnapUpdated", str(self.updated)) - self.event_listeners = [] - self.autoinstall_config = None - self.hub.subscribe('network-up', self._network_change) - self.hub.subscribe('network-proxy-set', self._proxy_set) - self.geoip = None self.is_reconfig = opts.reconfigure if self.is_reconfig and not opts.dry_run: self.controllers = [ @@ -121,305 +40,10 @@ class SystemSetupServer(Application): "Locale", "WSLConfiguration2", ] + super().__init__(opts, block_log_dir) def make_model(self): root = '/' if self.opts.dry_run: root = os.path.abspath('.subiquity') return SystemSetupModel(root, self.is_reconfig) - - def load_serialized_state(self): - for controller in self.controllers.instances: - controller.load_state() - - def add_event_listener(self, listener): - self.event_listeners.append(listener) - - def _maybe_push_to_journal(self, event_type, context, description): - pass - - def report_start_event(self, context, description): - for listener in self.event_listeners: - listener.report_start_event(context, description) - self._maybe_push_to_journal('start', context, description) - - def report_finish_event(self, context, description, status): - for listener in self.event_listeners: - listener.report_finish_event(context, description, status) - self._maybe_push_to_journal('finish', context, description) - - @property - def state(self): - return self._state - - def update_state(self, state): - self._state = state - self.state_event.set() - self.state_event.clear() - - def note_file_for_apport(self, key, path): - self.error_reporter.note_file_for_apport(key, path) - - def note_data_for_apport(self, key, value): - self.error_reporter.note_data_for_apport(key, value) - - def make_apport_report(self, kind, thing, *, wait=False, **kw): - return self.error_reporter.make_apport_report( - kind, thing, wait=wait, **kw) - - async def _run_error_cmds(self, report): - await report._info_task - Error = getattr(self.controllers, "Error", None) - if Error is not None and Error.cmds: - try: - await Error.run() - except Exception: - log.exception("running error-commands failed") - if not self.interactive: - self.update_state(ApplicationState.ERROR) - - def _exception_handler(self, loop, context): - exc = context.get('exception') - if exc is None: - super()._exception_handler(loop, context) - return - report = self.error_reporter.report_for_exc(exc) - log.error("top level error", exc_info=exc) - if not report: - report = self.make_apport_report( - ErrorReportKind.UNKNOWN, "unknown error", - exc=exc) - self.fatal_error = report - if self.interactive: - self.update_state(ApplicationState.ERROR) - if not self.running_error_commands: - self.running_error_commands = True - self.aio_loop.create_task(self._run_error_cmds(report)) - - @web.middleware - async def middleware(self, request, handler): - override_status = None - controller = await controller_for_request(request) - if isinstance(controller, SubiquityController): - if not controller.interactive(): - override_status = 'skip' - elif self.state == ApplicationState.NEEDS_CONFIRMATION: - if self.base_model.is_postinstall_only(controller.model_name): - override_status = 'confirm' - if override_status is not None: - resp = web.Response(headers={'x-status': override_status}) - else: - resp = await handler(request) - if self.updated: - resp.headers['x-updated'] = 'yes' - else: - resp.headers['x-updated'] = 'no' - if resp.get('exception'): - exc = resp['exception'] - log.debug( - 'request to {} crashed'.format(request.raw_path), exc_info=exc) - report = self.make_apport_report( - ErrorReportKind.SERVER_REQUEST_FAIL, - "request to {}".format(request.raw_path), - exc=exc) - resp.headers['x-error-report'] = to_json( - ErrorReportRef, report.ref()) - return resp - - @with_context() - async def apply_autoinstall_config(self, context): - for controller in self.controllers.instances: - if controller.interactive(): - log.debug( - "apply_autoinstall_config: skipping %s as interactive", - controller.name) - continue - await controller.apply_autoinstall_config() - controller.configured() - - def load_autoinstall_config(self, *, only_early): - log.debug("load_autoinstall_config only_early %s", only_early) - if self.opts.autoinstall is None: - return - with open(self.opts.autoinstall) as fp: - self.autoinstall_config = yaml.safe_load(fp) - if only_early: - self.controllers.Reporting.setup_autoinstall() - self.controllers.Reporting.start() - self.controllers.Error.setup_autoinstall() - with self.context.child("core_validation", level="INFO"): - jsonschema.validate(self.autoinstall_config, self.base_schema) - self.controllers.Early.setup_autoinstall() - else: - for controller in self.controllers.instances: - controller.setup_autoinstall() - - async def start_api_server(self): - app = web.Application(middlewares=[self.middleware]) - bind(app.router, API.meta, MetaController(self)) - bind(app.router, API.errors, ErrorController(self)) - if self.opts.dry_run: - from subiquity.server.dryrun import DryRunController - bind(app.router, API.dry_run, DryRunController(self)) - for controller in self.controllers.instances: - controller.add_routes(app) - runner = web.AppRunner(app, keepalive_timeout=0xffffffff) - await runner.setup() - site = web.UnixSite(runner, self.opts.socket) - await site.start() - # It is intended that a non-root client can connect. - os.chmod(self.opts.socket, 0o666) - - async def wait_for_cloudinit(self): - if self.opts.dry_run: - self.cloud_init_ok = True - return - ci_start = time.time() - status_coro = arun_command(["cloud-init", "status", "--wait"]) - try: - status_cp = await asyncio.wait_for(status_coro, 600) - except asyncio.CancelledError: - status_txt = '' - self.cloud_init_ok = False - else: - status_txt = status_cp.stdout - self.cloud_init_ok = True - log.debug("waited %ss for cloud-init", time.time() - ci_start) - if "status: done" in status_txt: - log.debug("loading cloud config") - init = stages.Init() - init.read_cfg() - init.fetch(existing="trust") - self.cloud = init.cloudify() - autoinstall_path = '/autoinstall.yaml' - if 'autoinstall' in self.cloud.cfg: - if not os.path.exists(autoinstall_path): - atomic_helper.write_file( - autoinstall_path, - safeyaml.dumps( - self.cloud.cfg['autoinstall']).encode('utf-8'), - mode=0o600) - if os.path.exists(autoinstall_path): - self.opts.autoinstall = autoinstall_path - else: - log.debug( - "cloud-init status: %r, assumed disabled", - status_txt) - - def _user_has_password(self, username): - with open('/etc/shadow') as fp: - for line in fp: - if line.startswith(username + ":$"): - return True - return False - - def set_installer_password(self): - if self.cloud is None: - return - - passfile = self.state_path("installer-user-passwd") - - if os.path.exists(passfile): - with open(passfile) as fp: - contents = fp.read() - self.installer_user_passwd_kind = PasswordKind.KNOWN - self.installer_user_name, self.installer_user_passwd = \ - contents.split(':', 1) - return - - def use_passwd(passwd): - self.installer_user_passwd = passwd - self.installer_user_passwd_kind = PasswordKind.KNOWN - with open(passfile, 'w') as fp: - fp.write(self.installer_user_name + ':' + passwd) - - if self.opts.dry_run: - self.installer_user_name = os.environ['USER'] - use_passwd(rand_user_password()) - return - - (users, _groups) = ug_util.normalize_users_groups( - self.cloud.cfg, self.cloud.distro) - (username, _user_config) = ug_util.extract_default(users) - - self.installer_user_name = username - - if self._user_has_password(username): - # Was the password set to a random password by a version of - # cloud-init that records the username in the log? (This is the - # case we hit on upgrading the subiquity snap) - passwd = get_installer_password_from_cloudinit_log() - if passwd: - use_passwd(passwd) - else: - self.installer_user_passwd_kind = PasswordKind.UNKNOWN - elif not user_key_fingerprints(username): - passwd = rand_user_password() - cp = run_command('chpasswd', input=username + ':'+passwd+'\n') - if cp.returncode == 0: - use_passwd(passwd) - else: - log.info("setting installer password failed %s", cp) - self.installer_user_passwd_kind = PasswordKind.NONE - else: - self.installer_user_passwd_kind = PasswordKind.NONE - - async def start(self): - self.controllers.load_all() - await self.start_api_server() - self.update_state(ApplicationState.CLOUD_INIT_WAIT) - await self.wait_for_cloudinit() - self.set_installer_password() - self.load_autoinstall_config(only_early=True) - if self.autoinstall_config and self.controllers.Early.cmds: - stamp_file = self.state_path("early-commands") - if not os.path.exists(stamp_file): - self.update_state(ApplicationState.EARLY_COMMANDS) - # Just wait a second for any clients to get ready to print - # output. - await asyncio.sleep(1) - await self.controllers.Early.run() - open(stamp_file, 'w').close() - await asyncio.sleep(1) - self.load_autoinstall_config(only_early=False) - if self.autoinstall_config: - self.interactive = bool( - self.autoinstall_config.get('interactive-sections')) - else: - self.interactive = True - if not self.interactive and not self.opts.dry_run: - open('/run/casper-no-prompt', 'w').close() - self.load_serialized_state() - self.update_state(ApplicationState.WAITING) - await super().start() - 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 = [ - sys.executable, '-m', 'subiquity.cmd.server', - ] + sys.argv[1:] - os.execvp(cmdline[0], cmdline) - - def make_autoinstall(self): - config = {'version': 1} - for controller in self.controllers.instances: - controller_conf = controller.make_autoinstall() - if controller_conf: - config[controller.autoinstall_key] = controller_conf - return config From 688af116f56dd6f07d76ffaf23552e99c9ec78b8 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Tue, 31 Aug 2021 12:07:20 +0200 Subject: [PATCH 39/69] Added shutdown and configure controller for the WSL server This is the WSL implementation of the Install and Shutdown controllers that inherit from the base controllers. --- system_setup/server/controllers/__init__.py | 4 ++ system_setup/server/controllers/configure.py | 75 ++++++++++++++++++++ system_setup/server/controllers/shutdown.py | 57 +++++++++++++++ system_setup/server/server.py | 6 ++ 4 files changed, 142 insertions(+) create mode 100644 system_setup/server/controllers/configure.py create mode 100644 system_setup/server/controllers/shutdown.py diff --git a/system_setup/server/controllers/__init__.py b/system_setup/server/controllers/__init__.py index 3c52b268..0be51cc4 100644 --- a/system_setup/server/controllers/__init__.py +++ b/system_setup/server/controllers/__init__.py @@ -24,6 +24,8 @@ from subiquity.server.controllers.userdata import UserdataController from .identity import IdentityController from .wslconf1 import WSLConfiguration1Controller from .wslconf2 import WSLConfiguration2Controller +from .configure import ConfigureController +from .shutdown import SetupShutdownController __all__ = [ 'EarlyController', @@ -32,7 +34,9 @@ __all__ = [ 'LateController', 'LocaleController', 'ReportingController', + 'SetupShutdownController', 'UserdataController', "WSLConfiguration1Controller", "WSLConfiguration2Controller", + 'ConfigureController', ] diff --git a/system_setup/server/controllers/configure.py b/system_setup/server/controllers/configure.py new file mode 100644 index 00000000..0463954f --- /dev/null +++ b/system_setup/server/controllers/configure.py @@ -0,0 +1,75 @@ +# 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 +import re + +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-configure-context', True) + try: + + self.app.update_state(ApplicationState.WAITING) + + self.app.update_state(ApplicationState.NEEDS_CONFIRMATION) + + self.app.update_state(ApplicationState.RUNNING) + + 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 + # 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 \ No newline at end of file diff --git a/system_setup/server/controllers/shutdown.py b/system_setup/server/controllers/shutdown.py new file mode 100644 index 00000000..c8e13aa6 --- /dev/null +++ b/system_setup/server/controllers/shutdown.py @@ -0,0 +1,57 @@ +# 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 asyncio +import logging +import os +import platform +import subprocess + +from subiquitycore.context import with_context +from subiquitycore.utils import arun_command, run_command + +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 self.opts.dry_run: + self.app.exit() + else: + 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") diff --git a/system_setup/server/server.py b/system_setup/server/server.py index 571d0e6d..1af911e8 100644 --- a/system_setup/server/server.py +++ b/system_setup/server/server.py @@ -29,6 +29,9 @@ class SystemSetupServer(SubiquityServer): "Identity", "WSLConfiguration1", "WSLConfiguration2", + "Configure", + "SetupShutdown", + "Late", ] def __init__(self, opts, block_log_dir): @@ -39,6 +42,9 @@ class SystemSetupServer(SubiquityServer): "Error", "Locale", "WSLConfiguration2", + "Configure", + "SetupShutdown", + "Late", ] super().__init__(opts, block_log_dir) From f41f44e9d671ebd1d659bdf2d44b595067350982 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Tue, 31 Aug 2021 12:09:15 +0200 Subject: [PATCH 40/69] Misc cleanups * Rename/add debug with correct naming * Remove uneeded and unused hostname in identity * Add automatic answer support to overview --- system_setup/client/controllers/overview.py | 7 ++++++- system_setup/server/controllers/identity.py | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/system_setup/client/controllers/overview.py b/system_setup/client/controllers/overview.py index 029b95dd..eb40b5bf 100644 --- a/system_setup/client/controllers/overview.py +++ b/system_setup/client/controllers/overview.py @@ -3,7 +3,7 @@ from subiquity.client.controller import SubiquityTuiController from system_setup.ui.views.overview import OverviewView -log = logging.getLogger('ubuntu_wsl_oobe.controllers.identity') +log = logging.getLogger('ubuntu_wsl_oobe.controllers.overview') class OverviewController(SubiquityTuiController): @@ -14,5 +14,10 @@ class OverviewController(SubiquityTuiController): def cancel(self): self.app.cancel() + def run_answers(self): + self.done(None) + def done(self, result): + log.debug( + "OverviewController.done next_screen") self.app.next_screen() diff --git a/system_setup/server/controllers/identity.py b/system_setup/server/controllers/identity.py index 2935170b..16dd98cc 100644 --- a/system_setup/server/controllers/identity.py +++ b/system_setup/server/controllers/identity.py @@ -30,7 +30,6 @@ class WSLIdentityController(IdentityController): 'properties': { 'realname': {'type': 'string'}, 'username': {'type': 'string'}, - 'hostname': {'type': 'string'}, 'password': {'type': 'string'}, }, 'required': ['username', 'password'], From b0299f255f6f8e0e363397d01efd6740af7d2cb0 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Tue, 31 Aug 2021 12:10:25 +0200 Subject: [PATCH 41/69] Added integration test for WSL This tests covers the configuration of WSL on first boot. --- examples/answers-system-setup.yaml | 18 ++++++++++ scripts/runtests.sh | 57 +++++++++++++++++++----------- 2 files changed, 54 insertions(+), 21 deletions(-) create mode 100644 examples/answers-system-setup.yaml diff --git a/examples/answers-system-setup.yaml b/examples/answers-system-setup.yaml new file mode 100644 index 00000000..d22b9d49 --- /dev/null +++ b/examples/answers-system-setup.yaml @@ -0,0 +1,18 @@ +Welcome: + lang: en_US +WSLIdentity: + realname: Ubuntu + username: ubuntu + # ubuntu + password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' +Integration: + custom_path: '/custom_mnt_path' + custom_mount_opt: 'opt1 opt2 opt3' + gen_host: false + gen_resolvconf: false +Overview: + noproperty: "there is no property for this view, just a done button but subiquity requires something to proceed" +InstallProgress: + reboot: yes + + 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 From 57a2212c7b6bc954d36aa6e111509fb34c95356d Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Tue, 31 Aug 2021 12:11:32 +0200 Subject: [PATCH 42/69] List of TODOs This is the list of identified TODOs for the OOBE. --- subiquity/common/types.py | 1 + system_setup/client/client.py | 4 ++++ system_setup/client/controllers/identity.py | 2 ++ system_setup/client/controllers/integration.py | 1 + system_setup/cmd/tui.py | 3 +++ system_setup/models/wslconf1.py | 2 ++ system_setup/models/wslconf2.py | 3 +++ system_setup/server/controllers/wslconf2.py | 2 +- system_setup/server/server.py | 2 ++ system_setup/ui/views/integration.py | 4 ++++ system_setup/ui/views/overview.py | 2 ++ 11 files changed, 25 insertions(+), 1 deletion(-) diff --git a/subiquity/common/types.py b/subiquity/common/types.py index e0818680..f9917dc2 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -355,6 +355,7 @@ class WSLConfiguration2Data: 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 WSLConfiguration1Data custom_path: str = attr.ib(default='/mnt/') custom_mount_opt: str = '' gen_host: bool = attr.ib(default=True) diff --git a/system_setup/client/client.py b/system_setup/client/client.py index 3143c2a2..07addc29 100644 --- a/system_setup/client/client.py +++ b/system_setup/client/client.py @@ -37,6 +37,10 @@ class SystemSetupClient(SubiquityClient): ] 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 if opts.reconfigure: self.controllers = [ "Welcome", diff --git a/system_setup/client/controllers/identity.py b/system_setup/client/controllers/identity.py index c4231189..b158d665 100644 --- a/system_setup/client/controllers/identity.py +++ b/system_setup/client/controllers/identity.py @@ -43,6 +43,8 @@ class WSLIdentityController(IdentityController): identity_data) if not self.opts.dry_run: username = identity_data.username + # TODO WSL: remove this as a way to pass the values to the backend and keep that in memory + # Then, remove the dry_run condition. with open('/var/run/ubuntu_wsl_oobe_assigned_account', 'w') as f: f.write(username) self.app.next_screen(self.endpoint.POST(identity_data)) diff --git a/system_setup/client/controllers/integration.py b/system_setup/client/controllers/integration.py index 223f7ae5..46e40113 100644 --- a/system_setup/client/controllers/integration.py +++ b/system_setup/client/controllers/integration.py @@ -6,6 +6,7 @@ from system_setup.ui.views.integration import IntegrationView log = logging.getLogger('system_setup.client.controllers.integration') +# TODO WSL: rename Integration to something else and change endpoint name class IntegrationController(SubiquityTuiController): endpoint_name = 'wslconf1' diff --git a/system_setup/cmd/tui.py b/system_setup/cmd/tui.py index 0b143181..d2b8d206 100755 --- a/system_setup/cmd/tui.py +++ b/system_setup/cmd/tui.py @@ -36,6 +36,7 @@ class ClickAction(argparse.Action): 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') @@ -47,6 +48,7 @@ def make_client_args_parser(): 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', @@ -70,6 +72,7 @@ def make_client_args_parser(): 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) parser.add_argument('--reconfigure', action='store_true') return parser diff --git a/system_setup/models/wslconf1.py b/system_setup/models/wslconf1.py index 78ed7709..a9864caf 100644 --- a/system_setup/models/wslconf1.py +++ b/system_setup/models/wslconf1.py @@ -30,6 +30,7 @@ class WSLConfiguration1(object): gen_resolvconf = attr.ib() +# TODO WSL: remove from WSLConfiguration1Model to something more meaningful class WSLConfiguration1Model(object): """ Model representing integration """ @@ -44,6 +45,7 @@ class WSLConfiguration1Model(object): d['gen_host'] = result.gen_host d['gen_resolvconf'] = result.gen_resolvconf self._wslconf1 = WSLConfiguration1(**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"], diff --git a/system_setup/models/wslconf2.py b/system_setup/models/wslconf2.py index 3f846047..5688dba9 100644 --- a/system_setup/models/wslconf2.py +++ b/system_setup/models/wslconf2.py @@ -21,6 +21,8 @@ from subiquitycore.utils import run_command log = logging.getLogger('subiquity.models.wsl_integration_2') +# TODO WSL: Remove all attributes in wslconf1 +# TODO WSL: remove from WSLConfiguration1Model to something more meaningful @attr.s class WSLConfiguration2(object): @@ -66,6 +68,7 @@ class WSLConfiguration2Model(object): d['automount'] = result.automount d['mountfstab'] = result.mountfstab self._wslconf2 = WSLConfiguration2(**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"], diff --git a/system_setup/server/controllers/wslconf2.py b/system_setup/server/controllers/wslconf2.py index ce65f39b..1ccf29d0 100644 --- a/system_setup/server/controllers/wslconf2.py +++ b/system_setup/server/controllers/wslconf2.py @@ -26,7 +26,7 @@ from subiquity.server.controller import SubiquityController log = logging.getLogger('subiquity.server.controllers.wsl_integration_2') - +# TODO WSL: remove all duplicates from WSL config 1 controller class WSLConfiguration2Controller(SubiquityController): endpoint = API.wslconf2 diff --git a/system_setup/server/server.py b/system_setup/server/server.py index 1af911e8..560cb8a1 100644 --- a/system_setup/server/server.py +++ b/system_setup/server/server.py @@ -35,6 +35,8 @@ class SystemSetupServer(SubiquityServer): ] def __init__(self, opts, block_log_dir): + # TODO WSL: remove reconfigure argument parser option and check dynamically what needs to be presented. + # TODO WSL: we should have WSLConfiguration1 (renamed) here to show multiple pages. self.is_reconfig = opts.reconfigure if self.is_reconfig and not opts.dry_run: self.controllers = [ diff --git a/system_setup/ui/views/integration.py b/system_setup/ui/views/integration.py index aacb2af8..bd68c225 100644 --- a/system_setup/ui/views/integration.py +++ b/system_setup/ui/views/integration.py @@ -21,6 +21,10 @@ from subiquitycore.view import BaseView from subiquity.common.types import WSLConfiguration1Data +# TODO WSL: rename from "integration" to something more meaningful + +# TODO WSL: add another view for configure in another file + class MountEditor(StringEditor, WantsToKnowFormField): def keypress(self, size, key): ''' restrict what chars we allow for mountpoints ''' diff --git a/system_setup/ui/views/overview.py b/system_setup/ui/views/overview.py index b978c9ca..d93619bc 100644 --- a/system_setup/ui/views/overview.py +++ b/system_setup/ui/views/overview.py @@ -14,6 +14,7 @@ from subiquitycore.view import BaseView log = logging.getLogger("ubuntu_wsl_oobe.ui.views.overview") +# TODO WSL: remove this WSL_USERNAME_PATH = "/var/run/ubuntu_wsl_oobe_assigned_account" @@ -22,6 +23,7 @@ class OverviewView(BaseView): def __init__(self, controller): self.controller = controller + # TODO WSL: remove this and always use in memory value user_name = "dryrun_user" if os.path.isfile(WSL_USERNAME_PATH): with open(WSL_USERNAME_PATH, 'r') as f: From 41945cb84c7fbfd6ccdcf931d850de2a6950d8cc Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Tue, 31 Aug 2021 12:54:15 +0200 Subject: [PATCH 43/69] Renamed configuration pages In order to make naming more explicit renamed wslconf1 to wslconfbase and wslconf2 to wslconfadvanced. --- subiquity/common/apidef.py | 8 +-- subiquity/common/types.py | 6 +-- system_setup/client/client.py | 6 ++- system_setup/client/controllers/__init__.py | 8 +-- .../client/controllers/integration.py | 36 ------------- ...{reconfiguration.py => wslconfadvanced.py} | 16 +++--- .../client/controllers/wslconfbase.py | 34 ++++++++++++ system_setup/models/system_server.py | 13 ++--- .../{wslconf2.py => wslconfadvanced.py} | 20 +++---- .../models/{wslconf1.py => wslconfbase.py} | 20 +++---- system_setup/server/controllers/__init__.py | 8 +-- .../{wslconf2.py => wslconfadvanced.py} | 54 +++++++++---------- .../{wslconf1.py => wslconfbase.py} | 30 +++++------ system_setup/server/server.py | 8 +-- system_setup/ui/views/__init__.py | 8 +-- ...{reconfiguration.py => wslconfadvanced.py} | 52 +++++++++--------- .../views/{integration.py => wslconfbase.py} | 32 +++++------ 17 files changed, 178 insertions(+), 181 deletions(-) delete mode 100644 system_setup/client/controllers/integration.py rename system_setup/client/controllers/{reconfiguration.py => wslconfadvanced.py} (82%) create mode 100644 system_setup/client/controllers/wslconfbase.py rename system_setup/models/{wslconf2.py => wslconfadvanced.py} (91%) rename system_setup/models/{wslconf1.py => wslconfbase.py} (83%) rename system_setup/server/controllers/{wslconf2.py => wslconfadvanced.py} (78%) rename system_setup/server/controllers/{wslconf1.py => wslconfbase.py} (68%) rename system_setup/ui/views/{reconfiguration.py => wslconfadvanced.py} (83%) rename system_setup/ui/views/{integration.py => wslconfbase.py} (81%) diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py index e9f1fd4d..e8400bc4 100644 --- a/subiquity/common/apidef.py +++ b/subiquity/common/apidef.py @@ -47,8 +47,8 @@ from subiquity.common.types import ( TimeZoneInfo, WLANSupportInstallState, ZdevInfo, - WSLConfiguration1Data, - WSLConfiguration2Data, + WSLConfigurationBase, + WSLConfigurationAdvanced, ) @@ -61,8 +61,8 @@ class API: proxy = simple_endpoint(str) ssh = simple_endpoint(SSHData) updates = simple_endpoint(str) - wslconf1 = simple_endpoint(WSLConfiguration1Data) - wslconf2 = simple_endpoint(WSLConfiguration2Data) + 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 f9917dc2..4000e459 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -338,7 +338,7 @@ class ShutdownMode(enum.Enum): @attr.s(auto_attribs=True) -class WSLConfiguration1Data: +class WSLConfigurationBase: custom_path: str = attr.ib(default='/mnt/') custom_mount_opt: str = '' gen_host: bool = attr.ib(default=True) @@ -346,7 +346,7 @@ class WSLConfiguration1Data: @attr.s(auto_attribs=True) -class WSLConfiguration2Data: +class WSLConfigurationAdvanced: gui_theme: str = attr.ib(default='default') gui_followwintheme: bool = attr.ib(default=True) legacy_gui: bool = attr.ib(default=False) @@ -355,7 +355,7 @@ class WSLConfiguration2Data: 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 WSLConfiguration1Data + # 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) diff --git a/system_setup/client/client.py b/system_setup/client/client.py index 07addc29..ceba6969 100644 --- a/system_setup/client/client.py +++ b/system_setup/client/client.py @@ -31,7 +31,7 @@ class SystemSetupClient(SubiquityClient): controllers = [ "Welcome", "WSLIdentity", - "Integration", + "WSLConfigurationBase", "Overview", "Progress", ] @@ -41,10 +41,12 @@ class SystemSetupClient(SubiquityClient): # 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 if opts.reconfigure: self.controllers = [ "Welcome", - "Reconfiguration", + "WSLConfigurationAdvanced", "Progress", ] super().__init__(opts) diff --git a/system_setup/client/controllers/__init__.py b/system_setup/client/controllers/__init__.py index 79abf691..7ec05a12 100644 --- a/system_setup/client/controllers/__init__.py +++ b/system_setup/client/controllers/__init__.py @@ -15,9 +15,9 @@ from .identity import WSLIdentityController -from .integration import IntegrationController +from .wslconfbase import WSLConfigurationBaseController from .overview import OverviewController -from .reconfiguration import ReconfigurationController +from .wslconfadvanced import WSLConfigurationAdvancedController from subiquity.client.controllers import (ProgressController, WelcomeController) @@ -27,7 +27,7 @@ __all__ = [ 'WelcomeController', 'WSLIdentityController', 'ProgressController', - 'IntegrationController', + 'WSLConfigurationBaseController', + 'WSLConfigurationAdvancedController', 'OverviewController', - 'ReconfigurationController', ] diff --git a/system_setup/client/controllers/integration.py b/system_setup/client/controllers/integration.py deleted file mode 100644 index 46e40113..00000000 --- a/system_setup/client/controllers/integration.py +++ /dev/null @@ -1,36 +0,0 @@ -import logging - -from subiquity.client.controller import SubiquityTuiController -from subiquity.common.types import WSLConfiguration1Data -from system_setup.ui.views.integration import IntegrationView - -log = logging.getLogger('system_setup.client.controllers.integration') - -# TODO WSL: rename Integration to something else and change endpoint name - -class IntegrationController(SubiquityTuiController): - endpoint_name = 'wslconf1' - - async def make_ui(self): - data = await self.endpoint.GET() - return IntegrationView(self, data) - - def run_answers(self): - if all(elem in self.answers for elem in - ['custom_path', 'custom_mount_opt', - 'gen_host', 'gen_resolvconf']): - integration = WSLConfiguration1Data( - 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(integration) - - def done(self, integration_data): - log.debug( - "IntegrationController.done next_screen user_spec=%s", - integration_data) - self.app.next_screen(self.endpoint.POST(integration_data)) - - def cancel(self): - self.app.prev_screen() diff --git a/system_setup/client/controllers/reconfiguration.py b/system_setup/client/controllers/wslconfadvanced.py similarity index 82% rename from system_setup/client/controllers/reconfiguration.py rename to system_setup/client/controllers/wslconfadvanced.py index 8fba4021..21d1b57c 100644 --- a/system_setup/client/controllers/reconfiguration.py +++ b/system_setup/client/controllers/wslconfadvanced.py @@ -16,18 +16,18 @@ import logging from subiquity.client.controller import SubiquityTuiController -from subiquity.common.types import WSLConfiguration2Data -from system_setup.ui.views.reconfiguration import ReconfigurationView +from subiquity.common.types import WSLConfigurationAdvanced +from system_setup.ui.views.wslconfadvanced import WSLConfigurationAdvancedView -log = logging.getLogger('system_setup.client.controllers.reconfiguration') +log = logging.getLogger('system_setup.client.controllers.wslconfigurationadvanced') -class ReconfigurationController(SubiquityTuiController): - endpoint_name = 'wslconf2' +class WSLConfigurationAdvancedController(SubiquityTuiController): + endpoint_name = 'wslconfadvanced' async def make_ui(self): data = await self.endpoint.GET() - return ReconfigurationView(self, data) + return WSLConfigurationAdvancedView(self, data) def run_answers(self): if all(elem in self.answers for elem in @@ -37,7 +37,7 @@ class ReconfigurationController(SubiquityTuiController): 'gui_followwintheme', 'legacy_gui', 'legacy_audio', 'adv_ip_detect', 'wsl_motd_news', 'automount', 'mountfstab']): - reconfiguration = WSLConfiguration2Data( + reconfiguration = WSLConfigurationAdvanced( custom_path=self.answers['custom_path'], custom_mount_opt=self.answers['custom_mount_opt'], gen_host=self.answers['gen_host'], @@ -58,7 +58,7 @@ class ReconfigurationController(SubiquityTuiController): def done(self, reconf_data): log.debug( - "ConfigurationController.done next_screen user_spec=%s", + "WSLConfigurationAdvancedController.done next_screen user_spec=%s", reconf_data) self.app.next_screen(self.endpoint.POST(reconf_data)) diff --git a/system_setup/client/controllers/wslconfbase.py b/system_setup/client/controllers/wslconfbase.py new file mode 100644 index 00000000..87d15e07 --- /dev/null +++ b/system_setup/client/controllers/wslconfbase.py @@ -0,0 +1,34 @@ +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/models/system_server.py b/system_setup/models/system_server.py index 21ca3d7a..3413a458 100644 --- a/system_setup/models/system_server.py +++ b/system_setup/models/system_server.py @@ -23,8 +23,8 @@ from subiquitycore.utils import is_wsl from subiquity.models.locale import LocaleModel from subiquity.models.identity import IdentityModel -from .wslconf1 import WSLConfiguration1Model -from .wslconf2 import WSLConfiguration2Model +from .wslconfbase import WSLConfigurationBaseModel +from .wslconfadvanced import WSLConfigurationAdvancedModel log = logging.getLogger('system_setup.models.system_server') @@ -51,14 +51,15 @@ class SystemSetupModel(SubiquityModel): INSTALL_MODEL_NAMES = ModelNames({ "locale", "identity", - "wslconf1", + "wslconfbase", }) def __init__(self, root, reconfigure=False): + # TODO WSL: add base model here to prevent overlap if reconfigure: self.INSTALL_MODEL_NAMES = ModelNames({ "locale", - "wslconf2", + "wslconfadvanced", }) # Parent class init is not called to not load models we don't need. self.root = root @@ -68,8 +69,8 @@ class SystemSetupModel(SubiquityModel): self.userdata = {} self.locale = LocaleModel() self.identity = IdentityModel() - self.wslconf1 = WSLConfiguration1Model() - self.wslconf2 = WSLConfiguration2Model() + self.wslconfbase = WSLConfigurationBaseModel() + self.wslconfadvanced = WSLConfigurationAdvancedModel() self._confirmation = asyncio.Event() self._confirmation_task = None diff --git a/system_setup/models/wslconf2.py b/system_setup/models/wslconfadvanced.py similarity index 91% rename from system_setup/models/wslconf2.py rename to system_setup/models/wslconfadvanced.py index 5688dba9..8cb73591 100644 --- a/system_setup/models/wslconf2.py +++ b/system_setup/models/wslconfadvanced.py @@ -19,13 +19,12 @@ import attr from subiquitycore.utils import run_command -log = logging.getLogger('subiquity.models.wsl_integration_2') +log = logging.getLogger('subiquity.models.wsl_configuration_advanced') -# TODO WSL: Remove all attributes in wslconf1 -# TODO WSL: remove from WSLConfiguration1Model to something more meaningful +# TODO WSL: Remove all attributes in wslconfbase @attr.s -class WSLConfiguration2(object): +class WSLConfigurationAdvanced(object): gui_theme = attr.ib() gui_followwintheme = attr.ib() legacy_gui = attr.ib() @@ -42,12 +41,13 @@ class WSLConfiguration2(object): interop_appendwindowspath = attr.ib() -class WSLConfiguration2Model(object): +class WSLConfigurationAdvancedModel(object): """ Model representing integration """ def __init__(self): - self._wslconf2 = None + self._wslconfadvanced = None + # TODO WSL: Load settings from system def apply_settings(self, result, is_dry_run=False): d = {} @@ -67,7 +67,7 @@ class WSLConfiguration2Model(object): d['wsl_motd_news'] = result.wsl_motd_news d['automount'] = result.automount d['mountfstab'] = result.mountfstab - self._wslconf2 = WSLConfiguration2(**d) + 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 @@ -126,8 +126,8 @@ class WSLConfiguration2Model(object): stdout=subprocess.DEVNULL) @property - def wslconf2(self): - return self._wslconf2 + def wslconfadvanced(self): + return self._wslconfadvanced def __repr__(self): - return "".format(self.wslconf2) + return "".format(self.wslconfadvanced) diff --git a/system_setup/models/wslconf1.py b/system_setup/models/wslconfbase.py similarity index 83% rename from system_setup/models/wslconf1.py rename to system_setup/models/wslconfbase.py index a9864caf..cfd661b5 100644 --- a/system_setup/models/wslconf1.py +++ b/system_setup/models/wslconfbase.py @@ -19,24 +19,24 @@ import attr from subiquitycore.utils import run_command -log = logging.getLogger('subiquity.models.wsl_integration_1') +log = logging.getLogger('subiquity.models.wsl_configuration_base') @attr.s -class WSLConfiguration1(object): +class WSLConfigurationBase(object): custom_path = attr.ib() custom_mount_opt = attr.ib() gen_host = attr.ib() gen_resolvconf = attr.ib() -# TODO WSL: remove from WSLConfiguration1Model to something more meaningful -class WSLConfiguration1Model(object): - """ Model representing integration +class WSLConfigurationBaseModel(object): + """ Model representing basic wsl configuration """ def __init__(self): - self._wslconf1 = None + self._wslconfbase = None + # TODO WSL: Load settings from system def apply_settings(self, result, is_dry_run=False): d = {} @@ -44,7 +44,7 @@ class WSLConfiguration1Model(object): d['custom_mount_opt'] = result.custom_mount_opt d['gen_host'] = result.gen_host d['gen_resolvconf'] = result.gen_resolvconf - self._wslconf1 = WSLConfiguration1(**d) + 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 @@ -66,8 +66,8 @@ class WSLConfiguration1Model(object): stdout=subprocess.DEVNULL) @property - def wslconf1(self): - return self._wslconf1 + def wslconfbase(self): + return self._wslconfbase def __repr__(self): - return "".format(self.wslconf1) + return "".format(self.wslconfbase) diff --git a/system_setup/server/controllers/__init__.py b/system_setup/server/controllers/__init__.py index 0be51cc4..e75eec73 100644 --- a/system_setup/server/controllers/__init__.py +++ b/system_setup/server/controllers/__init__.py @@ -22,8 +22,8 @@ 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 .wslconf1 import WSLConfiguration1Controller -from .wslconf2 import WSLConfiguration2Controller +from .wslconfbase import WSLConfigurationBaseController +from .wslconfadvanced import WSLConfigurationAdvancedController from .configure import ConfigureController from .shutdown import SetupShutdownController @@ -36,7 +36,7 @@ __all__ = [ 'ReportingController', 'SetupShutdownController', 'UserdataController', - "WSLConfiguration1Controller", - "WSLConfiguration2Controller", + 'WSLConfigurationBaseController', + 'WSLConfigurationAdvancedController', 'ConfigureController', ] diff --git a/system_setup/server/controllers/wslconf2.py b/system_setup/server/controllers/wslconfadvanced.py similarity index 78% rename from system_setup/server/controllers/wslconf2.py rename to system_setup/server/controllers/wslconfadvanced.py index 1ccf29d0..222b69da 100644 --- a/system_setup/server/controllers/wslconf2.py +++ b/system_setup/server/controllers/wslconfadvanced.py @@ -21,17 +21,17 @@ import configparser from subiquitycore.context import with_context from subiquity.common.apidef import API -from subiquity.common.types import WSLConfiguration2Data +from subiquity.common.types import WSLConfigurationAdvanced from subiquity.server.controller import SubiquityController -log = logging.getLogger('subiquity.server.controllers.wsl_integration_2') +log = logging.getLogger('subiquity.server.controllers.wsl_configuration_advanced') -# TODO WSL: remove all duplicates from WSL config 1 controller -class WSLConfiguration2Controller(SubiquityController): +# TODO WSL: remove all duplicates from WSL config base controller +class WSLConfigurationAdvancedController(SubiquityController): - endpoint = API.wslconf2 + endpoint = API.wslconfadvanced - autoinstall_key = model_name = "wslconf2" + autoinstall_key = model_name = "wslconfadvanced" autoinstall_schema = { 'type': 'object', 'properties': { @@ -115,7 +115,7 @@ class WSLConfiguration2Controller(SubiquityController): if data: def bool_converter(x): return x == 'true' - reconf_data = WSLConfiguration2Data( + reconf_data = WSLConfigurationAdvanced( custom_path=data['custom_path'], custom_mount_opt=data['custom_mount_opt'], gen_host=bool_converter(data['gen_host']), @@ -136,7 +136,7 @@ class WSLConfiguration2Controller(SubiquityController): def load_autoinstall_data(self, data): if data is not None: - reconf_data = WSLConfiguration2Data( + reconf_data = WSLConfigurationAdvanced( custom_path=data['custom_path'], custom_mount_opt=data['custom_mount_opt'], gen_host=data['gen_host'], @@ -159,29 +159,29 @@ class WSLConfiguration2Controller(SubiquityController): pass def make_autoinstall(self): - r = attr.asdict(self.model.wslconf2) + r = attr.asdict(self.model.wslconfadvanced) return r - async def GET(self) -> WSLConfiguration2Data: - data = WSLConfiguration2Data() - if self.model.wslconf2 is not None: - data.custom_path = self.model.wslconf2.custom_path - data.custom_mount_opt = self.model.wslconf2.custom_mount_opt - data.gen_host = self.model.wslconf2.gen_host - data.gen_resolvconf = self.model.wslconf2.gen_resolvconf - data.interop_enabled = self.model.wslconf2.interop_enabled + 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.wslconf2.interop_appendwindowspath - data.gui_theme = self.model.wslconf2.gui_theme - data.gui_followwintheme = self.model.wslconf2.gui_followwintheme - data.legacy_gui = self.model.wslconf2.legacy_gui - data.legacy_audio = self.model.wslconf2.legacy_audio - data.adv_ip_detect = self.model.wslconf2.adv_ip_detect - data.wsl_motd_news = self.model.wslconf2.wsl_motd_news - data.automount = self.model.wslconf2.automount - data.mountfstab = self.model.wslconf2.mountfstab + 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: WSLConfiguration2Data): + async def POST(self, data: WSLConfigurationAdvanced): self.model.apply_settings(data, self.opts.dry_run) self.configured() diff --git a/system_setup/server/controllers/wslconf1.py b/system_setup/server/controllers/wslconfbase.py similarity index 68% rename from system_setup/server/controllers/wslconf1.py rename to system_setup/server/controllers/wslconfbase.py index 4529cbcf..41f90350 100644 --- a/system_setup/server/controllers/wslconf1.py +++ b/system_setup/server/controllers/wslconfbase.py @@ -20,17 +20,17 @@ import attr from subiquitycore.context import with_context from subiquity.common.apidef import API -from subiquity.common.types import WSLConfiguration1Data +from subiquity.common.types import WSLConfigurationBase from subiquity.server.controller import SubiquityController -log = logging.getLogger('subiquity.server.controllers.wsl_integration_1') +log = logging.getLogger('subiquity.server.controllers.wsl_configuration_base') -class WSLConfiguration1Controller(SubiquityController): +class WSLConfigurationBaseController(SubiquityController): - endpoint = API.wslconf1 + endpoint = API.wslconfbase - autoinstall_key = model_name = "wslconf1" + autoinstall_key = model_name = "wslconfbase" autoinstall_schema = { 'type': 'object', 'properties': { @@ -45,7 +45,7 @@ class WSLConfiguration1Controller(SubiquityController): def load_autoinstall_data(self, data): if data is not None: - identity_data = WSLConfiguration1Data( + identity_data = WSLConfigurationBase( custom_path=data['custom_path'], custom_mount_opt=data['custom_mount_opt'], gen_host=data['gen_host'], @@ -58,18 +58,18 @@ class WSLConfiguration1Controller(SubiquityController): pass def make_autoinstall(self): - r = attr.asdict(self.model.wslconf1) + r = attr.asdict(self.model.wslconfbase) return r - async def GET(self) -> WSLConfiguration1Data: - data = WSLConfiguration1Data() - if self.model.wslconf1 is not None: - data.custom_path = self.model.wslconf1.custom_path - data.custom_mount_opt = self.model.wslconf1.custom_mount_opt - data.gen_host = self.model.wslconf1.gen_host - data.gen_resolvconf = self.model.wslconf1.gen_resolvconf + 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: WSLConfiguration1Data): + 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 index 560cb8a1..d49df6d5 100644 --- a/system_setup/server/server.py +++ b/system_setup/server/server.py @@ -27,8 +27,8 @@ class SystemSetupServer(SubiquityServer): "Userdata", "Locale", "Identity", - "WSLConfiguration1", - "WSLConfiguration2", + "WSLConfigurationBase", + "WSLConfigurationAdvanced", "Configure", "SetupShutdown", "Late", @@ -36,14 +36,14 @@ class SystemSetupServer(SubiquityServer): def __init__(self, opts, block_log_dir): # TODO WSL: remove reconfigure argument parser option and check dynamically what needs to be presented. - # TODO WSL: we should have WSLConfiguration1 (renamed) here to show multiple pages. + # TODO WSL: we should have WSLConfigurationBase here to show multiple pages. self.is_reconfig = opts.reconfigure if self.is_reconfig and not opts.dry_run: self.controllers = [ "Reporting", "Error", "Locale", - "WSLConfiguration2", + "WSLConfigurationAdvanced", "Configure", "SetupShutdown", "Late", diff --git a/system_setup/ui/views/__init__.py b/system_setup/ui/views/__init__.py index 81db478e..6ae62de7 100644 --- a/system_setup/ui/views/__init__.py +++ b/system_setup/ui/views/__init__.py @@ -14,13 +14,13 @@ # along with this program. If not, see . from .identity import WSLIdentityView -from .integration import IntegrationView +from .wslconfbase import WSLConfigurationBaseView +from .wslconfadvanced import WSLConfigurationAdvancedView from .overview import OverviewView -from .reconfiguration import ReconfigurationView __all__ = [ 'WSLIdentityView', - 'IntegrationView', + 'WSLConfigurationBaseView', + 'WSLConfigurationAdvancedView', 'OverviewView', - 'ReconfigurationView', ] diff --git a/system_setup/ui/views/reconfiguration.py b/system_setup/ui/views/wslconfadvanced.py similarity index 83% rename from system_setup/ui/views/reconfiguration.py rename to system_setup/ui/views/wslconfadvanced.py index a85320b0..483a2bec 100644 --- a/system_setup/ui/views/reconfiguration.py +++ b/system_setup/ui/views/wslconfadvanced.py @@ -1,6 +1,6 @@ -""" Reconfiguration View +""" WSLConfigurationAdvanced View -Integration provides user with options to set up integration configurations. +WSLConfigurationAdvanced provides user with options with additional settings for advanced configuration. """ import re @@ -19,7 +19,7 @@ from subiquitycore.ui.form import ( from subiquitycore.ui.interactive import StringEditor from subiquitycore.ui.utils import screen from subiquitycore.view import BaseView -from subiquity.common.types import WSLConfiguration2Data +from subiquity.common.types import WSLConfigurationAdvanced class MountEditor(StringEditor, WantsToKnowFormField): @@ -37,12 +37,12 @@ MountField = simple_field(MountEditor) StringField = simple_field(StringEditor) -class ReconfigurationForm(Form): +# 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) - # 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 " @@ -141,31 +141,31 @@ class ReconfigurationForm(Form): "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") +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, integration_data): + def __init__(self, controller, configuration_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, + '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': - 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, + 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 = ReconfigurationForm(initial=initial) + self.form = WSLConfigurationAdvancedForm(initial=initial) connect_signal(self.form, 'submit', self.done) super().__init__( @@ -178,7 +178,7 @@ class ReconfigurationView(BaseView): ) def done(self, result): - self.controller.done(WSLConfiguration2Data( + 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, diff --git a/system_setup/ui/views/integration.py b/system_setup/ui/views/wslconfbase.py similarity index 81% rename from system_setup/ui/views/integration.py rename to system_setup/ui/views/wslconfbase.py index bd68c225..27bad478 100644 --- a/system_setup/ui/views/integration.py +++ b/system_setup/ui/views/wslconfbase.py @@ -1,6 +1,6 @@ -""" Integration +""" WSLConfBase -Integration provides user with options to set up integration configurations. +WSLConfBase provides user with options to set up basic WSL configuration, requested on first setup. """ import re @@ -18,13 +18,9 @@ from subiquitycore.ui.form import ( from subiquitycore.ui.interactive import StringEditor from subiquitycore.ui.utils import screen from subiquitycore.view import BaseView -from subiquity.common.types import WSLConfiguration1Data +from subiquity.common.types import WSLConfigurationBase -# TODO WSL: rename from "integration" to something more meaningful - -# TODO WSL: add another view for configure in another file - class MountEditor(StringEditor, WantsToKnowFormField): def keypress(self, size, key): ''' restrict what chars we allow for mountpoints ''' @@ -40,7 +36,7 @@ MountField = simple_field(MountEditor) StringField = simple_field(StringEditor) -class IntegrationForm(Form): +class WSLConfBaseForm(Form): def __init__(self, initial): super().__init__(initial=initial) @@ -93,20 +89,20 @@ class IntegrationForm(Form): "for correct valid input").format(e_t) -class IntegrationView(BaseView): - title = _("Tweaks") - excerpt = _("In this page, you can tweak Ubuntu WSL to your needs. \n") +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, integration_data): + def __init__(self, controller, configuration_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, + '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 = IntegrationForm(initial=initial) + self.form = WSLConfBaseForm(initial=initial) connect_signal(self.form, 'submit', self.done) super().__init__( @@ -119,7 +115,7 @@ class IntegrationView(BaseView): ) def done(self, result): - self.controller.done(WSLConfiguration1Data( + 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, From ba6ce20d64fba2e2f34e06d770abe2e9004d8b97 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Tue, 31 Aug 2021 13:25:28 +0200 Subject: [PATCH 44/69] Do not use a file to share user name Instead retrieve it from the server and store it in memory. --- system_setup/client/controllers/identity.py | 6 ------ system_setup/client/controllers/overview.py | 7 ++++++- system_setup/ui/views/overview.py | 22 +++++---------------- 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/system_setup/client/controllers/identity.py b/system_setup/client/controllers/identity.py index b158d665..39b3e3b6 100644 --- a/system_setup/client/controllers/identity.py +++ b/system_setup/client/controllers/identity.py @@ -41,10 +41,4 @@ class WSLIdentityController(IdentityController): log.debug( "IdentityController.done next_screen user_spec=%s", identity_data) - if not self.opts.dry_run: - username = identity_data.username - # TODO WSL: remove this as a way to pass the values to the backend and keep that in memory - # Then, remove the dry_run condition. - with open('/var/run/ubuntu_wsl_oobe_assigned_account', 'w') as f: - f.write(username) self.app.next_screen(self.endpoint.POST(identity_data)) diff --git a/system_setup/client/controllers/overview.py b/system_setup/client/controllers/overview.py index eb40b5bf..d0f71acd 100644 --- a/system_setup/client/controllers/overview.py +++ b/system_setup/client/controllers/overview.py @@ -9,7 +9,12 @@ log = logging.getLogger('ubuntu_wsl_oobe.controllers.overview') class OverviewController(SubiquityTuiController): async def make_ui(self): - return OverviewView(self) + real_name = "" + identity = getattr(self.app.client, "identity") + if identity is not None: + data = await identity.GET() + real_name = data.realname + return OverviewView(self, real_name) def cancel(self): self.app.cancel() diff --git a/system_setup/ui/views/overview.py b/system_setup/ui/views/overview.py index d93619bc..bbddf5dc 100644 --- a/system_setup/ui/views/overview.py +++ b/system_setup/ui/views/overview.py @@ -14,32 +14,20 @@ from subiquitycore.view import BaseView log = logging.getLogger("ubuntu_wsl_oobe.ui.views.overview") -# TODO WSL: remove this -WSL_USERNAME_PATH = "/var/run/ubuntu_wsl_oobe_assigned_account" - class OverviewView(BaseView): title = _("Setup Complete") - def __init__(self, controller): + def __init__(self, controller, real_name): self.controller = controller - # TODO WSL: remove this and always use in memory value - user_name = "dryrun_user" - if os.path.isfile(WSL_USERNAME_PATH): - with open(WSL_USERNAME_PATH, 'r') as f: - user_name = f.read() - os.remove(WSL_USERNAME_PATH) - complete_text = _("Hi {username},\n" + complete_text = _("Hi {real_name},\n\n" "You have complete the setup!\n\n" - "It is suggested to run the following command" + "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" - "You can use the builtin `ubuntuwsl` command to " - "manage your WSL settings:\n\n\n" - " $ sudo ubuntuwsl ...\n\n\n" - "* All settings will take effect after first " - "restart of Ubuntu.").format(username=user_name) + "All settings will take effect after next " + "restart of Ubuntu.").format(real_name=real_name) super().__init__( screen( From 518cff3d3bf5a1995e83eb96c47848eeec2647b2 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Tue, 31 Aug 2021 13:26:49 +0200 Subject: [PATCH 45/69] Fixed linting --- system_setup/client/client.py | 3 ++- system_setup/client/controllers/wslconfadvanced.py | 3 ++- system_setup/client/controllers/wslconfbase.py | 1 + system_setup/models/wslconfadvanced.py | 4 +++- system_setup/models/wslconfbase.py | 3 ++- system_setup/server/controllers/configure.py | 7 +++---- system_setup/server/controllers/shutdown.py | 10 ++-------- system_setup/server/controllers/wslconfadvanced.py | 7 +++++-- system_setup/server/server.py | 7 +++++-- system_setup/ui/views/overview.py | 2 -- system_setup/ui/views/wslconfadvanced.py | 9 ++++++--- system_setup/ui/views/wslconfbase.py | 6 ++++-- 12 files changed, 35 insertions(+), 27 deletions(-) diff --git a/system_setup/client/client.py b/system_setup/client/client.py index ceba6969..3c067b95 100644 --- a/system_setup/client/client.py +++ b/system_setup/client/client.py @@ -39,7 +39,8 @@ class SystemSetupClient(SubiquityClient): 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?) + # 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 diff --git a/system_setup/client/controllers/wslconfadvanced.py b/system_setup/client/controllers/wslconfadvanced.py index 21d1b57c..8b0d65cd 100644 --- a/system_setup/client/controllers/wslconfadvanced.py +++ b/system_setup/client/controllers/wslconfadvanced.py @@ -19,7 +19,8 @@ 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') +log = logging.getLogger( + 'system_setup.client.controllers.wslconfigurationadvanced') class WSLConfigurationAdvancedController(SubiquityTuiController): diff --git a/system_setup/client/controllers/wslconfbase.py b/system_setup/client/controllers/wslconfbase.py index 87d15e07..a83efc0d 100644 --- a/system_setup/client/controllers/wslconfbase.py +++ b/system_setup/client/controllers/wslconfbase.py @@ -6,6 +6,7 @@ from system_setup.ui.views.wslconfbase import WSLConfigurationBaseView log = logging.getLogger('system_setup.client.controllers.wslconfigurationbase') + class WSLConfigurationBaseController(SubiquityTuiController): endpoint_name = 'wslconfbase' diff --git a/system_setup/models/wslconfadvanced.py b/system_setup/models/wslconfadvanced.py index 8cb73591..7b198ecb 100644 --- a/system_setup/models/wslconfadvanced.py +++ b/system_setup/models/wslconfadvanced.py @@ -23,6 +23,7 @@ log = logging.getLogger('subiquity.models.wsl_configuration_advanced') # TODO WSL: Remove all attributes in wslconfbase + @attr.s class WSLConfigurationAdvanced(object): gui_theme = attr.ib() @@ -68,7 +69,8 @@ class WSLConfigurationAdvancedModel(object): 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 + # 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"], diff --git a/system_setup/models/wslconfbase.py b/system_setup/models/wslconfbase.py index cfd661b5..c2df10b5 100644 --- a/system_setup/models/wslconfbase.py +++ b/system_setup/models/wslconfbase.py @@ -45,7 +45,8 @@ class WSLConfigurationBaseModel(object): 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 + # 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"], diff --git a/system_setup/server/controllers/configure.py b/system_setup/server/controllers/configure.py index 0463954f..6cd4ed70 100644 --- a/system_setup/server/controllers/configure.py +++ b/system_setup/server/controllers/configure.py @@ -14,7 +14,6 @@ # along with this program. If not, see . import logging -import re from subiquitycore.context import with_context @@ -36,7 +35,6 @@ class ConfigureController(SubiquityController): super().__init__(app) self.model = app.base_model - def start(self): self.install_task = self.app.aio_loop.create_task(self.configure()) @@ -57,7 +55,8 @@ class ConfigureController(SubiquityController): # 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 + # 2. Write directly (without wsl utilities) to wsl.conf and other + # fstab files # This must not use subprocesses. # If dry-run: write in .subiquity @@ -72,4 +71,4 @@ class ConfigureController(SubiquityController): def stop_uu(self): # This is a no-op to allow Shutdown controller to depend on this one - pass \ No newline at end of file + pass diff --git a/system_setup/server/controllers/shutdown.py b/system_setup/server/controllers/shutdown.py index c8e13aa6..4c5503b0 100644 --- a/system_setup/server/controllers/shutdown.py +++ b/system_setup/server/controllers/shutdown.py @@ -13,15 +13,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import asyncio import logging -import os -import platform -import subprocess from subiquitycore.context import with_context -from subiquitycore.utils import arun_command, run_command - from subiquity.common.types import ShutdownMode from subiquity.server.controllers import ShutdownController @@ -31,7 +25,8 @@ 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. + # 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 @@ -40,7 +35,6 @@ class SetupShutdownController(ShutdownController): 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() diff --git a/system_setup/server/controllers/wslconfadvanced.py b/system_setup/server/controllers/wslconfadvanced.py index 222b69da..3de13d2a 100644 --- a/system_setup/server/controllers/wslconfadvanced.py +++ b/system_setup/server/controllers/wslconfadvanced.py @@ -24,7 +24,9 @@ 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') +log = logging.getLogger( + 'subiquity.server.controllers.wsl_configuration_advanced') + # TODO WSL: remove all duplicates from WSL config base controller class WSLConfigurationAdvancedController(SubiquityController): @@ -173,7 +175,8 @@ class WSLConfigurationAdvancedController(SubiquityController): 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.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 diff --git a/system_setup/server/server.py b/system_setup/server/server.py index d49df6d5..3b0c3070 100644 --- a/system_setup/server/server.py +++ b/system_setup/server/server.py @@ -35,8 +35,11 @@ class SystemSetupServer(SubiquityServer): ] def __init__(self, opts, block_log_dir): - # TODO WSL: remove reconfigure argument parser option and check dynamically what needs to be presented. - # TODO WSL: we should have WSLConfigurationBase here to show multiple pages. + # TODO WSL: + # remove reconfigure argument parser option and check dynamically + # what needs to be presented. + # TODO WSL: + # we should have WSLConfigurationBase here to show multiple pages. self.is_reconfig = opts.reconfigure if self.is_reconfig and not opts.dry_run: self.controllers = [ diff --git a/system_setup/ui/views/overview.py b/system_setup/ui/views/overview.py index bbddf5dc..dec3e715 100644 --- a/system_setup/ui/views/overview.py +++ b/system_setup/ui/views/overview.py @@ -4,8 +4,6 @@ Overview provides user with the overview of all the current settings. """ - -import os import logging from subiquitycore.ui.buttons import done_btn diff --git a/system_setup/ui/views/wslconfadvanced.py b/system_setup/ui/views/wslconfadvanced.py index 483a2bec..6dc51f27 100644 --- a/system_setup/ui/views/wslconfadvanced.py +++ b/system_setup/ui/views/wslconfadvanced.py @@ -1,6 +1,7 @@ """ WSLConfigurationAdvanced View -WSLConfigurationAdvanced provides user with options with additional settings for advanced configuration. +WSLConfigurationAdvanced provides user with options with additional settings +for advanced configuration. """ import re @@ -37,7 +38,8 @@ MountField = simple_field(MountEditor) StringField = simple_field(StringEditor) -# TODO WSL: Advanced should not contain base configuration (it must be in 2 pages). +# TODO WSL: Advanced should not contain base configuration +# (it must be in 2 pages). class WSLConfigurationAdvancedForm(Form): def __init__(self, initial): @@ -143,7 +145,8 @@ class WSLConfigurationAdvancedForm(Form): class WSLConfigurationAdvancedView(BaseView): title = _("WSL advanced options") - excerpt = _("In this page, you can configure Ubuntu WSL advanced options your needs. \n") + excerpt = _("In this page, you can configure Ubuntu WSL" + "advanced options your needs. \n") def __init__(self, controller, configuration_data): self.controller = controller diff --git a/system_setup/ui/views/wslconfbase.py b/system_setup/ui/views/wslconfbase.py index 27bad478..96086d1b 100644 --- a/system_setup/ui/views/wslconfbase.py +++ b/system_setup/ui/views/wslconfbase.py @@ -1,6 +1,7 @@ """ WSLConfBase -WSLConfBase provides user with options to set up basic WSL configuration, requested on first setup. +WSLConfBase provides user with options to set up basic WSL configuration, +requested on first setup. """ import re @@ -91,7 +92,8 @@ class WSLConfBaseForm(Form): class WSLConfigurationBaseView(BaseView): title = _("WSL configuration options") - excerpt = _("In this page, you can configure Ubuntu WSL options to your needs.\n") + excerpt = _( + "In this page, you can configure Ubuntu WSL options to your needs.\n") def __init__(self, controller, configuration_data): self.controller = controller From dcab9c2e6e0e20fad26d879dafd9df6c795dae38 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Tue, 31 Aug 2021 14:59:08 +0200 Subject: [PATCH 46/69] Removal of --reconfigure for server Load all the server controller independently whether it is in first configuration or reconfiguration mode. Co-authored-by: Didier Roche --- system_setup/cmd/server.py | 1 - system_setup/cmd/tui.py | 3 +-- system_setup/models/system_server.py | 9 ++------- system_setup/models/wslconfadvanced.py | 2 +- system_setup/server/controllers/configure.py | 2 ++ system_setup/server/server.py | 21 +------------------- 6 files changed, 7 insertions(+), 31 deletions(-) diff --git a/system_setup/cmd/server.py b/system_setup/cmd/server.py index 4aa82574..0e174b1b 100644 --- a/system_setup/cmd/server.py +++ b/system_setup/cmd/server.py @@ -35,7 +35,6 @@ 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 d2b8d206..29e20acb 100755 --- a/system_setup/cmd/tui.py +++ b/system_setup/cmd/tui.py @@ -73,6 +73,7 @@ def make_client_args_parser(): 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 @@ -94,8 +95,6 @@ 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/system_server.py b/system_setup/models/system_server.py index 3413a458..9e99bec8 100644 --- a/system_setup/models/system_server.py +++ b/system_setup/models/system_server.py @@ -52,15 +52,10 @@ class SystemSetupModel(SubiquityModel): "locale", "identity", "wslconfbase", + "wslconfadvanced", }) - def __init__(self, root, reconfigure=False): - # TODO WSL: add base model here to prevent overlap - if reconfigure: - self.INSTALL_MODEL_NAMES = ModelNames({ - "locale", - "wslconfadvanced", - }) + def __init__(self, root): # Parent class init is not called to not load models we don't need. self.root = root self.is_wsl = is_wsl() diff --git a/system_setup/models/wslconfadvanced.py b/system_setup/models/wslconfadvanced.py index 7b198ecb..3b6262b2 100644 --- a/system_setup/models/wslconfadvanced.py +++ b/system_setup/models/wslconfadvanced.py @@ -21,7 +21,7 @@ from subiquitycore.utils import run_command log = logging.getLogger('subiquity.models.wsl_configuration_advanced') -# TODO WSL: Remove all attributes in wslconfbase +# TODO WSL: Remove all common attributes with wslconfbase @attr.s diff --git a/system_setup/server/controllers/configure.py b/system_setup/server/controllers/configure.py index 6cd4ed70..29d59750 100644 --- a/system_setup/server/controllers/configure.py +++ b/system_setup/server/controllers/configure.py @@ -57,9 +57,11 @@ class ConfigureController(SubiquityController): # 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) diff --git a/system_setup/server/server.py b/system_setup/server/server.py index 3b0c3070..564a61d5 100644 --- a/system_setup/server/server.py +++ b/system_setup/server/server.py @@ -34,27 +34,8 @@ class SystemSetupServer(SubiquityServer): "Late", ] - def __init__(self, opts, block_log_dir): - # TODO WSL: - # remove reconfigure argument parser option and check dynamically - # what needs to be presented. - # TODO WSL: - # we should have WSLConfigurationBase here to show multiple pages. - self.is_reconfig = opts.reconfigure - if self.is_reconfig and not opts.dry_run: - self.controllers = [ - "Reporting", - "Error", - "Locale", - "WSLConfigurationAdvanced", - "Configure", - "SetupShutdown", - "Late", - ] - super().__init__(opts, block_log_dir) - def make_model(self): root = '/' if self.opts.dry_run: root = os.path.abspath('.subiquity') - return SystemSetupModel(root, self.is_reconfig) + return SystemSetupModel(root) From 3044a1fc11c7b3a45e37667b8cc449942e542424 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Tue, 31 Aug 2021 15:43:35 +0200 Subject: [PATCH 47/69] Update answer file with renamed controllers WSL configuration controllers have been renamed, the answer file must be updated accordingly Co-authored-by: Didier Roche --- examples/answers-system-setup.yaml | 6 ++---- system_setup/server/server.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/answers-system-setup.yaml b/examples/answers-system-setup.yaml index d22b9d49..2a743b02 100644 --- a/examples/answers-system-setup.yaml +++ b/examples/answers-system-setup.yaml @@ -5,7 +5,7 @@ WSLIdentity: username: ubuntu # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' -Integration: +WSLConfigurationBase: custom_path: '/custom_mnt_path' custom_mount_opt: 'opt1 opt2 opt3' gen_host: false @@ -13,6 +13,4 @@ Integration: Overview: noproperty: "there is no property for this view, just a done button but subiquity requires something to proceed" InstallProgress: - reboot: yes - - + reboot: yes \ No newline at end of file diff --git a/system_setup/server/server.py b/system_setup/server/server.py index 564a61d5..bc80fd75 100644 --- a/system_setup/server/server.py +++ b/system_setup/server/server.py @@ -30,8 +30,8 @@ class SystemSetupServer(SubiquityServer): "WSLConfigurationBase", "WSLConfigurationAdvanced", "Configure", - "SetupShutdown", "Late", + "SetupShutdown", ] def make_model(self): From 9b67591335cffed9b0b2ef8754fc0e4f8d04ac19 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Tue, 31 Aug 2021 15:56:08 +0200 Subject: [PATCH 48/69] More linting fixes Co-authored-by: Didier Roche --- system_setup/cmd/tui.py | 3 ++- system_setup/server/controllers/configure.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/system_setup/cmd/tui.py b/system_setup/cmd/tui.py index 29e20acb..9958a7ad 100755 --- a/system_setup/cmd/tui.py +++ b/system_setup/cmd/tui.py @@ -73,7 +73,8 @@ def make_client_args_parser(): 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 + # 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 diff --git a/system_setup/server/controllers/configure.py b/system_setup/server/controllers/configure.py index 29d59750..52b3edc2 100644 --- a/system_setup/server/controllers/configure.py +++ b/system_setup/server/controllers/configure.py @@ -57,11 +57,11 @@ class ConfigureController(SubiquityController): # 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. + # 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) From 1556163d8754b36f585db92afe7182006802ba82 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Wed, 1 Sep 2021 10:42:22 +0200 Subject: [PATCH 49/69] Make variants configurable Current variant and supported one were hardcoded and not extensible. This makes it configurable per application. Co-authored-by: Didier Roche --- subiquity/client/client.py | 4 +++- subiquity/server/server.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/subiquity/client/client.py b/subiquity/client/client.py index d513ce38..f1060844 100644 --- a/subiquity/client/client.py +++ b/subiquity/client/client.py @@ -83,6 +83,8 @@ class SubiquityClient(TuiApplication): snapd_socket_path = '/run/snapd.socket' + variant = "server" + from subiquity.client import controllers as controllers_mod project = "subiquity" @@ -435,7 +437,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/server/server.py b/subiquity/server/server.py index 26c58b7f..cb3b6bd1 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -113,7 +113,7 @@ 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) @@ -227,6 +227,8 @@ class SubiquityServer(Application): "Shutdown", ] + supported_variants = ["server", "desktop"] + def make_model(self): root = '/' if self.opts.dry_run: From da5c83d4f11b12b9292da206832dbb210d6fc7cc Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Wed, 1 Sep 2021 10:44:37 +0200 Subject: [PATCH 50/69] Set supported and current variant for WSL Co-authored-by: Didier Roche --- system_setup/client/client.py | 3 +++ system_setup/server/server.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/system_setup/client/client.py b/system_setup/client/client.py index 3c067b95..8e343e8c 100644 --- a/system_setup/client/client.py +++ b/system_setup/client/client.py @@ -28,6 +28,8 @@ class SystemSetupClient(SubiquityClient): snapd_socket_path = None + variant = "wsl_setup" + controllers = [ "Welcome", "WSLIdentity", @@ -44,6 +46,7 @@ class SystemSetupClient(SubiquityClient): # 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 = [ "Welcome", diff --git a/system_setup/server/server.py b/system_setup/server/server.py index bc80fd75..9b54fc92 100644 --- a/system_setup/server/server.py +++ b/system_setup/server/server.py @@ -34,6 +34,8 @@ class SystemSetupServer(SubiquityServer): "SetupShutdown", ] + supported_variants = ["wsl_setup", "wsl_configuration"] + def make_model(self): root = '/' if self.opts.dry_run: From 1f4a9ab6f3b761af6de9e274d5c2b886923cf7c1 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Wed, 1 Sep 2021 10:45:32 +0200 Subject: [PATCH 51/69] Support empty default model names When the list of model names is empty it was implicitly casted to a dict but a set() was required for concatenation. Co-authored-by: Didier Roche --- subiquity/models/subiquity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subiquity/models/subiquity.py b/subiquity/models/subiquity.py index 93c93470..2129e1ea 100644 --- a/subiquity/models/subiquity.py +++ b/subiquity/models/subiquity.py @@ -76,7 +76,7 @@ class ModelNames: self.per_variant_names = per_variant_names def for_variant(self, variant): - return self.default_names | self.per_variant_names.get(variant, set()) + return set(self.default_names) | self.per_variant_names.get(variant, set()) def all(self): r = set(self.default_names) From a60da3e589cc56c5a31b197664d92aeb09c3d151 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Wed, 1 Sep 2021 10:47:24 +0200 Subject: [PATCH 52/69] Support empty post_install model list The signal telling postinstall was configured was never sent. Co-authored-by: Didier Roche --- subiquity/models/subiquity.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/subiquity/models/subiquity.py b/subiquity/models/subiquity.py index 2129e1ea..8ab44e1b 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) From 3eb5231fcab83a93efea928fa3bd23824f4718dd Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Wed, 1 Sep 2021 10:48:44 +0200 Subject: [PATCH 53/69] Only proceed for installation when ready. Wait for models to be configured before proceeding with installation. Co-authored-by: Didier Roche --- system_setup/server/controllers/configure.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/system_setup/server/controllers/configure.py b/system_setup/server/controllers/configure.py index 52b3edc2..63957943 100644 --- a/system_setup/server/controllers/configure.py +++ b/system_setup/server/controllers/configure.py @@ -42,15 +42,19 @@ class ConfigureController(SubiquityController): description="final system configuration", level="INFO", childlevel="DEBUG") async def configure(self, *, context): - context.set('is-configure-context', True) + 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: From 5d318b2fbbfe6b6817a8d90cf3244232aeb49190 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Wed, 1 Sep 2021 10:50:56 +0200 Subject: [PATCH 54/69] Rebase on new subiquity model. Variants are set at the application level and impact the models. Co-authored-by: Didier Roche --- .../{system_server.py => system_setup.py} | 53 +++---------------- system_setup/server/server.py | 21 ++++++-- 2 files changed, 26 insertions(+), 48 deletions(-) rename system_setup/models/{system_server.py => system_setup.py} (53%) diff --git a/system_setup/models/system_server.py b/system_setup/models/system_setup.py similarity index 53% rename from system_setup/models/system_server.py rename to system_setup/models/system_setup.py index 9e99bec8..158da905 100644 --- a/system_setup/models/system_server.py +++ b/system_setup/models/system_setup.py @@ -47,18 +47,11 @@ class SystemSetupModel(SubiquityModel): target = '/' - # Models that will be used in WSL system setup - INSTALL_MODEL_NAMES = ModelNames({ - "locale", - "identity", - "wslconfbase", - "wslconfadvanced", - }) - - def __init__(self, root): + 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 - self.is_wsl = is_wsl() + if root != '/': + self.target = root self.packages = [] self.userdata = {} @@ -71,40 +64,10 @@ class SystemSetupModel(SubiquityModel): self._confirmation_task = None self._configured_names = set() - self._install_model_names = self.INSTALL_MODEL_NAMES - self._postinstall_model_names = None - self._cur_install_model_names = self.INSTALL_MODEL_NAMES.default_names - self._cur_postinstall_model_names = None + 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() - - def set_source_variant(self, variant): - self._cur_install_model_names = \ - self._install_model_names.for_variant(variant) - if self._cur_postinstall_model_names is not None: - self._cur_postinstall_model_names = \ - self._postinstall_model_names.for_variant(variant) - unconfigured_install_model_names = \ - self._cur_install_model_names - self._configured_names - if unconfigured_install_model_names: - if self._install_event.is_set(): - self._install_event = asyncio.Event() - if self._confirmation_task is not None: - self._confirmation_task.cancel() - else: - self._install_event.set() - - def configured(self, model_name): - self._configured_names.add(model_name) - if model_name in self._cur_install_model_names: - stage = 'install' - names = self._cur_install_model_names - event = self._install_event - else: - return - unconfigured = names - self._configured_names - log.debug( - "model %s for %s stage is configured, to go %s", - model_name, stage, unconfigured) - if not unconfigured: - event.set() diff --git a/system_setup/server/server.py b/system_setup/server/server.py index 9b54fc92..8831deda 100644 --- a/system_setup/server/server.py +++ b/system_setup/server/server.py @@ -14,17 +14,32 @@ # along with this program. If not, see . from subiquity.server.server import SubiquityServer -from system_setup.models.system_server import SystemSetupModel +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({}) + + class SystemSetupServer(SubiquityServer): from system_setup.server import controllers as controllers_mod controllers = [ "Reporting", "Error", - "Userdata", "Locale", "Identity", "WSLConfigurationBase", @@ -40,4 +55,4 @@ class SystemSetupServer(SubiquityServer): root = '/' if self.opts.dry_run: root = os.path.abspath('.subiquity') - return SystemSetupModel(root) + return SystemSetupModel(root, INSTALL_MODEL_NAMES, POSTINSTALL_MODEL_NAMES) From 805a84ab7624ec1883edf83a5b3a0f9519eda2ec Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Wed, 1 Sep 2021 11:02:25 +0200 Subject: [PATCH 55/69] Linter fixes Co-authored-by: Didier Roche --- subiquity/models/subiquity.py | 3 ++- system_setup/models/system_setup.py | 5 +---- system_setup/server/server.py | 3 ++- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/subiquity/models/subiquity.py b/subiquity/models/subiquity.py index 8ab44e1b..f3b2c5cf 100644 --- a/subiquity/models/subiquity.py +++ b/subiquity/models/subiquity.py @@ -76,7 +76,8 @@ class ModelNames: self.per_variant_names = per_variant_names def for_variant(self, variant): - return set(self.default_names) | self.per_variant_names.get(variant, set()) + return set(self.default_names) | self.per_variant_names.get(variant, + set()) def all(self): r = set(self.default_names) diff --git a/system_setup/models/system_setup.py b/system_setup/models/system_setup.py index 158da905..3496b474 100644 --- a/system_setup/models/system_setup.py +++ b/system_setup/models/system_setup.py @@ -16,10 +16,7 @@ import asyncio import logging -from subiquity.models.subiquity import ModelNames, SubiquityModel - -from subiquitycore.utils import is_wsl - +from subiquity.models.subiquity import SubiquityModel from subiquity.models.locale import LocaleModel from subiquity.models.identity import IdentityModel diff --git a/system_setup/server/server.py b/system_setup/server/server.py index 8831deda..cc1ede26 100644 --- a/system_setup/server/server.py +++ b/system_setup/server/server.py @@ -55,4 +55,5 @@ class SystemSetupServer(SubiquityServer): root = '/' if self.opts.dry_run: root = os.path.abspath('.subiquity') - return SystemSetupModel(root, INSTALL_MODEL_NAMES, POSTINSTALL_MODEL_NAMES) + return SystemSetupModel(root, INSTALL_MODEL_NAMES, + POSTINSTALL_MODEL_NAMES) From 5b2e78a8a1cd19277de09fcf029e0f582b0405cf Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Wed, 1 Sep 2021 17:42:19 +0200 Subject: [PATCH 56/69] Ensure we always exit the server Co-authored-by: Didier Roche --- system_setup/server/controllers/shutdown.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/system_setup/server/controllers/shutdown.py b/system_setup/server/controllers/shutdown.py index 4c5503b0..17859cb8 100644 --- a/system_setup/server/controllers/shutdown.py +++ b/system_setup/server/controllers/shutdown.py @@ -38,9 +38,7 @@ class SetupShutdownController(ShutdownController): @with_context(description='mode={self.mode.name}') def shutdown(self, context): self.shuttingdown_event.set() - if self.opts.dry_run: - self.app.exit() - else: + if not self.opts.dry_run: if self.mode == ShutdownMode.REBOOT: # TODO WSL: # Implement a reboot that doesn't depend on systemd @@ -49,3 +47,4 @@ class SetupShutdownController(ShutdownController): # TODO WSL: # Implement a poweroff that doesn't depend on systemd log.Warning("poweroff command not implemented") + self.app.exit() From dadbb55fd2807588676b10407f59a0e5ad319f8f Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Wed, 1 Sep 2021 19:02:55 +0200 Subject: [PATCH 57/69] Fix controller removal Properly remove unsupported controllers. Co-authored-by: Didier Roche --- subiquity/server/server.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/subiquity/server/server.py b/subiquity/server/server.py index cb3b6bd1..ac427326 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -276,8 +276,13 @@ class SubiquityServer(Application): self.snapd = AsyncSnapd(connection) else: log.info("no snapd socket found. Snap support is disabled") - self.controllers.remove("Refresh") - self.controllers.remove("SnapList") + reload_needed = False + for controller in ["Refresh", "Snaplist"]: + if controller in self.controllers.controller_names: + self.controllers.controller_names.remove(controller) + reload_needed = True + if reload_needed: + self.controllers.load_all() self.snapd = None self.note_data_for_apport("SnapUpdated", str(self.updated)) self.event_listeners = [] From 15664d9016cb3b71debdfa032c43baaf2c9dba40 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Wed, 1 Sep 2021 19:03:52 +0200 Subject: [PATCH 58/69] Do not log to journald on non-systemd systems. Some systems like WSL do not support systemd and we cannot rely on journald for logging. Thus we remove the progress controller that would always be blank. Co-authored-by: Didier Roche --- subiquity/client/client.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/subiquity/client/client.py b/subiquity/client/client.py index f1060844..05acf150 100644 --- a/subiquity/client/client.py +++ b/subiquity/client/client.py @@ -319,14 +319,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() From ab161250e188073c59ddc046602ed7408e5bb982 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Wed, 1 Sep 2021 19:06:50 +0200 Subject: [PATCH 59/69] Removed the progress view. On non systemd systems (ie WSL) the progress view is always empty. Thus this view is removed and the summary (formerly overview) view becomes the last page of the setup with the reboot button. This button is displayed dynamically when setup is complete. Co-authored-by: Didier Roche --- examples/answers-system-setup.yaml | 4 +- system_setup/client/client.py | 7 +- system_setup/client/controllers/__init__.py | 8 +- system_setup/client/controllers/overview.py | 28 ------ system_setup/client/controllers/summary.py | 98 +++++++++++++++++++++ system_setup/ui/views/__init__.py | 4 +- system_setup/ui/views/overview.py | 41 --------- system_setup/ui/views/summary.py | 90 +++++++++++++++++++ 8 files changed, 197 insertions(+), 83 deletions(-) delete mode 100644 system_setup/client/controllers/overview.py create mode 100644 system_setup/client/controllers/summary.py delete mode 100644 system_setup/ui/views/overview.py create mode 100644 system_setup/ui/views/summary.py diff --git a/examples/answers-system-setup.yaml b/examples/answers-system-setup.yaml index 2a743b02..4da0b03e 100644 --- a/examples/answers-system-setup.yaml +++ b/examples/answers-system-setup.yaml @@ -10,7 +10,5 @@ WSLConfigurationBase: custom_mount_opt: 'opt1 opt2 opt3' gen_host: false gen_resolvconf: false -Overview: - noproperty: "there is no property for this view, just a done button but subiquity requires something to proceed" -InstallProgress: +Summary: reboot: yes \ No newline at end of file diff --git a/system_setup/client/client.py b/system_setup/client/client.py index 8e343e8c..6f95670a 100644 --- a/system_setup/client/client.py +++ b/system_setup/client/client.py @@ -34,8 +34,7 @@ class SystemSetupClient(SubiquityClient): "Welcome", "WSLIdentity", "WSLConfigurationBase", - "Overview", - "Progress", + "Summary", ] def __init__(self, opts): @@ -49,9 +48,9 @@ class SystemSetupClient(SubiquityClient): # self.variant = "wsl_configuration" if opts.reconfigure: self.controllers = [ - "Welcome", + "WSLConfigurationBase", "WSLConfigurationAdvanced", - "Progress", + "Summary", ] super().__init__(opts) diff --git a/system_setup/client/controllers/__init__.py b/system_setup/client/controllers/__init__.py index 7ec05a12..a896553f 100644 --- a/system_setup/client/controllers/__init__.py +++ b/system_setup/client/controllers/__init__.py @@ -16,18 +16,16 @@ from .identity import WSLIdentityController from .wslconfbase import WSLConfigurationBaseController -from .overview import OverviewController +from .summary import SummaryController from .wslconfadvanced import WSLConfigurationAdvancedController -from subiquity.client.controllers import (ProgressController, - WelcomeController) +from subiquity.client.controllers import (WelcomeController) __all__ = [ 'WelcomeController', 'WSLIdentityController', - 'ProgressController', 'WSLConfigurationBaseController', 'WSLConfigurationAdvancedController', - 'OverviewController', + 'SummaryController', ] diff --git a/system_setup/client/controllers/overview.py b/system_setup/client/controllers/overview.py deleted file mode 100644 index d0f71acd..00000000 --- a/system_setup/client/controllers/overview.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging -from subiquity.client.controller import SubiquityTuiController - -from system_setup.ui.views.overview import OverviewView - -log = logging.getLogger('ubuntu_wsl_oobe.controllers.overview') - - -class OverviewController(SubiquityTuiController): - - 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 - return OverviewView(self, real_name) - - def cancel(self): - self.app.cancel() - - def run_answers(self): - self.done(None) - - def done(self, result): - log.debug( - "OverviewController.done next_screen") - self.app.next_screen() diff --git a/system_setup/client/controllers/summary.py b/system_setup/client/controllers/summary.py new file mode 100644 index 00000000..e63c8dc3 --- /dev/null +++ b/system_setup/client/controllers/summary.py @@ -0,0 +1,98 @@ +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.NEEDS_CONFIRMATION: + if self.showing: + self.app.show_confirm_install() + + 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/ui/views/__init__.py b/system_setup/ui/views/__init__.py index 6ae62de7..524b68f9 100644 --- a/system_setup/ui/views/__init__.py +++ b/system_setup/ui/views/__init__.py @@ -16,11 +16,11 @@ from .identity import WSLIdentityView from .wslconfbase import WSLConfigurationBaseView from .wslconfadvanced import WSLConfigurationAdvancedView -from .overview import OverviewView +from .summary import SummaryView __all__ = [ 'WSLIdentityView', 'WSLConfigurationBaseView', 'WSLConfigurationAdvancedView', - 'OverviewView', + 'SummaryView', ] diff --git a/system_setup/ui/views/overview.py b/system_setup/ui/views/overview.py deleted file mode 100644 index dec3e715..00000000 --- a/system_setup/ui/views/overview.py +++ /dev/null @@ -1,41 +0,0 @@ -""" Overview - -Overview provides user with the overview of all the current settings. - -""" - -import logging - -from subiquitycore.ui.buttons import done_btn -from subiquitycore.ui.utils import button_pile, screen -from subiquitycore.view import BaseView - -log = logging.getLogger("ubuntu_wsl_oobe.ui.views.overview") - - -class OverviewView(BaseView): - title = _("Setup Complete") - - def __init__(self, controller, real_name): - self.controller = controller - complete_text = _("Hi {real_name},\n\n" - "You have complete 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) - - super().__init__( - screen( - rows=[], - buttons=button_pile( - [done_btn(_("Done"), on_press=self.confirm), ]), - focus_buttons=True, - excerpt=complete_text, - ) - ) - - def confirm(self, result): - self.controller.done(result) 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() From ef9d387b4fadbf7aff006482019ae7e2a8329ced Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Fri, 3 Sep 2021 09:08:44 +0200 Subject: [PATCH 60/69] Removed unused function Co-authored-by: Didier Roche --- subiquitycore/utils.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/subiquitycore/utils.py b/subiquitycore/utils.py index 067ceeee..14e94a10 100644 --- a/subiquitycore/utils.py +++ b/subiquitycore/utils.py @@ -144,8 +144,3 @@ def disable_subiquity(): "snap.subiquity.subiquity-service.service", "serial-subiquity@*.service"]) return - - -def is_wsl(): - """ Returns True if we are on a WSL system """ - return pathlib.Path("/proc/sys/fs/binfmt_misc/WSLInterop").is_file() From 4c83e4afde072f23a6e6d6cdd1077e0c6cb8b3da Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Fri, 3 Sep 2021 09:38:50 +0200 Subject: [PATCH 61/69] Remove duplication in server execution code Make command line and dry-run command line a class member. Co-authored-by: Didier Roche --- subiquity/client/client.py | 6 ++++-- system_setup/client/client.py | 30 ++---------------------------- 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/subiquity/client/client.py b/subiquity/client/client.py index 05acf150..ef3f8111 100644 --- a/subiquity/client/client.py +++ b/subiquity/client/client.py @@ -84,6 +84,8 @@ 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" @@ -173,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]) diff --git a/system_setup/client/client.py b/system_setup/client/client.py index 6f95670a..57bacdf4 100644 --- a/system_setup/client/client.py +++ b/system_setup/client/client.py @@ -29,6 +29,8 @@ class SystemSetupClient(SubiquityClient): snapd_socket_path = None variant = "wsl_setup" + cmdline = sys.argv + dryrun_cmdline_module = 'system_setup.cmd.tui' controllers = [ "Welcome", @@ -54,32 +56,4 @@ class SystemSetupClient(SubiquityClient): ] super().__init__(opts) - def restart(self, remove_last_screen=True, restart_server=False): - log.debug(f"restart {remove_last_screen} {restart_server}") - if self.fg_proc is not None: - log.debug( - "killing foreground process %s before restarting", - self.fg_proc) - self.restarting = True - self.aio_loop.create_task( - self._kill_fg_proc(remove_last_screen, restart_server)) - return - if remove_last_screen: - self._remove_last_screen() - if restart_server: - self.restarting = True - self.ui.block_input = True - self.aio_loop.create_task(self._restart_server()) - return - if self.urwid_loop is not None: - self.urwid_loop.stop() - cmdline = sys.argv - if self.opts.dry_run: - cmdline = [ - sys.executable, '-m', 'system_setup.cmd.tui', - ] + sys.argv[1:] + ['--socket', self.opts.socket] - if self.opts.server_pid is not None: - cmdline.extend(['--server-pid', self.opts.server_pid]) - log.debug("restarting %r", cmdline) - os.execvp(cmdline[0], cmdline) From 2a36e30812b68ddcd4a0d16bcb2e32c6ce16b0ea Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Fri, 3 Sep 2021 09:39:32 +0200 Subject: [PATCH 62/69] Remove set conversion and explicitely init it Co-authored-by: Didier Roche --- subiquity/models/subiquity.py | 2 +- system_setup/server/server.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/subiquity/models/subiquity.py b/subiquity/models/subiquity.py index f3b2c5cf..d3eb009e 100644 --- a/subiquity/models/subiquity.py +++ b/subiquity/models/subiquity.py @@ -76,7 +76,7 @@ class ModelNames: self.per_variant_names = per_variant_names def for_variant(self, variant): - return set(self.default_names) | self.per_variant_names.get(variant, + return self.default_names | self.per_variant_names.get(variant, set()) def all(self): diff --git a/system_setup/server/server.py b/system_setup/server/server.py index cc1ede26..b73acf8e 100644 --- a/system_setup/server/server.py +++ b/system_setup/server/server.py @@ -31,7 +31,7 @@ INSTALL_MODEL_NAMES = ModelNames({ "wslconfadvanced", }) -POSTINSTALL_MODEL_NAMES = ModelNames({}) +POSTINSTALL_MODEL_NAMES = ModelNames(set()) class SystemSetupServer(SubiquityServer): From 3cdaf76153ddba71b2d5d12a8db93eacd30e0d6a Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Fri, 3 Sep 2021 09:44:51 +0200 Subject: [PATCH 63/69] Mark as TODO adding a helper for future refactoring Co-authored-by: Didier Roche --- system_setup/cmd/tui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system_setup/cmd/tui.py b/system_setup/cmd/tui.py index 9958a7ad..92fb8c57 100755 --- a/system_setup/cmd/tui.py +++ b/system_setup/cmd/tui.py @@ -89,6 +89,7 @@ def main(): 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: From ea297095738fbb1fe60dba2e7e86178be0d13df2 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Fri, 3 Sep 2021 09:45:37 +0200 Subject: [PATCH 64/69] Fix typo and punctuation Co-authored-by: Didier Roche --- system_setup/client/controllers/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system_setup/client/controllers/__init__.py b/system_setup/client/controllers/__init__.py index a896553f..9720388c 100644 --- a/system_setup/client/controllers/__init__.py +++ b/system_setup/client/controllers/__init__.py @@ -19,7 +19,7 @@ from .wslconfbase import WSLConfigurationBaseController from .summary import SummaryController from .wslconfadvanced import WSLConfigurationAdvancedController -from subiquity.client.controllers import (WelcomeController) +from subiquity.client.controllers import WelcomeController __all__ = [ From 039201949dbcdc490e1e7f8837fc9ad4553e8aaf Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Fri, 3 Sep 2021 09:52:08 +0200 Subject: [PATCH 65/69] Remove uneeded confirmation handling in system_setup Co-authored-by: Didier Roche --- system_setup/client/controllers/summary.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/system_setup/client/controllers/summary.py b/system_setup/client/controllers/summary.py index e63c8dc3..7616af60 100644 --- a/system_setup/client/controllers/summary.py +++ b/system_setup/client/controllers/summary.py @@ -67,10 +67,6 @@ class SummaryController(SubiquityTuiController): self.ui.set_body(self.summary_view) self.app.show_error_report(self.crash_report_ref) - if self.app_state == ApplicationState.NEEDS_CONFIRMATION: - if self.showing: - self.app.show_confirm_install() - if self.app_state == ApplicationState.RUNNING: if app_status.confirming_tty != self.app.our_tty: install_running = InstallRunning( From 46eb52c9cb673d3f1c693cc877430c3dbdf1a06f Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Fri, 3 Sep 2021 09:52:23 +0200 Subject: [PATCH 66/69] Split system_identity controller from subiquity one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is little inherited from it, let’s split them. Co-authored-by: Didier Roche --- system_setup/client/controllers/identity.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/system_setup/client/controllers/identity.py b/system_setup/client/controllers/identity.py index 39b3e3b6..48364b53 100644 --- a/system_setup/client/controllers/identity.py +++ b/system_setup/client/controllers/identity.py @@ -15,14 +15,16 @@ import logging -from subiquity.client.controllers import IdentityController +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(IdentityController): +class WSLIdentityController(SubiquityTuiController): + + endpoint_name = 'identity' async def make_ui(self): data = await self.endpoint.GET() @@ -37,6 +39,9 @@ class WSLIdentityController(IdentityController): 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", From 0a590e2c10182d655f7075d07c05cbd77e8e6dcd Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Fri, 3 Sep 2021 10:02:23 +0200 Subject: [PATCH 67/69] Helper for timedatectl detection This is done now at running time. Co-authored-by: Didier Roche --- subiquity/server/controllers/timezone.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/subiquity/server/controllers/timezone.py b/subiquity/server/controllers/timezone.py index 9fc44784..760e7657 100644 --- a/subiquity/server/controllers/timezone.py +++ b/subiquity/server/controllers/timezone.py @@ -24,22 +24,23 @@ import os log = logging.getLogger('subiquity.server.controllers.timezone') -TIMEDATECTLCMD = which('timedatectl') -SD_BOOTED = os.path.exists('/run/systemd/system') + +def active_timedatectl(): + return which('timedatectl') and os.path.exists('/run/systemd/system') def generate_possible_tzs(): special_keys = ['', 'geoip'] - if not TIMEDATECTLCMD or not SD_BOOTED: + if not active_timedatectl(): return special_keys - tzcmd = [TIMEDATECTLCMD, 'list-timezones'] + tzcmd = ['timedatectl', 'list-timezones'] list_tz_out = subprocess.check_output(tzcmd, universal_newlines=True) real_tzs = list_tz_out.splitlines() return special_keys + real_tzs def timedatectl_settz(app, tz): - tzcmd = [TIMEDATECTLCMD, 'set-timezone', tz] + tzcmd = ['timedatectl', 'set-timezone', tz] if app.opts.dry_run: tzcmd = ['sleep', str(1/app.scale_factor)] @@ -51,7 +52,7 @@ def timedatectl_settz(app, tz): def timedatectl_gettz(): # timedatectl show would be easier, but isn't on bionic - tzcmd = [TIMEDATECTLCMD, 'status'] + tzcmd = ['timedatectl', 'status'] env = {'LC_ALL': 'C'} # ... # Time zone: America/Denver (MDT, -0600) From 2585ab0ce232738235496ae5f409027018027b71 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Fri, 3 Sep 2021 10:11:35 +0200 Subject: [PATCH 68/69] Remove uneeded controller unloading This was a leftover from where we were loading them unconditionally Co-authored-by: Didier Roche --- subiquity/server/server.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/subiquity/server/server.py b/subiquity/server/server.py index ac427326..e833367d 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -276,13 +276,6 @@ class SubiquityServer(Application): self.snapd = AsyncSnapd(connection) else: log.info("no snapd socket found. Snap support is disabled") - reload_needed = False - for controller in ["Refresh", "Snaplist"]: - if controller in self.controllers.controller_names: - self.controllers.controller_names.remove(controller) - reload_needed = True - if reload_needed: - self.controllers.load_all() self.snapd = None self.note_data_for_apport("SnapUpdated", str(self.updated)) self.event_listeners = [] From b47362ea2a69c77afba1e7cdc861c076da96c438 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lallement Date: Fri, 3 Sep 2021 10:18:18 +0200 Subject: [PATCH 69/69] Fixed linting Co-authored-by: Didier Roche --- subiquity/models/subiquity.py | 3 +-- subiquitycore/utils.py | 1 - system_setup/client/client.py | 3 --- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/subiquity/models/subiquity.py b/subiquity/models/subiquity.py index d3eb009e..99de443c 100644 --- a/subiquity/models/subiquity.py +++ b/subiquity/models/subiquity.py @@ -76,8 +76,7 @@ class ModelNames: self.per_variant_names = per_variant_names def for_variant(self, variant): - return self.default_names | self.per_variant_names.get(variant, - set()) + return self.default_names | self.per_variant_names.get(variant, set()) def all(self): r = set(self.default_names) diff --git a/subiquitycore/utils.py b/subiquitycore/utils.py index 14e94a10..aacc80fd 100644 --- a/subiquitycore/utils.py +++ b/subiquitycore/utils.py @@ -19,7 +19,6 @@ import logging import os import random import subprocess -import pathlib log = logging.getLogger("subiquitycore.utils") diff --git a/system_setup/client/client.py b/system_setup/client/client.py index 57bacdf4..2439f967 100644 --- a/system_setup/client/client.py +++ b/system_setup/client/client.py @@ -14,7 +14,6 @@ # along with this program. If not, see . import logging -import os import sys from subiquity.client.client import SubiquityClient @@ -55,5 +54,3 @@ class SystemSetupClient(SubiquityClient): "Summary", ] super().__init__(opts) - -