From 339e66a2c3ed80e620048f65b06be290bb4dce8b Mon Sep 17 00:00:00 2001 From: "Jinming Wu, Patrick" Date: Fri, 2 Jul 2021 16:26:21 +0800 Subject: [PATCH] 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)