split subiquity.core into subiquity.client.client and subiquity.server.server
This commit is contained in:
parent
a70d301cfd
commit
aad036d925
|
@ -1,4 +1,7 @@
|
||||||
[encoding: UTF-8]
|
[encoding: UTF-8]
|
||||||
|
subiquity/client/client.py
|
||||||
|
subiquity/client/__init__.py
|
||||||
|
subiquity/client/keycodes.py
|
||||||
subiquity/cmd/common.py
|
subiquity/cmd/common.py
|
||||||
subiquity/cmd/__init__.py
|
subiquity/cmd/__init__.py
|
||||||
subiquity/cmd/schema.py
|
subiquity/cmd/schema.py
|
||||||
|
@ -61,7 +64,6 @@ subiquitycore/models/network.py
|
||||||
subiquitycore/netplan.py
|
subiquitycore/netplan.py
|
||||||
subiquitycore/palette.py
|
subiquitycore/palette.py
|
||||||
subiquitycore/prober.py
|
subiquitycore/prober.py
|
||||||
subiquity/core.py
|
|
||||||
subiquitycore/screen.py
|
subiquitycore/screen.py
|
||||||
subiquitycore/signals.py
|
subiquitycore/signals.py
|
||||||
subiquitycore/snapd.py
|
subiquitycore/snapd.py
|
||||||
|
@ -100,7 +102,6 @@ subiquitycore/view.py
|
||||||
subiquity/__init__.py
|
subiquity/__init__.py
|
||||||
subiquity/journald.py
|
subiquity/journald.py
|
||||||
subiquity/keyboard.py
|
subiquity/keyboard.py
|
||||||
subiquity/keycodes.py
|
|
||||||
subiquity/lockfile.py
|
subiquity/lockfile.py
|
||||||
subiquity/__main__.py
|
subiquity/__main__.py
|
||||||
subiquity/models/filesystem.py
|
subiquity/models/filesystem.py
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
|
@ -0,0 +1,455 @@
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from subiquitycore.async_helpers import (
|
||||||
|
run_in_thread,
|
||||||
|
)
|
||||||
|
from subiquitycore.screen import is_linux_tty
|
||||||
|
from subiquitycore.tuicontroller import Skip
|
||||||
|
from subiquitycore.tui import TuiApplication
|
||||||
|
from subiquitycore.view import BaseView
|
||||||
|
|
||||||
|
from subiquity.client.controller import Confirm
|
||||||
|
from subiquity.client.keycodes import (
|
||||||
|
DummyKeycodesFilter,
|
||||||
|
KeyCodesFilter,
|
||||||
|
)
|
||||||
|
from subiquity.common.api.client import make_client_for_conn
|
||||||
|
from subiquity.common.apidef import API
|
||||||
|
from subiquity.common.errorreport import (
|
||||||
|
ErrorReporter,
|
||||||
|
)
|
||||||
|
from subiquity.common.serialize import from_json
|
||||||
|
from subiquity.common.types import (
|
||||||
|
ApplicationState,
|
||||||
|
ErrorReportKind,
|
||||||
|
ErrorReportRef,
|
||||||
|
InstallState,
|
||||||
|
)
|
||||||
|
from subiquity.journald import journald_listen
|
||||||
|
from subiquity.ui.frame import SubiquityUI
|
||||||
|
from subiquity.ui.views.error import ErrorReportStretchy
|
||||||
|
from subiquity.ui.views.help import HelpMenu
|
||||||
|
from subiquity.ui.views.installprogress import (
|
||||||
|
InstallConfirmation,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger('subiquity.client.client')
|
||||||
|
|
||||||
|
|
||||||
|
class Abort(Exception):
|
||||||
|
def __init__(self, error_report_ref):
|
||||||
|
self.error_report_ref = error_report_ref
|
||||||
|
|
||||||
|
|
||||||
|
DEBUG_SHELL_INTRO = _("""\
|
||||||
|
Installer shell session activated.
|
||||||
|
|
||||||
|
This shell session is running inside the installer environment. You
|
||||||
|
will be returned to the installer when this shell is exited, for
|
||||||
|
example by typing Control-D or 'exit'.
|
||||||
|
|
||||||
|
Be aware that this is an ephemeral environment. Changes to this
|
||||||
|
environment will not survive a reboot. If the install has started, the
|
||||||
|
installed system will be mounted at /target.""")
|
||||||
|
|
||||||
|
|
||||||
|
class SubiquityClient(TuiApplication):
|
||||||
|
|
||||||
|
snapd_socket_path = '/run/snapd.socket'
|
||||||
|
|
||||||
|
from subiquity.client import controllers as controllers_mod
|
||||||
|
project = "subiquity"
|
||||||
|
|
||||||
|
def make_model(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def make_ui(self):
|
||||||
|
return SubiquityUI(self, self.help_menu)
|
||||||
|
|
||||||
|
controllers = []
|
||||||
|
|
||||||
|
def __init__(self, opts):
|
||||||
|
if is_linux_tty():
|
||||||
|
self.input_filter = KeyCodesFilter()
|
||||||
|
else:
|
||||||
|
self.input_filter = DummyKeycodesFilter()
|
||||||
|
|
||||||
|
self.help_menu = HelpMenu(self)
|
||||||
|
super().__init__(opts)
|
||||||
|
self.interactive = None
|
||||||
|
self.server_updated = None
|
||||||
|
self.restarting_server = False
|
||||||
|
self.global_overlays = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.our_tty = os.ttyname(0)
|
||||||
|
except OSError:
|
||||||
|
self.our_tty = "not a tty"
|
||||||
|
|
||||||
|
self.conn = aiohttp.UnixConnector(self.opts.socket)
|
||||||
|
self.client = make_client_for_conn(API, self.conn, self.resp_hook)
|
||||||
|
|
||||||
|
self.error_reporter = ErrorReporter(
|
||||||
|
self.context.child("ErrorReporter"), self.opts.dry_run, self.root,
|
||||||
|
self.client)
|
||||||
|
|
||||||
|
self.note_data_for_apport("SnapUpdated", str(self.updated))
|
||||||
|
self.note_data_for_apport("UsingAnswers", str(bool(self.answers)))
|
||||||
|
|
||||||
|
async def _restart_server(self):
|
||||||
|
log.debug("_restart_server")
|
||||||
|
try:
|
||||||
|
await self.client.meta.restart.POST()
|
||||||
|
except aiohttp.ServerDisconnectedError:
|
||||||
|
pass
|
||||||
|
self.restart(remove_last_screen=False)
|
||||||
|
|
||||||
|
def restart(self, remove_last_screen=True, restart_server=False):
|
||||||
|
log.debug(f"restart {remove_last_screen} {restart_server}")
|
||||||
|
if remove_last_screen:
|
||||||
|
self._remove_last_screen()
|
||||||
|
if restart_server:
|
||||||
|
self.restarting_server = 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 = ['snap', 'run', 'subiquity']
|
||||||
|
if self.opts.dry_run:
|
||||||
|
cmdline = [
|
||||||
|
sys.executable, '-m', 'subiquity.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)
|
||||||
|
|
||||||
|
def resp_hook(self, response):
|
||||||
|
headers = response.headers
|
||||||
|
if 'x-updated' in headers:
|
||||||
|
if self.server_updated is None:
|
||||||
|
self.server_updated = headers['x-updated']
|
||||||
|
elif self.server_updated != headers['x-updated']:
|
||||||
|
self.restart(remove_last_screen=False)
|
||||||
|
status = headers.get('x-status')
|
||||||
|
if status == 'skip':
|
||||||
|
raise Skip
|
||||||
|
elif status == 'confirm':
|
||||||
|
raise Confirm
|
||||||
|
if headers.get('x-error-report') is not None:
|
||||||
|
ref = from_json(ErrorReportRef, headers['x-error-report'])
|
||||||
|
raise Abort(ref)
|
||||||
|
try:
|
||||||
|
response.raise_for_status()
|
||||||
|
except aiohttp.ClientError:
|
||||||
|
report = self.error_reporter.make_apport_report(
|
||||||
|
ErrorReportKind.SERVER_REQUEST_FAIL,
|
||||||
|
"request to {}".format(response.url.path))
|
||||||
|
raise Abort(report.ref())
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def noninteractive_confirmation(self):
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
yes = _('yes')
|
||||||
|
no = _('no')
|
||||||
|
answer = no
|
||||||
|
print(_("Confirmation is required to continue."))
|
||||||
|
print(_("Add 'autoinstall' to your kernel command line to avoid this"))
|
||||||
|
print()
|
||||||
|
prompt = "\n\n{} ({}|{})".format(
|
||||||
|
_("Continue with autoinstall?"), yes, no)
|
||||||
|
while answer != yes:
|
||||||
|
print(prompt)
|
||||||
|
answer = await run_in_thread(input)
|
||||||
|
await self.confirm_install()
|
||||||
|
|
||||||
|
async def noninteractive_watch_install_state(self):
|
||||||
|
install_state = None
|
||||||
|
confirm_task = None
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
install_status = await self.client.install.status.GET(
|
||||||
|
cur=install_state)
|
||||||
|
install_state = install_status.state
|
||||||
|
except aiohttp.ClientError:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
continue
|
||||||
|
if install_state == InstallState.NEEDS_CONFIRMATION:
|
||||||
|
if confirm_task is not None:
|
||||||
|
confirm_task = self.aio_loop.create_task(
|
||||||
|
self.noninteractive_confirmation())
|
||||||
|
elif confirm_task is not None:
|
||||||
|
confirm_task.cancel()
|
||||||
|
confirm_task = None
|
||||||
|
|
||||||
|
def subiquity_event_noninteractive(self, event):
|
||||||
|
if event['SUBIQUITY_EVENT_TYPE'] == 'start':
|
||||||
|
print('start: ' + event["MESSAGE"])
|
||||||
|
elif event['SUBIQUITY_EVENT_TYPE'] == 'finish':
|
||||||
|
print('finish: ' + event["MESSAGE"])
|
||||||
|
context_name = event.get('SUBIQUITY_CONTEXT_NAME', '')
|
||||||
|
if context_name == 'subiquity/Reboot/reboot':
|
||||||
|
self.exit()
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
print("connecting...", end='', flush=True)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
status = await self.client.meta.status.GET()
|
||||||
|
except aiohttp.ClientError:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
print(".", end='', flush=True)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
print()
|
||||||
|
self.event_syslog_id = status.event_syslog_id
|
||||||
|
if status.state == ApplicationState.STARTING:
|
||||||
|
print("server is starting...", end='', flush=True)
|
||||||
|
while status.state == ApplicationState.STARTING:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
print(".", end='', flush=True)
|
||||||
|
status = await self.client.meta.status.GET()
|
||||||
|
print()
|
||||||
|
if status.state == ApplicationState.EARLY_COMMANDS:
|
||||||
|
print("running early commands...")
|
||||||
|
fd = journald_listen(
|
||||||
|
self.aio_loop,
|
||||||
|
[status.early_commands_syslog_id],
|
||||||
|
lambda e: print(e['MESSAGE']))
|
||||||
|
status.state = await self.client.meta.status.GET(cur=status.state)
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
self.aio_loop.remove_reader(fd)
|
||||||
|
return status
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
status = await self.connect()
|
||||||
|
if status.state == ApplicationState.INTERACTIVE:
|
||||||
|
self.interactive = True
|
||||||
|
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)
|
||||||
|
self.error_reporter.load_reports()
|
||||||
|
for report in self.error_reporter.reports:
|
||||||
|
if report.kind == ErrorReportKind.UI and not report.seen:
|
||||||
|
self.show_error_report(report.ref())
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.interactive = False
|
||||||
|
if self.opts.run_on_serial:
|
||||||
|
# Thanks to the fact that we are launched with agetty's
|
||||||
|
# --skip-login option, on serial lines we can end up starting
|
||||||
|
# with some strange terminal settings (see the docs for
|
||||||
|
# --skip-login in agetty(8)). For an interactive install this
|
||||||
|
# does not matter as the settings will soon be clobbered but
|
||||||
|
# for a non-interactive one we need to clear things up or the
|
||||||
|
# prompting for confirmation will be confusing.
|
||||||
|
os.system('stty sane')
|
||||||
|
journald_listen(
|
||||||
|
self.aio_loop,
|
||||||
|
[status.event_syslog_id],
|
||||||
|
self.subiquity_event_noninteractive,
|
||||||
|
seek=True)
|
||||||
|
self.aio_loop.create_task(
|
||||||
|
self.noninteractive_watch_install_state())
|
||||||
|
|
||||||
|
def _exception_handler(self, loop, context):
|
||||||
|
exc = context.get('exception')
|
||||||
|
if self.restarting_server:
|
||||||
|
log.debug('ignoring %s %s during restart', exc, type(exc))
|
||||||
|
return
|
||||||
|
if isinstance(exc, Abort):
|
||||||
|
self.show_error_report(exc.error_report_ref)
|
||||||
|
return
|
||||||
|
super()._exception_handler(loop, context)
|
||||||
|
|
||||||
|
def extra_urwid_loop_args(self):
|
||||||
|
return dict(input_filter=self.input_filter.filter)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
super().run()
|
||||||
|
except Exception:
|
||||||
|
print("generating crash report")
|
||||||
|
try:
|
||||||
|
report = self.make_apport_report(
|
||||||
|
ErrorReportKind.UI, "Installer UI", interrupt=False,
|
||||||
|
wait=True)
|
||||||
|
if report is not None:
|
||||||
|
print("report saved to {path}".format(path=report.path))
|
||||||
|
except Exception:
|
||||||
|
print("report generation failed")
|
||||||
|
traceback.print_exc()
|
||||||
|
Error = getattr(self.controllers, "Error", None)
|
||||||
|
if Error is not None and Error.cmds:
|
||||||
|
new_loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(new_loop)
|
||||||
|
new_loop.run_until_complete(Error.run())
|
||||||
|
if self.interactive:
|
||||||
|
self._remove_last_screen()
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
traceback.print_exc()
|
||||||
|
signal.pause()
|
||||||
|
finally:
|
||||||
|
if self.opts.server_pid:
|
||||||
|
print('killing server {}'.format(self.opts.server_pid))
|
||||||
|
pid = int(self.opts.server_pid)
|
||||||
|
os.kill(pid, 2)
|
||||||
|
os.waitpid(pid, 0)
|
||||||
|
|
||||||
|
async def confirm_install(self):
|
||||||
|
await self.client.meta.confirm.POST(self.our_tty)
|
||||||
|
|
||||||
|
def add_global_overlay(self, overlay):
|
||||||
|
self.global_overlays.append(overlay)
|
||||||
|
if isinstance(self.ui.body, BaseView):
|
||||||
|
self.ui.body.show_stretchy_overlay(overlay)
|
||||||
|
|
||||||
|
def remove_global_overlay(self, overlay):
|
||||||
|
self.global_overlays.remove(overlay)
|
||||||
|
if isinstance(self.ui.body, BaseView):
|
||||||
|
self.ui.body.remove_overlay(overlay)
|
||||||
|
|
||||||
|
def _remove_last_screen(self):
|
||||||
|
last_screen = self.state_path('last-screen')
|
||||||
|
if os.path.exists(last_screen):
|
||||||
|
os.unlink(last_screen)
|
||||||
|
|
||||||
|
def exit(self):
|
||||||
|
self._remove_last_screen()
|
||||||
|
super().exit()
|
||||||
|
|
||||||
|
def select_initial_screen(self):
|
||||||
|
last_screen = None
|
||||||
|
if self.updated:
|
||||||
|
state_path = self.state_path('last-screen')
|
||||||
|
if os.path.exists(state_path):
|
||||||
|
with open(state_path) as fp:
|
||||||
|
last_screen = fp.read().strip()
|
||||||
|
index = 0
|
||||||
|
if last_screen:
|
||||||
|
for i, controller in enumerate(self.controllers.instances):
|
||||||
|
if controller.name == last_screen:
|
||||||
|
index = i
|
||||||
|
self.aio_loop.create_task(self._select_initial_screen(index))
|
||||||
|
|
||||||
|
async def _select_initial_screen(self, index):
|
||||||
|
endpoint_names = []
|
||||||
|
for c in self.controllers.instances[:index]:
|
||||||
|
if c.endpoint_name:
|
||||||
|
endpoint_names.append(c.endpoint_name)
|
||||||
|
if endpoint_names:
|
||||||
|
await self.client.meta.mark_configured.POST(endpoint_names)
|
||||||
|
self.controllers.index = index - 1
|
||||||
|
self.next_screen()
|
||||||
|
|
||||||
|
async def move_screen(self, increment, coro):
|
||||||
|
try:
|
||||||
|
await super().move_screen(increment, coro)
|
||||||
|
except Confirm:
|
||||||
|
self.show_confirm_install()
|
||||||
|
|
||||||
|
def show_confirm_install(self):
|
||||||
|
log.debug("showing InstallConfirmation over %s", self.ui.body)
|
||||||
|
self.add_global_overlay(InstallConfirmation(self))
|
||||||
|
|
||||||
|
async def make_view_for_controller(self, new):
|
||||||
|
view = await super().make_view_for_controller(new)
|
||||||
|
if new.answers:
|
||||||
|
self.aio_loop.call_soon(new.run_answers)
|
||||||
|
with open(self.state_path('last-screen'), 'w') as fp:
|
||||||
|
fp.write(new.name)
|
||||||
|
return view
|
||||||
|
|
||||||
|
def show_progress(self):
|
||||||
|
self.ui.set_body(self.controllers.Progress.progress_view)
|
||||||
|
|
||||||
|
def unhandled_input(self, key):
|
||||||
|
if key == 'f1':
|
||||||
|
if not self.ui.right_icon.current_help:
|
||||||
|
self.ui.right_icon.open_pop_up()
|
||||||
|
elif key in ['ctrl z', 'f2']:
|
||||||
|
self.debug_shell()
|
||||||
|
elif self.opts.dry_run:
|
||||||
|
self.unhandled_input_dry_run(key)
|
||||||
|
else:
|
||||||
|
super().unhandled_input(key)
|
||||||
|
|
||||||
|
def unhandled_input_dry_run(self, key):
|
||||||
|
if key in ['ctrl e', 'ctrl r']:
|
||||||
|
interrupt = key == 'ctrl e'
|
||||||
|
try:
|
||||||
|
1/0
|
||||||
|
except ZeroDivisionError:
|
||||||
|
self.make_apport_report(
|
||||||
|
ErrorReportKind.UNKNOWN, "example", interrupt=interrupt)
|
||||||
|
elif key == 'ctrl u':
|
||||||
|
1/0
|
||||||
|
elif key == 'ctrl b':
|
||||||
|
self.aio_loop.create_task(self.client.dry_run.crash.GET())
|
||||||
|
else:
|
||||||
|
super().unhandled_input(key)
|
||||||
|
|
||||||
|
def debug_shell(self, after_hook=None):
|
||||||
|
|
||||||
|
def _before():
|
||||||
|
os.system("clear")
|
||||||
|
print(DEBUG_SHELL_INTRO)
|
||||||
|
|
||||||
|
self.run_command_in_foreground(
|
||||||
|
["bash"], before_hook=_before, after_hook=after_hook, cwd='/')
|
||||||
|
|
||||||
|
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, *, interrupt, wait=False, **kw):
|
||||||
|
report = self.error_reporter.make_apport_report(
|
||||||
|
kind, thing, wait=wait, **kw)
|
||||||
|
|
||||||
|
if report is not None and interrupt:
|
||||||
|
self.show_error_report(report.ref())
|
||||||
|
|
||||||
|
return report
|
||||||
|
|
||||||
|
def show_error_report(self, error_ref):
|
||||||
|
log.debug("show_error_report %r", error_ref.base)
|
||||||
|
if isinstance(self.ui.body, BaseView):
|
||||||
|
w = getattr(self.ui.body._w, 'stretchy', None)
|
||||||
|
if isinstance(w, ErrorReportStretchy):
|
||||||
|
# Don't show an error if already looking at one.
|
||||||
|
return
|
||||||
|
self.add_global_overlay(ErrorReportStretchy(self, error_ref))
|
|
@ -19,7 +19,7 @@ import os
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
log = logging.getLogger('subiquity.keycodes')
|
log = logging.getLogger('subiquity.client.keycodes')
|
||||||
|
|
||||||
# /usr/include/linux/kd.h
|
# /usr/include/linux/kd.h
|
||||||
K_RAW = 0x00
|
K_RAW = 0x00
|
|
@ -34,6 +34,35 @@ def make_server_args_parser():
|
||||||
dest='dry_run',
|
dest='dry_run',
|
||||||
help='menu-only, do not call installer function')
|
help='menu-only, do not call installer function')
|
||||||
parser.add_argument('--socket')
|
parser.add_argument('--socket')
|
||||||
|
parser.add_argument('--machine-config', metavar='CONFIG',
|
||||||
|
dest='machine_config',
|
||||||
|
help="Don't Probe. Use probe data file")
|
||||||
|
parser.add_argument('--source', default=[], action='append',
|
||||||
|
dest='sources', metavar='URL',
|
||||||
|
help='install from url instead of default.')
|
||||||
|
parser.add_argument('--bootloader',
|
||||||
|
choices=['none', 'bios', 'prep', 'uefi'],
|
||||||
|
help='Override style of bootloader to use')
|
||||||
|
parser.add_argument('--autoinstall', action='store')
|
||||||
|
with open('/proc/cmdline') as fp:
|
||||||
|
cmdline = fp.read()
|
||||||
|
parser.add_argument('--kernel-cmdline', action='store', default=cmdline)
|
||||||
|
parser.add_argument(
|
||||||
|
'--snaps-from-examples', action='store_const', const=True,
|
||||||
|
dest="snaps_from_examples",
|
||||||
|
help=("Load snap details from examples/snaps instead of store. "
|
||||||
|
"Default in dry-run mode. "
|
||||||
|
"See examples/snaps/README.md for more."))
|
||||||
|
parser.add_argument(
|
||||||
|
'--no-snaps-from-examples', action='store_const', const=False,
|
||||||
|
dest="snaps_from_examples",
|
||||||
|
help=("Load snap details from store instead of examples. "
|
||||||
|
"Default in when not in dry-run mode. "
|
||||||
|
"See examples/snaps/README.md for more."))
|
||||||
|
parser.add_argument(
|
||||||
|
'--snap-section', action='store', default='server',
|
||||||
|
help=("Show snaps from this section of the store in the snap "
|
||||||
|
"list screen."))
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,6 +76,8 @@ def main():
|
||||||
opts = parser.parse_args(sys.argv[1:])
|
opts = parser.parse_args(sys.argv[1:])
|
||||||
logdir = LOGDIR
|
logdir = LOGDIR
|
||||||
if opts.dry_run:
|
if opts.dry_run:
|
||||||
|
if opts.snaps_from_examples is None:
|
||||||
|
opts.snaps_from_examples = True
|
||||||
logdir = ".subiquity"
|
logdir = ".subiquity"
|
||||||
if opts.socket is None:
|
if opts.socket is None:
|
||||||
if opts.dry_run:
|
if opts.dry_run:
|
||||||
|
@ -55,6 +86,17 @@ def main():
|
||||||
opts.socket = '/run/subiquity/socket'
|
opts.socket = '/run/subiquity/socket'
|
||||||
os.makedirs(os.path.basename(opts.socket), exist_ok=True)
|
os.makedirs(os.path.basename(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"))
|
||||||
|
logging.getLogger('probert').addHandler(handler)
|
||||||
|
handler.addFilter(lambda rec: rec.name != 'probert.network')
|
||||||
|
logging.getLogger('curtin').addHandler(handler)
|
||||||
|
logging.getLogger('block-discover').addHandler(handler)
|
||||||
|
|
||||||
logfiles = setup_logger(dir=logdir, base='subiquity-server')
|
logfiles = setup_logger(dir=logdir, base='subiquity-server')
|
||||||
|
|
||||||
logger = logging.getLogger('subiquity')
|
logger = logging.getLogger('subiquity')
|
||||||
|
@ -62,14 +104,14 @@ def main():
|
||||||
logger.info("Starting Subiquity server revision {}".format(version))
|
logger.info("Starting Subiquity server revision {}".format(version))
|
||||||
logger.info("Arguments passed: {}".format(sys.argv))
|
logger.info("Arguments passed: {}".format(sys.argv))
|
||||||
|
|
||||||
subiquity_interface = SubiquityServer(opts)
|
server = SubiquityServer(opts, block_log_dir)
|
||||||
|
|
||||||
subiquity_interface.note_file_for_apport(
|
server.note_file_for_apport(
|
||||||
"InstallerServerLog", logfiles['debug'])
|
"InstallerServerLog", logfiles['debug'])
|
||||||
subiquity_interface.note_file_for_apport(
|
server.note_file_for_apport(
|
||||||
"InstallerServerLogInfo", logfiles['info'])
|
"InstallerServerLogInfo", logfiles['info'])
|
||||||
|
|
||||||
subiquity_interface.run()
|
server.run()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -64,12 +64,6 @@ def make_client_args_parser():
|
||||||
parser.add_argument('--unicode', action='store_false',
|
parser.add_argument('--unicode', action='store_false',
|
||||||
dest='ascii',
|
dest='ascii',
|
||||||
help='Run the installer in unicode mode.')
|
help='Run the installer in unicode mode.')
|
||||||
parser.add_argument('--machine-config', metavar='CONFIG',
|
|
||||||
dest='machine_config',
|
|
||||||
help="Don't Probe. Use probe data file")
|
|
||||||
parser.add_argument('--bootloader',
|
|
||||||
choices=['none', 'bios', 'prep', 'uefi'],
|
|
||||||
help='Override style of bootloader to use')
|
|
||||||
parser.add_argument('--screens', action='append', dest='screens',
|
parser.add_argument('--screens', action='append', dest='screens',
|
||||||
default=[])
|
default=[])
|
||||||
parser.add_argument('--script', metavar="SCRIPT", action='append',
|
parser.add_argument('--script', metavar="SCRIPT", action='append',
|
||||||
|
@ -79,29 +73,6 @@ def make_client_args_parser():
|
||||||
parser.add_argument('--click', metavar="PAT", action=ClickAction,
|
parser.add_argument('--click', metavar="PAT", action=ClickAction,
|
||||||
help='Synthesize a click on a button matching PAT')
|
help='Synthesize a click on a button matching PAT')
|
||||||
parser.add_argument('--answers')
|
parser.add_argument('--answers')
|
||||||
parser.add_argument('--autoinstall', action='store')
|
|
||||||
with open('/proc/cmdline') as fp:
|
|
||||||
cmdline = fp.read()
|
|
||||||
parser.add_argument('--kernel-cmdline', action='store', default=cmdline)
|
|
||||||
parser.add_argument('--source', default=[], action='append',
|
|
||||||
dest='sources', metavar='URL',
|
|
||||||
help='install from url instead of default.')
|
|
||||||
parser.add_argument(
|
|
||||||
'--snaps-from-examples', action='store_const', const=True,
|
|
||||||
dest="snaps_from_examples",
|
|
||||||
help=("Load snap details from examples/snaps instead of store. "
|
|
||||||
"Default in dry-run mode. "
|
|
||||||
"See examples/snaps/README.md for more."))
|
|
||||||
parser.add_argument(
|
|
||||||
'--no-snaps-from-examples', action='store_const', const=False,
|
|
||||||
dest="snaps_from_examples",
|
|
||||||
help=("Load snap details from store instead of examples. "
|
|
||||||
"Default in when not in dry-run mode. "
|
|
||||||
"See examples/snaps/README.md for more."))
|
|
||||||
parser.add_argument(
|
|
||||||
'--snap-section', action='store', default='server',
|
|
||||||
help=("Show snaps from this section of the store in the snap "
|
|
||||||
"list screen."))
|
|
||||||
parser.add_argument('--server-pid')
|
parser.add_argument('--server-pid')
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
@ -113,7 +84,7 @@ def main():
|
||||||
setup_environment()
|
setup_environment()
|
||||||
# setup_environment sets $APPORT_DATA_DIR which must be set before
|
# setup_environment sets $APPORT_DATA_DIR which must be set before
|
||||||
# apport is imported, which is done by this import:
|
# apport is imported, which is done by this import:
|
||||||
from subiquity.core import Subiquity
|
from subiquity.client.client import SubiquityClient
|
||||||
parser = make_client_args_parser()
|
parser = make_client_args_parser()
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
if '--dry-run' in args:
|
if '--dry-run' in args:
|
||||||
|
@ -143,10 +114,8 @@ def main():
|
||||||
os.makedirs(os.path.basename(opts.socket), exist_ok=True)
|
os.makedirs(os.path.basename(opts.socket), exist_ok=True)
|
||||||
logdir = LOGDIR
|
logdir = LOGDIR
|
||||||
if opts.dry_run:
|
if opts.dry_run:
|
||||||
if opts.snaps_from_examples is None:
|
|
||||||
opts.snaps_from_examples = True
|
|
||||||
logdir = ".subiquity"
|
logdir = ".subiquity"
|
||||||
logfiles = setup_logger(dir=logdir, base='subiquity')
|
logfiles = setup_logger(dir=logdir, base='subiquity-client')
|
||||||
|
|
||||||
logger = logging.getLogger('subiquity')
|
logger = logging.getLogger('subiquity')
|
||||||
version = os.environ.get("SNAP_REVISION", "unknown")
|
version = os.environ.get("SNAP_REVISION", "unknown")
|
||||||
|
@ -178,17 +147,6 @@ def main():
|
||||||
"cloud-init status: %r, assumed disabled",
|
"cloud-init status: %r, assumed disabled",
|
||||||
status_txt)
|
status_txt)
|
||||||
|
|
||||||
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"))
|
|
||||||
logging.getLogger('probert').addHandler(handler)
|
|
||||||
handler.addFilter(lambda rec: rec.name != 'probert.network')
|
|
||||||
logging.getLogger('curtin').addHandler(handler)
|
|
||||||
logging.getLogger('block-discover').addHandler(handler)
|
|
||||||
|
|
||||||
if opts.ssh:
|
if opts.ssh:
|
||||||
from subiquity.ui.views.help import (
|
from subiquity.ui.views.help import (
|
||||||
ssh_help_texts, get_installer_password)
|
ssh_help_texts, get_installer_password)
|
||||||
|
@ -219,7 +177,7 @@ def main():
|
||||||
opts.answers.close()
|
opts.answers.close()
|
||||||
opts.answers = None
|
opts.answers = None
|
||||||
|
|
||||||
subiquity_interface = Subiquity(opts, block_log_dir)
|
subiquity_interface = SubiquityClient(opts)
|
||||||
|
|
||||||
subiquity_interface.note_file_for_apport(
|
subiquity_interface.note_file_for_apport(
|
||||||
"InstallerLog", logfiles['debug'])
|
"InstallerLog", logfiles['debug'])
|
||||||
|
|
|
@ -13,9 +13,12 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
from subiquity.common.api.defs import api
|
from subiquity.common.api.defs import api
|
||||||
from subiquity.common.types import (
|
from subiquity.common.types import (
|
||||||
ApplicationState,
|
ApplicationState,
|
||||||
|
ApplicationStatus,
|
||||||
ErrorReportRef,
|
ErrorReportRef,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -26,9 +29,18 @@ class API:
|
||||||
|
|
||||||
class meta:
|
class meta:
|
||||||
class status:
|
class status:
|
||||||
def GET() -> ApplicationState:
|
def GET(cur: Optional[ApplicationState] = None) \
|
||||||
|
-> ApplicationStatus:
|
||||||
"""Get the installer state."""
|
"""Get the installer state."""
|
||||||
|
|
||||||
|
class mark_configured:
|
||||||
|
def POST(endpoint_names: List[str]) -> None:
|
||||||
|
"""Mark the controllers for endpoint_names as configured."""
|
||||||
|
|
||||||
|
class confirm:
|
||||||
|
def POST(tty: str) -> None:
|
||||||
|
"""Confirm that the installation should proceed."""
|
||||||
|
|
||||||
class restart:
|
class restart:
|
||||||
def POST() -> None:
|
def POST() -> None:
|
||||||
"""Restart the server process."""
|
"""Restart the server process."""
|
||||||
|
|
|
@ -27,6 +27,17 @@ import attr
|
||||||
|
|
||||||
class ApplicationState(enum.Enum):
|
class ApplicationState(enum.Enum):
|
||||||
STARTING = enum.auto()
|
STARTING = enum.auto()
|
||||||
|
EARLY_COMMANDS = enum.auto()
|
||||||
|
INTERACTIVE = enum.auto()
|
||||||
|
NON_INTERACTIVE = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(auto_attribs=True)
|
||||||
|
class ApplicationStatus:
|
||||||
|
state: ApplicationState
|
||||||
|
early_commands_syslog_id: str
|
||||||
|
log_syslog_id: str
|
||||||
|
event_syslog_id: str
|
||||||
|
|
||||||
|
|
||||||
class ErrorReportState(enum.Enum):
|
class ErrorReportState(enum.Enum):
|
||||||
|
|
|
@ -1,648 +0,0 @@
|
||||||
# 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import shlex
|
|
||||||
import signal
|
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
import jsonschema
|
|
||||||
|
|
||||||
from systemd import journal
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from subiquitycore.async_helpers import (
|
|
||||||
run_in_thread,
|
|
||||||
schedule_task,
|
|
||||||
)
|
|
||||||
from subiquitycore.prober import Prober
|
|
||||||
from subiquitycore.screen import is_linux_tty
|
|
||||||
from subiquitycore.tuicontroller import Skip
|
|
||||||
from subiquitycore.tui import TuiApplication
|
|
||||||
from subiquitycore.snapd import (
|
|
||||||
AsyncSnapd,
|
|
||||||
FakeSnapdConnection,
|
|
||||||
SnapdConnection,
|
|
||||||
)
|
|
||||||
from subiquitycore.view import BaseView
|
|
||||||
|
|
||||||
from subiquity.common.api.client import make_client_for_conn
|
|
||||||
from subiquity.common.apidef import API
|
|
||||||
from subiquity.common.errorreport import (
|
|
||||||
ErrorReporter,
|
|
||||||
)
|
|
||||||
from subiquity.common.serialize import from_json
|
|
||||||
from subiquity.common.types import (
|
|
||||||
ErrorReportKind,
|
|
||||||
ErrorReportRef,
|
|
||||||
)
|
|
||||||
from subiquity.controller import Confirm
|
|
||||||
from subiquity.journald import journald_listen
|
|
||||||
from subiquity.keycodes import (
|
|
||||||
DummyKeycodesFilter,
|
|
||||||
KeyCodesFilter,
|
|
||||||
)
|
|
||||||
from subiquity.lockfile import Lockfile
|
|
||||||
from subiquity.models.subiquity import SubiquityModel
|
|
||||||
from subiquity.ui.frame import SubiquityUI
|
|
||||||
from subiquity.ui.views.error import ErrorReportStretchy
|
|
||||||
from subiquity.ui.views.help import HelpMenu
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger('subiquity.core')
|
|
||||||
|
|
||||||
|
|
||||||
class Abort(Exception):
|
|
||||||
def __init__(self, error_report_ref):
|
|
||||||
self.error_report_ref = error_report_ref
|
|
||||||
|
|
||||||
|
|
||||||
DEBUG_SHELL_INTRO = _("""\
|
|
||||||
Installer shell session activated.
|
|
||||||
|
|
||||||
This shell session is running inside the installer environment. You
|
|
||||||
will be returned to the installer when this shell is exited, for
|
|
||||||
example by typing Control-D or 'exit'.
|
|
||||||
|
|
||||||
Be aware that this is an ephemeral environment. Changes to this
|
|
||||||
environment will not survive a reboot. If the install has started, the
|
|
||||||
installed system will be mounted at /target.""")
|
|
||||||
|
|
||||||
|
|
||||||
class Subiquity(TuiApplication):
|
|
||||||
|
|
||||||
snapd_socket_path = '/run/snapd.socket'
|
|
||||||
|
|
||||||
base_schema = {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': {
|
|
||||||
'version': {
|
|
||||||
'type': 'integer',
|
|
||||||
'minimum': 1,
|
|
||||||
'maximum': 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'required': ['version'],
|
|
||||||
'additionalProperties': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
from subiquity import controllers as controllers_mod
|
|
||||||
project = "subiquity"
|
|
||||||
|
|
||||||
def make_model(self):
|
|
||||||
root = '/'
|
|
||||||
if self.opts.dry_run:
|
|
||||||
root = os.path.abspath('.subiquity')
|
|
||||||
return SubiquityModel(root, self.opts.sources)
|
|
||||||
|
|
||||||
def make_ui(self):
|
|
||||||
return SubiquityUI(self, self.help_menu)
|
|
||||||
|
|
||||||
controllers = [
|
|
||||||
"Early",
|
|
||||||
"Reporting",
|
|
||||||
"Error",
|
|
||||||
"Userdata",
|
|
||||||
"Package",
|
|
||||||
"Debconf",
|
|
||||||
"Welcome",
|
|
||||||
"Refresh",
|
|
||||||
"Keyboard",
|
|
||||||
"Zdev",
|
|
||||||
"Network",
|
|
||||||
"Proxy",
|
|
||||||
"Mirror",
|
|
||||||
"Refresh",
|
|
||||||
"Filesystem",
|
|
||||||
"Identity",
|
|
||||||
"SSH",
|
|
||||||
"SnapList",
|
|
||||||
"InstallProgress",
|
|
||||||
"Late",
|
|
||||||
"Reboot",
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, opts, block_log_dir):
|
|
||||||
if is_linux_tty():
|
|
||||||
self.input_filter = KeyCodesFilter()
|
|
||||||
else:
|
|
||||||
self.input_filter = DummyKeycodesFilter()
|
|
||||||
|
|
||||||
self.help_menu = HelpMenu(self)
|
|
||||||
super().__init__(opts)
|
|
||||||
|
|
||||||
self.event_syslog_id = 'subiquity_event.{}'.format(os.getpid())
|
|
||||||
self.log_syslog_id = 'subiquity_log.{}'.format(os.getpid())
|
|
||||||
|
|
||||||
self.server_updated = None
|
|
||||||
self.restarting_server = False
|
|
||||||
self.prober = Prober(opts.machine_config, self.debug_flags)
|
|
||||||
journald_listen(
|
|
||||||
self.aio_loop, ["subiquity"], self.subiquity_event, seek=True)
|
|
||||||
self.event_listeners = []
|
|
||||||
self.install_lock_file = Lockfile(self.state_path("installing"))
|
|
||||||
self.global_overlays = []
|
|
||||||
self.block_log_dir = block_log_dir
|
|
||||||
self.kernel_cmdline = shlex.split(opts.kernel_cmdline)
|
|
||||||
if opts.snaps_from_examples:
|
|
||||||
connection = FakeSnapdConnection(
|
|
||||||
os.path.join(
|
|
||||||
os.path.dirname(
|
|
||||||
os.path.dirname(__file__)),
|
|
||||||
"examples", "snaps"),
|
|
||||||
self.scale_factor)
|
|
||||||
else:
|
|
||||||
connection = SnapdConnection(self.root, self.snapd_socket_path)
|
|
||||||
self.snapd = AsyncSnapd(connection)
|
|
||||||
self.signal.connect_signals([
|
|
||||||
('network-proxy-set', lambda: schedule_task(self._proxy_set())),
|
|
||||||
('network-change', self._network_change),
|
|
||||||
])
|
|
||||||
|
|
||||||
self.conn = aiohttp.UnixConnector(self.opts.socket)
|
|
||||||
self.client = make_client_for_conn(API, self.conn, self.resp_hook)
|
|
||||||
|
|
||||||
self.autoinstall_config = {}
|
|
||||||
self.error_reporter = ErrorReporter(
|
|
||||||
self.context.child("ErrorReporter"), self.opts.dry_run, self.root,
|
|
||||||
self.client)
|
|
||||||
|
|
||||||
self.note_data_for_apport("SnapUpdated", str(self.updated))
|
|
||||||
self.note_data_for_apport("UsingAnswers", str(bool(self.answers)))
|
|
||||||
|
|
||||||
def subiquity_event(self, event):
|
|
||||||
if event["MESSAGE"] == "starting install":
|
|
||||||
if event["_PID"] == os.getpid():
|
|
||||||
return
|
|
||||||
if not self.install_lock_file.is_exclusively_locked():
|
|
||||||
return
|
|
||||||
from subiquity.ui.views.installprogress import (
|
|
||||||
InstallRunning,
|
|
||||||
)
|
|
||||||
tty = self.install_lock_file.read_content()
|
|
||||||
install_running = InstallRunning(self.ui.body, self, tty)
|
|
||||||
self.add_global_overlay(install_running)
|
|
||||||
schedule_task(self._hide_install_running(install_running))
|
|
||||||
|
|
||||||
async def _hide_install_running(self, install_running):
|
|
||||||
# Wait until the install has completed...
|
|
||||||
async with self.install_lock_file.shared():
|
|
||||||
# And remove the overlay.
|
|
||||||
self.remove_global_overlay(install_running)
|
|
||||||
|
|
||||||
async def _restart_server(self):
|
|
||||||
log.debug("_restart_server")
|
|
||||||
try:
|
|
||||||
await self.client.meta.restart.POST()
|
|
||||||
except aiohttp.ServerDisconnectedError:
|
|
||||||
pass
|
|
||||||
self.restart(remove_last_screen=False)
|
|
||||||
|
|
||||||
def restart(self, remove_last_screen=True, restart_server=False):
|
|
||||||
log.debug(f"restart {remove_last_screen} {restart_server}")
|
|
||||||
if remove_last_screen:
|
|
||||||
self._remove_last_screen()
|
|
||||||
if restart_server:
|
|
||||||
self.restarting_server = 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 = ['snap', 'run', 'subiquity']
|
|
||||||
if self.opts.dry_run:
|
|
||||||
cmdline = [
|
|
||||||
sys.executable, '-m', 'subiquity.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)
|
|
||||||
|
|
||||||
def get_primary_tty(self):
|
|
||||||
tty = '/dev/tty1'
|
|
||||||
for work in self.kernel_cmdline:
|
|
||||||
if work.startswith('console='):
|
|
||||||
tty = '/dev/' + work[len('console='):].split(',')[0]
|
|
||||||
return tty
|
|
||||||
|
|
||||||
async def load_autoinstall_config(self):
|
|
||||||
with open(self.opts.autoinstall) as fp:
|
|
||||||
self.autoinstall_config = yaml.safe_load(fp)
|
|
||||||
primary_tty = self.get_primary_tty()
|
|
||||||
try:
|
|
||||||
our_tty = os.ttyname(0)
|
|
||||||
except OSError:
|
|
||||||
# This is a gross hack for testing in travis.
|
|
||||||
our_tty = "/dev/not a tty"
|
|
||||||
if not self.interactive() and our_tty != primary_tty:
|
|
||||||
while True:
|
|
||||||
print(
|
|
||||||
_("the installer running on {tty} will perform the "
|
|
||||||
"autoinstall").format(tty=primary_tty))
|
|
||||||
print()
|
|
||||||
print(_("press enter to start a shell"))
|
|
||||||
input()
|
|
||||||
os.system("cd / && bash")
|
|
||||||
self.controllers.Reporting.start()
|
|
||||||
with self.context.child("core_validation", level="INFO"):
|
|
||||||
jsonschema.validate(self.autoinstall_config, self.base_schema)
|
|
||||||
self.controllers.Reporting.setup_autoinstall()
|
|
||||||
self.controllers.Early.setup_autoinstall()
|
|
||||||
self.controllers.Error.setup_autoinstall()
|
|
||||||
if self.controllers.Early.cmds:
|
|
||||||
stamp_file = self.state_path("early-commands")
|
|
||||||
if our_tty != primary_tty:
|
|
||||||
print(
|
|
||||||
_("waiting for installer running on {tty} to run early "
|
|
||||||
"commands").format(tty=primary_tty))
|
|
||||||
while not os.path.exists(stamp_file):
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
elif not os.path.exists(stamp_file):
|
|
||||||
await self.controllers.Early.run()
|
|
||||||
open(stamp_file, 'w').close()
|
|
||||||
with open(self.opts.autoinstall) as fp:
|
|
||||||
self.autoinstall_config = yaml.safe_load(fp)
|
|
||||||
with self.context.child("core_validation", level="INFO"):
|
|
||||||
jsonschema.validate(self.autoinstall_config, self.base_schema)
|
|
||||||
for controller in self.controllers.instances:
|
|
||||||
controller.setup_autoinstall()
|
|
||||||
if not self.interactive() and self.opts.run_on_serial:
|
|
||||||
# Thanks to the fact that we are launched with agetty's
|
|
||||||
# --skip-login option, on serial lines we can end up starting with
|
|
||||||
# some strange terminal settings (see the docs for --skip-login in
|
|
||||||
# agetty(8)). For an interactive install this does not matter as
|
|
||||||
# the settings will soon be clobbered but for a non-interactive
|
|
||||||
# one we need to clear things up or the prompting for confirmation
|
|
||||||
# in next_screen below will be confusing.
|
|
||||||
os.system('stty sane')
|
|
||||||
|
|
||||||
def resp_hook(self, response):
|
|
||||||
headers = response.headers
|
|
||||||
if 'x-updated' in headers:
|
|
||||||
if self.server_updated is None:
|
|
||||||
self.server_updated = headers['x-updated']
|
|
||||||
elif self.server_updated != headers['x-updated']:
|
|
||||||
self.restart(remove_last_screen=False)
|
|
||||||
status = response.headers.get('x-status')
|
|
||||||
if status == 'skip':
|
|
||||||
raise Skip
|
|
||||||
elif status == 'confirm':
|
|
||||||
raise Confirm
|
|
||||||
if headers.get('x-error-report') is not None:
|
|
||||||
ref = from_json(ErrorReportRef, headers['x-error-report'])
|
|
||||||
raise Abort(ref)
|
|
||||||
try:
|
|
||||||
response.raise_for_status()
|
|
||||||
except aiohttp.ClientError:
|
|
||||||
report = self.error_reporter.make_apport_report(
|
|
||||||
ErrorReportKind.SERVER_REQUEST_FAIL,
|
|
||||||
"request to {}".format(response.url.path))
|
|
||||||
raise Abort(report.ref())
|
|
||||||
return response
|
|
||||||
|
|
||||||
def subiquity_event_noninteractive(self, event):
|
|
||||||
if event['SUBIQUITY_EVENT_TYPE'] == 'start':
|
|
||||||
print('start: ' + event["MESSAGE"])
|
|
||||||
elif event['SUBIQUITY_EVENT_TYPE'] == 'finish':
|
|
||||||
print('finish: ' + event["MESSAGE"])
|
|
||||||
context_name = event.get('SUBIQUITY_CONTEXT_NAME', '')
|
|
||||||
if context_name == 'subiquity/Reboot/reboot':
|
|
||||||
self.exit()
|
|
||||||
|
|
||||||
async def connect(self):
|
|
||||||
print("connecting...", end='', flush=True)
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
await self.client.meta.status.GET()
|
|
||||||
except aiohttp.ClientError:
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
print(".", end='', flush=True)
|
|
||||||
else:
|
|
||||||
print()
|
|
||||||
break
|
|
||||||
|
|
||||||
def load_serialized_state(self):
|
|
||||||
for controller in self.controllers.instances:
|
|
||||||
controller.load_state()
|
|
||||||
|
|
||||||
async def start(self):
|
|
||||||
self.controllers.load_all()
|
|
||||||
self.load_serialized_state()
|
|
||||||
await self.connect()
|
|
||||||
if self.opts.autoinstall is not None:
|
|
||||||
await self.load_autoinstall_config()
|
|
||||||
if not self.interactive() and not self.opts.dry_run:
|
|
||||||
open('/run/casper-no-prompt', 'w').close()
|
|
||||||
interactive = self.interactive()
|
|
||||||
if interactive:
|
|
||||||
journald_listen(
|
|
||||||
self.aio_loop,
|
|
||||||
[self.event_syslog_id],
|
|
||||||
self.controllers.InstallProgress.event)
|
|
||||||
journald_listen(
|
|
||||||
self.aio_loop,
|
|
||||||
[self.log_syslog_id],
|
|
||||||
self.controllers.InstallProgress.log_line)
|
|
||||||
else:
|
|
||||||
journald_listen(
|
|
||||||
self.aio_loop,
|
|
||||||
[self.event_syslog_id],
|
|
||||||
self.subiquity_event_noninteractive,
|
|
||||||
seek=True)
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
await super().start(start_urwid=interactive)
|
|
||||||
if not interactive:
|
|
||||||
self.select_initial_screen()
|
|
||||||
|
|
||||||
def _exception_handler(self, loop, context):
|
|
||||||
exc = context.get('exception')
|
|
||||||
if self.restarting_server:
|
|
||||||
log.debug('ignoring %s %s during restart', exc, type(exc))
|
|
||||||
return
|
|
||||||
if isinstance(exc, Abort):
|
|
||||||
self.show_error_report(exc.error_report_ref)
|
|
||||||
return
|
|
||||||
super()._exception_handler(loop, context)
|
|
||||||
|
|
||||||
def _remove_last_screen(self):
|
|
||||||
last_screen = self.state_path('last-screen')
|
|
||||||
if os.path.exists(last_screen):
|
|
||||||
os.unlink(last_screen)
|
|
||||||
|
|
||||||
def exit(self):
|
|
||||||
self._remove_last_screen()
|
|
||||||
super().exit()
|
|
||||||
|
|
||||||
def extra_urwid_loop_args(self):
|
|
||||||
return dict(input_filter=self.input_filter.filter)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
try:
|
|
||||||
super().run()
|
|
||||||
except Exception:
|
|
||||||
print("generating crash report")
|
|
||||||
try:
|
|
||||||
report = self.make_apport_report(
|
|
||||||
ErrorReportKind.UI, "Installer UI", interrupt=False,
|
|
||||||
wait=True)
|
|
||||||
if report is not None:
|
|
||||||
print("report saved to {path}".format(path=report.path))
|
|
||||||
except Exception:
|
|
||||||
print("report generation failed")
|
|
||||||
traceback.print_exc()
|
|
||||||
Error = getattr(self.controllers, "Error", None)
|
|
||||||
if Error is not None and Error.cmds:
|
|
||||||
new_loop = asyncio.new_event_loop()
|
|
||||||
asyncio.set_event_loop(new_loop)
|
|
||||||
new_loop.run_until_complete(Error.run())
|
|
||||||
if self.interactive():
|
|
||||||
self._remove_last_screen()
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
traceback.print_exc()
|
|
||||||
signal.pause()
|
|
||||||
finally:
|
|
||||||
if self.opts.server_pid:
|
|
||||||
print('killing server {}'.format(self.opts.server_pid))
|
|
||||||
pid = int(self.opts.server_pid)
|
|
||||||
os.kill(pid, 2)
|
|
||||||
os.waitpid(pid, 0)
|
|
||||||
|
|
||||||
def add_event_listener(self, listener):
|
|
||||||
self.event_listeners.append(listener)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def _maybe_push_to_journal(self, event_type, context, description):
|
|
||||||
if not context.get('is-install-context') and self.interactive():
|
|
||||||
controller = context.get('controller')
|
|
||||||
if controller is None or controller.interactive():
|
|
||||||
return
|
|
||||||
if context.get('request'):
|
|
||||||
return
|
|
||||||
indent = context.full_name().count('/') - 2
|
|
||||||
if context.get('is-install-context') and self.interactive():
|
|
||||||
indent -= 1
|
|
||||||
msg = context.description
|
|
||||||
else:
|
|
||||||
msg = context.full_name()
|
|
||||||
if description:
|
|
||||||
msg += ': ' + description
|
|
||||||
msg = ' ' * indent + msg
|
|
||||||
if context.parent:
|
|
||||||
parent_id = str(context.parent.id)
|
|
||||||
else:
|
|
||||||
parent_id = ''
|
|
||||||
journal.send(
|
|
||||||
msg,
|
|
||||||
PRIORITY=context.level,
|
|
||||||
SYSLOG_IDENTIFIER=self.event_syslog_id,
|
|
||||||
SUBIQUITY_CONTEXT_NAME=context.full_name(),
|
|
||||||
SUBIQUITY_EVENT_TYPE=event_type,
|
|
||||||
SUBIQUITY_CONTEXT_ID=str(context.id),
|
|
||||||
SUBIQUITY_CONTEXT_PARENT_ID=parent_id)
|
|
||||||
|
|
||||||
async def confirm_install(self):
|
|
||||||
self.base_model.confirm()
|
|
||||||
|
|
||||||
def interactive(self):
|
|
||||||
if not self.autoinstall_config:
|
|
||||||
return True
|
|
||||||
return bool(self.autoinstall_config.get('interactive-sections'))
|
|
||||||
|
|
||||||
def add_global_overlay(self, overlay):
|
|
||||||
self.global_overlays.append(overlay)
|
|
||||||
if isinstance(self.ui.body, BaseView):
|
|
||||||
self.ui.body.show_stretchy_overlay(overlay)
|
|
||||||
|
|
||||||
def remove_global_overlay(self, overlay):
|
|
||||||
self.global_overlays.remove(overlay)
|
|
||||||
if isinstance(self.ui.body, BaseView):
|
|
||||||
self.ui.body.remove_overlay(overlay)
|
|
||||||
|
|
||||||
def initial_controller_index(self):
|
|
||||||
if not self.updated:
|
|
||||||
return 0
|
|
||||||
state_path = self.state_path('last-screen')
|
|
||||||
if not os.path.exists(state_path):
|
|
||||||
return 0
|
|
||||||
with open(state_path) as fp:
|
|
||||||
last_screen = fp.read().strip()
|
|
||||||
controller_index = 0
|
|
||||||
for i, controller in enumerate(self.controllers.instances):
|
|
||||||
if controller.name == last_screen:
|
|
||||||
controller_index = i
|
|
||||||
return controller_index
|
|
||||||
|
|
||||||
def select_initial_screen(self):
|
|
||||||
self.error_reporter.load_reports()
|
|
||||||
for report in self.error_reporter.reports:
|
|
||||||
if report.kind == ErrorReportKind.UI and not report.seen:
|
|
||||||
self.show_error_report(report.ref())
|
|
||||||
break
|
|
||||||
index = self.initial_controller_index()
|
|
||||||
for controller in self.controllers.instances[:index]:
|
|
||||||
controller.configured()
|
|
||||||
self.controllers.index = index - 1
|
|
||||||
self.next_screen()
|
|
||||||
|
|
||||||
async def move_screen(self, increment, coro):
|
|
||||||
try:
|
|
||||||
await super().move_screen(increment, coro)
|
|
||||||
except Confirm:
|
|
||||||
if self.interactive():
|
|
||||||
log.debug("showing InstallConfirmation over %s", self.ui.body)
|
|
||||||
from subiquity.ui.views.installprogress import (
|
|
||||||
InstallConfirmation,
|
|
||||||
)
|
|
||||||
self.add_global_overlay(InstallConfirmation(self))
|
|
||||||
else:
|
|
||||||
yes = _('yes')
|
|
||||||
no = _('no')
|
|
||||||
answer = no
|
|
||||||
if 'autoinstall' in self.kernel_cmdline:
|
|
||||||
answer = yes
|
|
||||||
else:
|
|
||||||
print(_("Confirmation is required to continue."))
|
|
||||||
print(_("Add 'autoinstall' to your kernel command line to"
|
|
||||||
" avoid this"))
|
|
||||||
print()
|
|
||||||
prompt = "\n\n{} ({}|{})".format(
|
|
||||||
_("Continue with autoinstall?"), yes, no)
|
|
||||||
while answer != yes:
|
|
||||||
print(prompt)
|
|
||||||
answer = input()
|
|
||||||
self.next_screen(self.confirm_install())
|
|
||||||
|
|
||||||
async def make_view_for_controller(self, new):
|
|
||||||
if self.base_model.needs_confirmation(new.model_name):
|
|
||||||
raise Confirm
|
|
||||||
if new.interactive():
|
|
||||||
view = await super().make_view_for_controller(new)
|
|
||||||
if new.answers:
|
|
||||||
self.aio_loop.call_soon(new.run_answers)
|
|
||||||
with open(self.state_path('last-screen'), 'w') as fp:
|
|
||||||
fp.write(new.name)
|
|
||||||
return view
|
|
||||||
else:
|
|
||||||
if self.autoinstall_config and not new.autoinstall_applied:
|
|
||||||
await new.apply_autoinstall_config()
|
|
||||||
new.autoinstall_applied = True
|
|
||||||
new.configured()
|
|
||||||
raise Skip
|
|
||||||
|
|
||||||
def show_progress(self):
|
|
||||||
self.ui.set_body(self.controllers.InstallProgress.progress_view)
|
|
||||||
|
|
||||||
def _network_change(self):
|
|
||||||
self.signal.emit_signal('snapd-network-change')
|
|
||||||
|
|
||||||
async def _proxy_set(self):
|
|
||||||
await run_in_thread(
|
|
||||||
self.snapd.connection.configure_proxy, self.base_model.proxy)
|
|
||||||
self.signal.emit_signal('snapd-network-change')
|
|
||||||
|
|
||||||
def unhandled_input(self, key):
|
|
||||||
if key == 'f1':
|
|
||||||
if not self.ui.right_icon.current_help:
|
|
||||||
self.ui.right_icon.open_pop_up()
|
|
||||||
elif key in ['ctrl z', 'f2']:
|
|
||||||
self.debug_shell()
|
|
||||||
elif self.opts.dry_run:
|
|
||||||
self.unhandled_input_dry_run(key)
|
|
||||||
else:
|
|
||||||
super().unhandled_input(key)
|
|
||||||
|
|
||||||
def unhandled_input_dry_run(self, key):
|
|
||||||
if key == 'ctrl g':
|
|
||||||
from systemd import journal
|
|
||||||
|
|
||||||
async def mock_install():
|
|
||||||
async with self.install_lock_file.exclusive():
|
|
||||||
self.install_lock_file.write_content("nowhere")
|
|
||||||
journal.send(
|
|
||||||
"starting install", SYSLOG_IDENTIFIER="subiquity")
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
schedule_task(mock_install())
|
|
||||||
elif key in ['ctrl e', 'ctrl r']:
|
|
||||||
interrupt = key == 'ctrl e'
|
|
||||||
try:
|
|
||||||
1/0
|
|
||||||
except ZeroDivisionError:
|
|
||||||
self.make_apport_report(
|
|
||||||
ErrorReportKind.UNKNOWN, "example", interrupt=interrupt)
|
|
||||||
elif key == 'ctrl u':
|
|
||||||
1/0
|
|
||||||
elif key == 'ctrl b':
|
|
||||||
self.aio_loop.create_task(self.client.dry_run.crash.GET())
|
|
||||||
else:
|
|
||||||
super().unhandled_input(key)
|
|
||||||
|
|
||||||
def debug_shell(self, after_hook=None):
|
|
||||||
|
|
||||||
def _before():
|
|
||||||
os.system("clear")
|
|
||||||
print(DEBUG_SHELL_INTRO)
|
|
||||||
|
|
||||||
self.run_command_in_foreground(
|
|
||||||
["bash"], before_hook=_before, after_hook=after_hook, cwd='/')
|
|
||||||
|
|
||||||
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, *, interrupt, wait=False, **kw):
|
|
||||||
report = self.error_reporter.make_apport_report(
|
|
||||||
kind, thing, wait=wait, **kw)
|
|
||||||
|
|
||||||
if report is not None and interrupt and self.interactive():
|
|
||||||
self.show_error_report(report.ref())
|
|
||||||
|
|
||||||
return report
|
|
||||||
|
|
||||||
def show_error_report(self, error_ref):
|
|
||||||
log.debug("show_error_report %r", error_ref.base)
|
|
||||||
if isinstance(self.ui.body, BaseView):
|
|
||||||
w = getattr(self.ui.body._w, 'stretchy', None)
|
|
||||||
if isinstance(w, ErrorReportStretchy):
|
|
||||||
# Don't show an error if already looking at one.
|
|
||||||
return
|
|
||||||
self.add_global_overlay(ErrorReportStretchy(self, error_ref))
|
|
||||||
|
|
||||||
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
|
|
|
@ -136,28 +136,23 @@ class SubiquityModel:
|
||||||
if model_name in INSTALL_MODEL_NAMES:
|
if model_name in INSTALL_MODEL_NAMES:
|
||||||
unconfigured = {
|
unconfigured = {
|
||||||
mn for mn in INSTALL_MODEL_NAMES
|
mn for mn in INSTALL_MODEL_NAMES
|
||||||
if not self.is_configured(mn)
|
if not self._events[model_name].is_set()
|
||||||
}
|
}
|
||||||
elif model_name in POSTINSTALL_MODEL_NAMES:
|
elif model_name in POSTINSTALL_MODEL_NAMES:
|
||||||
unconfigured = {
|
unconfigured = {
|
||||||
mn for mn in POSTINSTALL_MODEL_NAMES
|
mn for mn in POSTINSTALL_MODEL_NAMES
|
||||||
if not self.is_configured(mn)
|
if not self._events[model_name].is_set()
|
||||||
}
|
}
|
||||||
log.debug("model %s is configured, to go %s", model_name, unconfigured)
|
log.debug("model %s is configured, to go %s", model_name, unconfigured)
|
||||||
|
|
||||||
def needs_confirmation(self, model_name):
|
def needs_configuration(self, model_name):
|
||||||
if model_name is None:
|
if model_name is None:
|
||||||
return False
|
return False
|
||||||
if not all(e.is_set() for e in self.install_events):
|
return not self._events[model_name].is_set()
|
||||||
return None
|
|
||||||
return not self.confirmation.is_set()
|
|
||||||
|
|
||||||
def confirm(self):
|
def confirm(self):
|
||||||
self.confirmation.set()
|
self.confirmation.set()
|
||||||
|
|
||||||
def is_configured(self, model_name):
|
|
||||||
return self._events[model_name].is_set()
|
|
||||||
|
|
||||||
def get_target_groups(self):
|
def get_target_groups(self):
|
||||||
command = ['chroot', self.target, 'getent', 'group']
|
command = ['chroot', self.target, 'getent', 'group']
|
||||||
if self.root != '/':
|
if self.root != '/':
|
||||||
|
|
|
@ -13,13 +13,25 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import shlex
|
||||||
import sys
|
import sys
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
|
import jsonschema
|
||||||
|
|
||||||
|
from systemd import journal
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from subiquitycore.async_helpers import run_in_thread, schedule_task
|
||||||
|
from subiquitycore.context import with_context
|
||||||
from subiquitycore.core import Application
|
from subiquitycore.core import Application
|
||||||
|
from subiquitycore.prober import Prober
|
||||||
|
|
||||||
from subiquity.common.api.server import (
|
from subiquity.common.api.server import (
|
||||||
bind,
|
bind,
|
||||||
|
@ -33,10 +45,18 @@ from subiquity.common.errorreport import (
|
||||||
from subiquity.common.serialize import to_json
|
from subiquity.common.serialize import to_json
|
||||||
from subiquity.common.types import (
|
from subiquity.common.types import (
|
||||||
ApplicationState,
|
ApplicationState,
|
||||||
|
ApplicationStatus,
|
||||||
ErrorReportRef,
|
ErrorReportRef,
|
||||||
|
InstallState,
|
||||||
)
|
)
|
||||||
from subiquity.server.controller import SubiquityController
|
from subiquity.server.controller import SubiquityController
|
||||||
|
from subiquity.models.subiquity import SubiquityModel
|
||||||
from subiquity.server.errors import ErrorController
|
from subiquity.server.errors import ErrorController
|
||||||
|
from subiquitycore.snapd import (
|
||||||
|
AsyncSnapd,
|
||||||
|
FakeSnapdConnection,
|
||||||
|
SnapdConnection,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger('subiquity.server.server')
|
log = logging.getLogger('subiquity.server.server')
|
||||||
|
@ -48,28 +68,146 @@ class MetaController:
|
||||||
self.app = app
|
self.app = app
|
||||||
self.context = app.context.child("Meta")
|
self.context = app.context.child("Meta")
|
||||||
|
|
||||||
async def status_GET(self) -> ApplicationState:
|
async def status_GET(self, cur: Optional[ApplicationState] = None) \
|
||||||
return self.app.status
|
-> ApplicationStatus:
|
||||||
|
if cur == self.app.state:
|
||||||
|
await self.app.state_event.wait()
|
||||||
|
return ApplicationStatus(
|
||||||
|
self.app.state,
|
||||||
|
early_commands_syslog_id=self.app.early_commands_syslog_id,
|
||||||
|
event_syslog_id=self.app.event_syslog_id,
|
||||||
|
log_syslog_id=self.app.log_syslog_id)
|
||||||
|
|
||||||
|
async def confirm_POST(self, tty: str) -> None:
|
||||||
|
self.app.confirming_tty = tty
|
||||||
|
self.app.base_model.confirm()
|
||||||
|
|
||||||
async def restart_POST(self) -> None:
|
async def restart_POST(self) -> None:
|
||||||
self.app.restart()
|
self.app.restart()
|
||||||
|
|
||||||
|
async def mark_configured_POST(self, endpoint_names: List[str]) -> None:
|
||||||
|
endpoints = {getattr(API, en, None) for en in endpoint_names}
|
||||||
|
for controller in self.app.controllers.instances:
|
||||||
|
if controller.endpoint in endpoints:
|
||||||
|
controller.configured()
|
||||||
|
|
||||||
|
|
||||||
class SubiquityServer(Application):
|
class SubiquityServer(Application):
|
||||||
|
|
||||||
|
snapd_socket_path = '/run/snapd.socket'
|
||||||
|
|
||||||
|
base_schema = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'version': {
|
||||||
|
'type': 'integer',
|
||||||
|
'minimum': 1,
|
||||||
|
'maximum': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['version'],
|
||||||
|
'additionalProperties': True,
|
||||||
|
}
|
||||||
|
|
||||||
project = "subiquity"
|
project = "subiquity"
|
||||||
from subiquity.server import controllers as controllers_mod
|
from subiquity.server import controllers as controllers_mod
|
||||||
controllers = []
|
controllers = []
|
||||||
|
|
||||||
def make_model(self):
|
def make_model(self):
|
||||||
return None
|
root = '/'
|
||||||
|
if self.opts.dry_run:
|
||||||
|
root = os.path.abspath('.subiquity')
|
||||||
|
return SubiquityModel(root, self.opts.sources)
|
||||||
|
|
||||||
def __init__(self, opts):
|
def __init__(self, opts, block_log_dir):
|
||||||
super().__init__(opts)
|
super().__init__(opts)
|
||||||
self.status = ApplicationState.STARTING
|
self.block_log_dir = block_log_dir
|
||||||
self.server_proc = None
|
self._state = ApplicationState.STARTING
|
||||||
|
self.state_event = asyncio.Event()
|
||||||
|
self.confirming_tty = ''
|
||||||
|
|
||||||
|
self.early_commands_syslog_id = 'subiquity_commands.{}'.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.error_reporter = ErrorReporter(
|
||||||
self.context.child("ErrorReporter"), self.opts.dry_run, self.root)
|
self.context.child("ErrorReporter"), self.opts.dry_run, self.root)
|
||||||
|
self.prober = Prober(opts.machine_config, self.debug_flags)
|
||||||
|
self.kernel_cmdline = shlex.split(opts.kernel_cmdline)
|
||||||
|
if opts.snaps_from_examples:
|
||||||
|
connection = FakeSnapdConnection(
|
||||||
|
os.path.join(
|
||||||
|
os.path.dirname(
|
||||||
|
os.path.dirname(
|
||||||
|
os.path.dirname(__file__))),
|
||||||
|
"examples", "snaps"),
|
||||||
|
self.scale_factor)
|
||||||
|
else:
|
||||||
|
connection = SnapdConnection(self.root, self.snapd_socket_path)
|
||||||
|
self.snapd = AsyncSnapd(connection)
|
||||||
|
self.note_data_for_apport("SnapUpdated", str(self.updated))
|
||||||
|
self.event_listeners = []
|
||||||
|
self.autoinstall_config = None
|
||||||
|
self.signal.connect_signals([
|
||||||
|
('network-proxy-set', lambda: schedule_task(self._proxy_set())),
|
||||||
|
('network-change', self._network_change),
|
||||||
|
])
|
||||||
|
|
||||||
|
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):
|
||||||
|
if not context.get('is-install-context') and self.interactive():
|
||||||
|
controller = context.get('controller')
|
||||||
|
if controller is None or controller.interactive():
|
||||||
|
return
|
||||||
|
if context.get('request'):
|
||||||
|
return
|
||||||
|
indent = context.full_name().count('/') - 2
|
||||||
|
if context.get('is-install-context') and self.interactive():
|
||||||
|
indent -= 1
|
||||||
|
msg = context.description
|
||||||
|
else:
|
||||||
|
msg = context.full_name()
|
||||||
|
if description:
|
||||||
|
msg += ': ' + description
|
||||||
|
msg = ' ' * indent + msg
|
||||||
|
if context.parent:
|
||||||
|
parent_id = str(context.parent.id)
|
||||||
|
else:
|
||||||
|
parent_id = ''
|
||||||
|
journal.send(
|
||||||
|
msg,
|
||||||
|
PRIORITY=context.level,
|
||||||
|
SYSLOG_IDENTIFIER=self.event_syslog_id,
|
||||||
|
SUBIQUITY_CONTEXT_NAME=context.full_name(),
|
||||||
|
SUBIQUITY_EVENT_TYPE=event_type,
|
||||||
|
SUBIQUITY_CONTEXT_ID=str(context.id),
|
||||||
|
SUBIQUITY_CONTEXT_PARENT_ID=parent_id)
|
||||||
|
|
||||||
|
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):
|
def note_file_for_apport(self, key, path):
|
||||||
self.error_reporter.note_file_for_apport(key, path)
|
self.error_reporter.note_file_for_apport(key, path)
|
||||||
|
@ -81,22 +219,30 @@ class SubiquityServer(Application):
|
||||||
return self.error_reporter.make_apport_report(
|
return self.error_reporter.make_apport_report(
|
||||||
kind, thing, wait=wait, **kw)
|
kind, thing, wait=wait, **kw)
|
||||||
|
|
||||||
|
def interactive(self):
|
||||||
|
if not self.autoinstall_config:
|
||||||
|
return True
|
||||||
|
return bool(self.autoinstall_config.get('interactive-sections'))
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def middleware(self, request, handler):
|
async def middleware(self, request, handler):
|
||||||
if self.updated:
|
override_status = None
|
||||||
updated = 'yes'
|
|
||||||
else:
|
|
||||||
updated = 'no'
|
|
||||||
controller = await controller_for_request(request)
|
controller = await controller_for_request(request)
|
||||||
if isinstance(controller, SubiquityController):
|
if isinstance(controller, SubiquityController):
|
||||||
|
install_state = self.controllers.Install.install_state
|
||||||
if not controller.interactive():
|
if not controller.interactive():
|
||||||
return web.Response(
|
override_status = 'skip'
|
||||||
headers={'x-status': 'skip', 'x-updated': updated})
|
elif install_state == InstallState.NEEDS_CONFIRMATION:
|
||||||
elif self.base_model.needs_confirmation(controller.model_name):
|
if self.base_model.needs_configuration(controller.model_name):
|
||||||
return web.Response(
|
override_status = 'confirm'
|
||||||
headers={'x-status': 'confirm', 'x-updated': updated})
|
if override_status is not None:
|
||||||
|
resp = web.Response(headers={'x-status': override_status})
|
||||||
|
else:
|
||||||
resp = await handler(request)
|
resp = await handler(request)
|
||||||
resp.headers['x-updated'] = updated
|
if self.updated:
|
||||||
|
resp.headers['x-updated'] = 'yes'
|
||||||
|
else:
|
||||||
|
resp.headers['x-updated'] = 'no'
|
||||||
if resp.get('exception'):
|
if resp.get('exception'):
|
||||||
exc = resp['exception']
|
exc = resp['exception']
|
||||||
log.debug(
|
log.debug(
|
||||||
|
@ -109,6 +255,34 @@ class SubiquityServer(Application):
|
||||||
ErrorReportRef, report.ref())
|
ErrorReportRef, report.ref())
|
||||||
return resp
|
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):
|
async def start_api_server(self):
|
||||||
app = web.Application(middlewares=[self.middleware])
|
app = web.Application(middlewares=[self.middleware])
|
||||||
bind(app.router, API.meta, MetaController(self))
|
bind(app.router, API.meta, MetaController(self))
|
||||||
|
@ -116,14 +290,42 @@ class SubiquityServer(Application):
|
||||||
if self.opts.dry_run:
|
if self.opts.dry_run:
|
||||||
from .dryrun import DryRunController
|
from .dryrun import DryRunController
|
||||||
bind(app.router, API.dry_run, DryRunController(self))
|
bind(app.router, API.dry_run, DryRunController(self))
|
||||||
|
for controller in self.controllers.instances:
|
||||||
|
controller.add_routes(app)
|
||||||
runner = web.AppRunner(app)
|
runner = web.AppRunner(app)
|
||||||
await runner.setup()
|
await runner.setup()
|
||||||
site = web.UnixSite(runner, self.opts.socket)
|
site = web.UnixSite(runner, self.opts.socket)
|
||||||
await site.start()
|
await site.start()
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
await super().start()
|
self.controllers.load_all()
|
||||||
await self.start_api_server()
|
await self.start_api_server()
|
||||||
|
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)
|
||||||
|
await self.controllers.Early.run()
|
||||||
|
open(stamp_file, 'w').close()
|
||||||
|
self.load_autoinstall_config(only_early=False)
|
||||||
|
if not self.interactive() and not self.opts.dry_run:
|
||||||
|
open('/run/casper-no-prompt', 'w').close()
|
||||||
|
self.load_serialized_state()
|
||||||
|
if self.interactive():
|
||||||
|
self.update_state(ApplicationState.INTERACTIVE)
|
||||||
|
else:
|
||||||
|
self.update_state(ApplicationState.NON_INTERACTIVE)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
await super().start()
|
||||||
|
await self.apply_autoinstall_config()
|
||||||
|
|
||||||
|
def _network_change(self):
|
||||||
|
self.signal.emit_signal('snapd-network-change')
|
||||||
|
|
||||||
|
async def _proxy_set(self):
|
||||||
|
await run_in_thread(
|
||||||
|
self.snapd.connection.configure_proxy, self.base_model.proxy)
|
||||||
|
self.signal.emit_signal('snapd-network-change')
|
||||||
|
|
||||||
def restart(self):
|
def restart(self):
|
||||||
cmdline = ['snap', 'run', 'subiquity']
|
cmdline = ['snap', 'run', 'subiquity']
|
||||||
|
@ -132,3 +334,11 @@ class SubiquityServer(Application):
|
||||||
sys.executable, '-m', 'subiquity.cmd.server',
|
sys.executable, '-m', 'subiquity.cmd.server',
|
||||||
] + sys.argv[1:]
|
] + sys.argv[1:]
|
||||||
os.execvp(cmdline[0], cmdline)
|
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
|
||||||
|
|
|
@ -129,12 +129,8 @@ class Application:
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.base_model = self.make_model()
|
self.base_model = self.make_model()
|
||||||
try:
|
|
||||||
self.aio_loop.create_task(self.start())
|
self.aio_loop.create_task(self.start())
|
||||||
self.aio_loop.run_forever()
|
self.aio_loop.run_forever()
|
||||||
finally:
|
|
||||||
self.aio_loop.run_until_complete(
|
|
||||||
self.aio_loop.shutdown_asyncgens())
|
|
||||||
if self._exc:
|
if self._exc:
|
||||||
exc, self._exc = self._exc, None
|
exc, self._exc = self._exc, None
|
||||||
raise exc
|
raise exc
|
||||||
|
|
Loading…
Reference in New Issue