diff --git a/po/POTFILES.in b/po/POTFILES.in
index 1fe47dc5..c149c731 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -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
diff --git a/subiquity/client/client.py b/subiquity/client/client.py
index 9f658012..652f4b4d 100644
--- a/subiquity/client/client.py
+++ b/subiquity/client/client.py
@@ -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():
diff --git a/subiquity/client/controllers/__init__.py b/subiquity/client/controllers/__init__.py
new file mode 100644
index 00000000..24de8e16
--- /dev/null
+++ b/subiquity/client/controllers/__init__.py
@@ -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 .
+
+from .progress import ProgressController
+
+__all__ = [
+ 'ProgressController',
+]
diff --git a/subiquity/client/controllers/progress.py b/subiquity/client/controllers/progress.py
new file mode 100644
index 00000000..e02bdf12
--- /dev/null
+++ b/subiquity/client/controllers/progress.py
@@ -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 .
+
+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
diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py
index ed97fdb8..299ae9e4 100644
--- a/subiquity/common/apidef.py
+++ b/subiquity/common/apidef.py
@@ -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: ...
diff --git a/subiquity/common/types.py b/subiquity/common/types.py
index 7382e252..fb82e8b9 100644
--- a/subiquity/common/types.py
+++ b/subiquity/common/types.py
@@ -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
diff --git a/subiquity/controllers/__init__.py b/subiquity/controllers/__init__.py
index 36e903da..25a94520 100644
--- a/subiquity/controllers/__init__.py
+++ b/subiquity/controllers/__init__.py
@@ -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',
diff --git a/subiquity/server/controllers/__init__.py b/subiquity/server/controllers/__init__.py
index 8e549e25..ee88e0f1 100644
--- a/subiquity/server/controllers/__init__.py
+++ b/subiquity/server/controllers/__init__.py
@@ -12,3 +12,9 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+
+from .install import InstallController
+
+__all__ = [
+ 'InstallController',
+]
diff --git a/subiquity/controllers/installprogress.py b/subiquity/server/controllers/install.py
similarity index 75%
rename from subiquity/controllers/installprogress.py
rename to subiquity/server/controllers/install.py
index 57244408..66e59130 100644
--- a/subiquity/controllers/installprogress.py
+++ b/subiquity/server/controllers/install.py
@@ -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
diff --git a/subiquity/server/server.py b/subiquity/server/server.py
index 311dd3f8..404a8a06 100644
--- a/subiquity/server/server.py
+++ b/subiquity/server/server.py
@@ -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 = '/'
diff --git a/subiquity/ui/views/tests/test_installprogress.py b/subiquity/ui/views/tests/test_installprogress.py
index ac344cf3..411b9d7a 100644
--- a/subiquity/ui/views/tests/test_installprogress.py
+++ b/subiquity/ui/views/tests/test_installprogress.py
@@ -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)