split installprogress controller into install (server) and progress (client)

This commit is contained in:
Michael Hudson-Doyle 2020-10-09 15:40:57 +13:00
parent 92252f8119
commit 3c30e14313
11 changed files with 233 additions and 119 deletions

View File

@ -1,6 +1,8 @@
[encoding: UTF-8]
subiquity/client/client.py
subiquity/client/controller.py
subiquity/client/controllers/__init__.py
subiquity/client/controllers/progress.py
subiquity/client/__init__.py
subiquity/client/keycodes.py
subiquity/cmd/common.py
@ -30,7 +32,6 @@ subiquity/controllers/error.py
subiquity/controllers/filesystem.py
subiquity/controllers/identity.py
subiquity/controllers/__init__.py
subiquity/controllers/installprogress.py
subiquity/controllers/keyboard.py
subiquity/controllers/mirror.py
subiquity/controllers/network.py
@ -122,6 +123,7 @@ subiquity/models/tests/test_mirror.py
subiquity/models/tests/test_subiquity.py
subiquity/server/controller.py
subiquity/server/controllers/__init__.py
subiquity/server/controllers/install.py
subiquity/server/dryrun.py
subiquity/server/errors.py
subiquity/server/__init__.py

View File

@ -89,7 +89,9 @@ class SubiquityClient(TuiApplication):
def make_ui(self):
return SubiquityUI(self, self.help_menu)
controllers = []
controllers = [
"Progress",
]
def __init__(self, opts):
if is_linux_tty():

View File

@ -0,0 +1,20 @@
# 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/>.
from .progress import ProgressController
__all__ = [
'ProgressController',
]

View File

@ -0,0 +1,121 @@
# 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 aiohttp
from subiquitycore.context import with_context
from subiquity.client.controller import SubiquityTuiController
from subiquity.common.types import InstallState
from subiquity.ui.views.installprogress import (
InstallRunning,
ProgressView,
)
log = logging.getLogger("subiquity.client.controllers.progress")
class ProgressController(SubiquityTuiController):
endpoint_name = 'install'
def __init__(self, app):
super().__init__(app)
self.progress_view = ProgressView(self)
self.install_state = None
self.crash_report_ref = None
self.answers = app.answers.get("InstallProgress", {})
def event(self, event):
if event["SUBIQUITY_EVENT_TYPE"] == "start":
self.progress_view.event_start(
event["SUBIQUITY_CONTEXT_ID"],
event.get("SUBIQUITY_CONTEXT_PARENT_ID"),
event["MESSAGE"])
elif event["SUBIQUITY_EVENT_TYPE"] == "finish":
self.progress_view.event_finish(
event["SUBIQUITY_CONTEXT_ID"])
def log_line(self, event):
log_line = event['MESSAGE']
self.progress_view.add_log_line(log_line)
def cancel(self):
pass
def start(self):
self.app.aio_loop.create_task(self._wait_status())
def click_reboot(self):
self.app.aio_loop.create_task(self.send_reboot_and_wait())
async def send_reboot_and_wait(self):
try:
await self.app.client.reboot.POST()
except aiohttp.ClientError:
pass
self.app.exit()
@with_context()
async def _wait_status(self, context):
install_running = None
while True:
try:
install_status = await self.endpoint.status.GET(
cur=self.install_state)
except aiohttp.ClientError:
await asyncio.sleep(1)
continue
self.install_state = install_status.state
self.progress_view.update_for_state(self.install_state)
if self.ui.body is self.progress_view:
self.ui.set_header(self.progress_view.title)
if install_status.error is not None:
if self.crash_report_ref is None:
self.crash_report_ref = install_status.error
self.ui.set_body(self.progress_view)
self.app.show_error_report(self.crash_report_ref)
if self.install_state == InstallState.NEEDS_CONFIRMATION:
if self.showing:
self.app.show_confirm_install()
if self.install_state == InstallState.RUNNING:
if install_status.confirming_tty != self.app.our_tty:
install_running = InstallRunning(
self.app, install_status.confirming_tty)
self.app.add_global_overlay(install_running)
else:
if install_running is not None:
self.app.remove_global_overlay(install_running)
install_running = None
if self.install_state == InstallState.DONE:
if self.answers.get('reboot', False):
self.click_reboot()
def make_ui(self):
if self.install_state == InstallState.NEEDS_CONFIRMATION:
self.app.show_confirm_install()
return self.progress_view
def run_answers(self):
pass

