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)