View File

@ -20,6 +20,8 @@ from subiquity.common.types import (
ApplicationState,
ApplicationStatus,
ErrorReportRef,
InstallState,
InstallStatus,
)
@ -55,3 +57,7 @@ class API:
class crash:
def GET() -> None:
"""Requests to this method will fail with a HTTP 500."""
class install:
class status:
def GET(cur: Optional[InstallState] = None) -> InstallStatus: ...

View File

@ -183,3 +183,10 @@ class InstallState(enum.Enum):
UU_CANCELLING = enum.auto()
DONE = enum.auto()
ERROR = enum.auto()
@attr.s(auto_attribs=True)
class InstallStatus:
state: InstallState
confirming_tty: str = ''
error: Optional[ErrorReportRef] = None

View File

@ -19,7 +19,6 @@ from .debconf import DebconfController
from .error import ErrorController
from .filesystem import FilesystemController
from .identity import IdentityController
from .installprogress import InstallProgressController
from .keyboard import KeyboardController
from .mirror import MirrorController
from .network import NetworkController
@ -40,7 +39,6 @@ __all__ = [
'ErrorController',
'FilesystemController',
'IdentityController',
'InstallProgressController',
'KeyboardController',
'LateController',
'MirrorController',

View File

@ -12,3 +12,9 @@
#
# 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/>.
from .install import InstallController
__all__ = [
'InstallController',
]

View File

@ -1,4 +1,4 @@
# Copyright 2015 Canonical, Ltd.
# 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
@ -21,7 +21,7 @@ import re
import shutil
import sys
import tempfile
import traceback
from typing import Optional
from curtin.commands.install import (
ERROR_TARFILE,
@ -29,13 +29,10 @@ from curtin.commands.install import (
)
from curtin.util import write_file
from systemd import journal
import yaml
from subiquitycore.async_helpers import (
run_in_thread,
schedule_task,
)
from subiquitycore.context import Status, with_context
from subiquitycore.utils import (
@ -43,14 +40,18 @@ from subiquitycore.utils import (
astart_command,
)
from subiquity.common.apidef import API
from subiquity.common.errorreport import ErrorReportKind
from subiquity.common.types import InstallState
from subiquity.controller import SubiquityTuiController
from subiquity.server.controller import (
SubiquityController,
)
from subiquity.common.types import (
InstallState,
InstallStatus,
)
from subiquity.journald import journald_listen
from subiquity.ui.views.installprogress import ProgressView
log = logging.getLogger("subiquitycore.controller.installprogress")
log = logging.getLogger("subiquity.server.controllers.install")
class TracebackExtractor:
@ -72,18 +73,16 @@ class TracebackExtractor:
self.traceback.append(line)
class InstallProgressController(SubiquityTuiController):
class InstallController(SubiquityController):
endpoint = API.install
def __init__(self, app):
super().__init__(app)
self.model = app.base_model
self.progress_view = ProgressView(self)
self.crash_report_ref = None
self._install_state = InstallState.NOT_STARTED
self.reboot_clicked = asyncio.Event()
if self.answers.get('reboot', False):
self.reboot_clicked.set()
self._install_state_event = asyncio.Event()
self.error_ref = None
self.unattended_upgrades_proc = None
self.unattended_upgrades_ctx = None
@ -91,71 +90,53 @@ class InstallProgressController(SubiquityTuiController):
self.tb_extractor = TracebackExtractor()
self.curtin_event_contexts = {}
def event(self, event):
if event["SUBIQUITY_EVENT_TYPE"] == "start":
self.progress_view.event_start(
event["SUBIQUITY_CONTEXT_ID"],
event.get("SUBIQUITY_CONTEXT_PARENT_ID"),
event["MESSAGE"])
elif event["SUBIQUITY_EVENT_TYPE"] == "finish":
self.progress_view.event_finish(
event["SUBIQUITY_CONTEXT_ID"])
def log_line(self, event):
log_line = event['MESSAGE']
self.progress_view.add_log_line(log_line)
def interactive(self):
return self.app.interactive()
return True
async def status_GET(
self, cur: Optional[InstallState] = None) -> InstallStatus:
if cur == self.install_state:
await self._install_state_event.wait()
return InstallStatus(
self.install_state,
self.app.confirming_tty,
self.error_ref)
def stop_uu(self):
if self.install_state == InstallState.UU_RUNNING:
self.update_state(InstallState.UU_CANCELLING)
self.app.aio_loop.create_task(self.stop_unattended_upgrades())
def start(self):
self.install_task = schedule_task(self.install())
@with_context()
async def apply_autoinstall_config(self, context):
await self.install_task
self.app.reboot_on_exit = True
self.install_task = self.app.aio_loop.create_task(self.install())
@property
def install_state(self):
return self._install_state
def update_state(self, state):
self._install_state_event.set()
self._install_state_event.clear()
self._install_state = state
self.progress_view.update_for_state(state)
def tpath(self, *path):
return os.path.join(self.model.target, *path)
def curtin_error(self):
self.update_state(InstallState.ERROR)
kw = {}
if sys.exc_info()[0] is not None:
log.exception("curtin_error")
self.progress_view.add_log_line(traceback.format_exc())
# send traceback.format_exc() to journal?
if self.tb_extractor.traceback:
kw["Traceback"] = "\n".join(self.tb_extractor.traceback)
crash_report = self.app.make_apport_report(
ErrorReportKind.INSTALL_FAIL, "install failed", interrupt=False,
**kw)
if crash_report is not None:
self.crash_report_ref = crash_report.ref()
self.progress_view.finish_all()
self.progress_view.set_status(
('info_error', _("An error has occurred")))
if not self.showing:
self.app.controllers.index = self.controller_index - 1
self.app.next_screen()
self.update_state(InstallState.ERROR)
if self.crash_report_ref is not None:
self.app.show_error_report(self.crash_report_ref)
self.error_ref = self.app.make_apport_report(
ErrorReportKind.INSTALL_FAIL, "install failed", **kw).ref()
def logged_command(self, cmd):
return ['systemd-cat', '--level-prefix=false',
'--identifier=' + self.app.log_syslog_id] + cmd
def log_event(self, event):
self.curtin_log(event)
def curtin_event(self, event):
e = {
"EVENT_TYPE": "???",
@ -189,7 +170,7 @@ class InstallProgressController(SubiquityTuiController):
if curtin_ctx is not None:
curtin_ctx.exit(result=status)
def curtin_log(self, event):
def log_event(self, event):
self.tb_extractor.feed(event['MESSAGE'])
def _write_config(self, path, config):
@ -202,7 +183,7 @@ class InstallProgressController(SubiquityTuiController):
def _get_curtin_command(self):
config_file_name = 'subiquity-curtin-install.conf'
if self.opts.dry_run:
if self.app.opts.dry_run:
config_location = os.path.join('.subiquity/', config_file_name)
log_location = '.subiquity/install.log'
event_file = "examples/curtin-events.json"
@ -219,9 +200,9 @@ class InstallProgressController(SubiquityTuiController):
config_location, 'install']
log_location = INSTALL_LOG
self._write_config(
config_location,
self.model.render(syslog_identifier=self._event_syslog_id))
ident = self._event_syslog_id
self._write_config(config_location,
self.model.render(syslog_identifier=ident))
self.app.note_file_for_apport("CurtinConfig", config_location)
self.app.note_file_for_apport("CurtinLog", log_location)
@ -235,10 +216,10 @@ class InstallProgressController(SubiquityTuiController):
sys.executable, '-m', 'curtin', 'unmount',
'-t', target,
]
if self.opts.dry_run:
if self.app.opts.dry_run:
cmd = ['sleep', str(0.2/self.app.scale_factor)]
await arun_command(cmd)
if not self.opts.dry_run:
if not self.app.opts.dry_run:
shutil.rmtree(target)
@with_context(
@ -250,7 +231,7 @@ class InstallProgressController(SubiquityTuiController):
loop = self.app.aio_loop
fds = [
journald_listen(loop, [self.app.log_syslog_id], self.curtin_log),
journald_listen(loop, [self.app.log_syslog_id], self.log_event),
journald_listen(loop, [self._event_syslog_id], self.curtin_event),
]
@ -258,32 +239,24 @@ class InstallProgressController(SubiquityTuiController):
log.debug('curtin install cmd: {}'.format(curtin_cmd))
async with self.app.install_lock_file.exclusive():
try:
our_tty = os.ttyname(0)
except OSError:
# This is a gross hack for testing in travis.
our_tty = "/dev/not a tty"
self.app.install_lock_file.write_content(our_tty)
journal.send("starting install", SYSLOG_IDENTIFIER="subiquity")
try:
cp = await arun_command(
self.logged_command(curtin_cmd), check=True)
finally:
for fd in fds:
loop.remove_reader(fd)
try:
cp = await arun_command(
self.logged_command(curtin_cmd), check=True)
finally:
for fd in fds:
loop.remove_reader(fd)
log.debug('curtin_install completed: %s', cp.returncode)
def cancel(self):
pass
@with_context()
async def install(self, *, context):
context.set('is-install-context', True)
try:
await asyncio.wait(
{e.wait() for e in self.model.install_events})
await asyncio.wait({e.wait() for e in self.model.install_events})
if not self.app.interactive():
if 'autoinstall' in self.app.kernel_cmdline:
self.model.confirm()
self.update_state(InstallState.NEEDS_CONFIRMATION)
@ -315,16 +288,10 @@ class InstallProgressController(SubiquityTuiController):
self.update_state(InstallState.DONE)
except Exception:
self.curtin_error()
if not self.interactive():
raise
async def move_on(self):
await self.install_task
self.app.next_screen()
async def drain_curtin_events(self, *, context):
waited = 0.0
while self.progress_view.ongoing and waited < 5.0:
while len(self.curtin_event_contexts) > 1 and waited < 5.0:
await asyncio.sleep(0.1)
waited += 0.1
log.debug("waited %s seconds for events to drain", waited)
@ -356,7 +323,7 @@ class InstallProgressController(SubiquityTuiController):
name="install_{package}",
description="installing {package}")
async def install_package(self, *, context, package):
if self.opts.dry_run:
if self.app.opts.dry_run:
cmd = ["sleep", str(2/self.app.scale_factor)]
else:
cmd = [
@ -368,7 +335,7 @@ class InstallProgressController(SubiquityTuiController):
@with_context(description="restoring apt configuration")
async def restore_apt_config(self, context):
if self.opts.dry_run:
if self.app.opts.dry_run:
cmds = [["sleep", str(1/self.app.scale_factor)]]
else:
cmds = [
@ -395,7 +362,7 @@ class InstallProgressController(SubiquityTuiController):
env = os.environ.copy()
env["APT_CONFIG"] = apt_conf.name[len(self.model.target):]
self.unattended_upgrades_ctx = context
if self.opts.dry_run:
if self.app.opts.dry_run:
self.unattended_upgrades_proc = await astart_command(
self.logged_command(
["sleep", str(5/self.app.scale_factor)]), env=env)
@ -411,11 +378,10 @@ class InstallProgressController(SubiquityTuiController):
os.remove(apt_conf.name)
async def stop_unattended_upgrades(self):
self.progress_view.event_finish(self.unattended_upgrades_ctx)
with self.unattended_upgrades_ctx.parent.child(
"stop_unattended_upgrades",
"cancelling update"):
if self.opts.dry_run:
if self.app.opts.dry_run:
await asyncio.sleep(1)
self.unattended_upgrades_proc.terminate()
else:
@ -426,22 +392,6 @@ class InstallProgressController(SubiquityTuiController):
'--stop-only',
]), check=True)
async def _click_reboot(self):
if self.unattended_upgrades_ctx is not None:
self.update_state(InstallState.UU_CANCELLING)
await self.stop_unattended_upgrades()
self.reboot_clicked.set()
def click_reboot(self):
schedule_task(self._click_reboot())
def make_ui(self):
schedule_task(self.move_on())
return self.progress_view
def run_answers(self):
pass
uu_apt_conf = """\
# Config for the unattended-upgrades run to avoid failing on battery power or

View File

@ -111,7 +111,9 @@ class SubiquityServer(Application):
project = "subiquity"
from subiquity.server import controllers as controllers_mod
controllers = []
controllers = [
"Install",
]
def make_model(self):
root = '/'

View File

@ -3,15 +3,15 @@ from unittest import mock
from subiquitycore.testing import view_helpers
from subiquity.client.controllers.progress import ProgressController
from subiquity.common.types import InstallState
from subiquity.controllers.installprogress import InstallProgressController
from subiquity.ui.views.installprogress import ProgressView
class IdentityViewTests(unittest.TestCase):
def make_view(self):
controller = mock.create_autospec(spec=InstallProgressController)
controller = mock.create_autospec(spec=ProgressController)
controller.app = mock.Mock()
controller.app.aio_loop = None
return ProgressView(controller)