diff --git a/po/POTFILES.in b/po/POTFILES.in index 7bbdfae2..1a15bd69 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -3,6 +3,7 @@ subiquity/client/client.py subiquity/client/controller.py subiquity/client/controllers/__init__.py subiquity/client/controllers/progress.py +subiquity/client/controllers/refresh.py subiquity/client/controllers/welcome.py subiquity/client/__init__.py subiquity/client/keycodes.py @@ -35,7 +36,6 @@ subiquity/controllers/mirror.py subiquity/controllers/network.py subiquity/controllers/proxy.py subiquity/controllers/reboot.py -subiquity/controllers/refresh.py subiquity/controllers/snaplist.py subiquity/controllers/ssh.py subiquity/controllers/tests/__init__.py @@ -122,6 +122,7 @@ subiquity/server/controllers/__init__.py subiquity/server/controllers/install.py subiquity/server/controllers/locale.py subiquity/server/controllers/package.py +subiquity/server/controllers/refresh.py subiquity/server/controllers/reporting.py subiquity/server/controllers/userdata.py subiquity/server/dryrun.py diff --git a/subiquity/client/client.py b/subiquity/client/client.py index 6f455572..8291c352 100644 --- a/subiquity/client/client.py +++ b/subiquity/client/client.py @@ -91,6 +91,7 @@ class SubiquityClient(TuiApplication): controllers = [ "Welcome", + "Refresh", "Progress", ] diff --git a/subiquity/client/controllers/__init__.py b/subiquity/client/controllers/__init__.py index 0ec38317..4aa0d493 100644 --- a/subiquity/client/controllers/__init__.py +++ b/subiquity/client/controllers/__init__.py @@ -14,9 +14,11 @@ # along with this program. If not, see . from .progress import ProgressController +from .refresh import RefreshController from .welcome import WelcomeController __all__ = [ 'ProgressController', + 'RefreshController', 'WelcomeController', ] diff --git a/subiquity/client/controllers/refresh.py b/subiquity/client/controllers/refresh.py new file mode 100644 index 00000000..05214a9e --- /dev/null +++ b/subiquity/client/controllers/refresh.py @@ -0,0 +1,90 @@ +# Copyright 2019 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.tuicontroller import ( + Skip, + ) + +from subiquity.common.types import ( + RefreshCheckState, + ) +from subiquity.client.controller import ( + SubiquityTuiController, + ) +from subiquity.ui.views.refresh import RefreshView + + +log = logging.getLogger('subiquity.client.controllers.refresh') + + +class RefreshController(SubiquityTuiController): + + endpoint_name = 'refresh' + + def __init__(self, app): + super().__init__(app) + self.offered_first_time = False + + async def get_progress(self, change): + while True: + try: + return await self.endpoint.progress.GET(change_id=change) + except aiohttp.ClientError: + # Probably the server is restarting. + await asyncio.sleep(1) + + async def make_ui(self, index=1): + if self.app.updated: + raise Skip() + show = False + self.status = await self.endpoint.GET() + if index == 1: + if self.status.availability == RefreshCheckState.AVAILABLE: + show = True + self.offered_first_time = True + elif index == 2: + if not self.offered_first_time: + if self.status.availability in [RefreshCheckState.UNKNOWN, + RefreshCheckState.AVAILABLE]: + show = True + else: + raise AssertionError("unexpected index {}".format(index)) + if show: + return RefreshView(self) + else: + raise Skip() + + async def wait_for_check(self): + self.status = await self.endpoint.GET(wait=True) + return self.status + + async def start_update(self): + return await self.endpoint.POST() + + def run_answers(self): + # Handled in the view + pass + + def done(self, sender=None): + log.debug("RefreshController.done next_screen") + self.app.next_screen() + + def cancel(self, sender=None): + self.app.prev_screen() diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py index 0724bc96..b871ca88 100644 --- a/subiquity/common/apidef.py +++ b/subiquity/common/apidef.py @@ -22,6 +22,7 @@ from subiquity.common.types import ( ErrorReportRef, InstallState, InstallStatus, + RefreshStatus, ) @@ -59,6 +60,18 @@ class API: def GET() -> None: """Requests to this method will fail with a HTTP 500.""" + class refresh: + def GET(wait: bool = False) -> RefreshStatus: + """Get information about the snap refresh status. + + If wait is true, block until the status is known.""" + + def POST() -> int: + """Start the update and return the change id.""" + + class progress: + def GET(change_id: int) -> dict: ... + class install: class status: def GET(cur: Optional[InstallState] = None) -> InstallStatus: ... diff --git a/subiquity/controllers/__init__.py b/subiquity/controllers/__init__.py index cb644f2e..0f295ff2 100644 --- a/subiquity/controllers/__init__.py +++ b/subiquity/controllers/__init__.py @@ -21,7 +21,6 @@ from .mirror import MirrorController from .network import NetworkController from .proxy import ProxyController from .reboot import RebootController -from .refresh import RefreshController from .snaplist import SnapListController from .ssh import SSHController from .zdev import ZdevController @@ -34,7 +33,6 @@ __all__ = [ 'NetworkController', 'ProxyController', 'RebootController', - 'RefreshController', 'RepeatedController', 'SnapListController', 'SSHController', diff --git a/subiquity/server/controllers/__init__.py b/subiquity/server/controllers/__init__.py index d80883f4..6da15bc7 100644 --- a/subiquity/server/controllers/__init__.py +++ b/subiquity/server/controllers/__init__.py @@ -18,6 +18,7 @@ from .debconf import DebconfController from .install import InstallController from .locale import LocaleController from .package import PackageController +from .refresh import RefreshController from .reporting import ReportingController from .userdata import UserdataController @@ -29,6 +30,7 @@ __all__ = [ 'LateController', 'LocaleController', 'PackageController', + 'RefreshController', 'ReportingController', 'UserdataController', ] diff --git a/subiquity/controllers/refresh.py b/subiquity/server/controllers/refresh.py similarity index 76% rename from subiquity/controllers/refresh.py rename to subiquity/server/controllers/refresh.py index 2f379a3a..faabed92 100644 --- a/subiquity/controllers/refresh.py +++ b/subiquity/server/controllers/refresh.py @@ -24,24 +24,23 @@ from subiquitycore.async_helpers import ( SingleInstanceTask, ) from subiquitycore.context import with_context -from subiquitycore.tuicontroller import ( - Skip, - ) +from subiquity.common.apidef import API from subiquity.common.types import ( RefreshCheckState, RefreshStatus, ) -from subiquity.controller import ( - SubiquityTuiController, +from subiquity.server.controller import ( + SubiquityController, ) -from subiquity.ui.views.refresh import RefreshView -log = logging.getLogger('subiquity.controllers.refresh') +log = logging.getLogger('subiquity.server.controllers.refresh') -class RefreshController(SubiquityTuiController): +class RefreshController(SubiquityController): + + endpoint = API.refresh autoinstall_key = "refresh-installer" autoinstall_schema = { @@ -58,23 +57,24 @@ class RefreshController(SubiquityTuiController): ] def __init__(self, app): - self.ai_data = {} super().__init__(app) + self.ai_data = {} self.snap_name = os.environ.get("SNAP_NAME", "subiquity") self.configure_task = None self.check_task = None self.status = RefreshStatus(availability=RefreshCheckState.UNKNOWN) - self.offered_first_time = False - if 'update' in self.ai_data: - self.active = self.ai_data['update'] - else: - self.active = self.interactive() - def load_autoinstall_data(self, data): if data is not None: self.ai_data = data + @property + def active(self): + if 'update' in self.ai_data: + return True + else: + return self.interactive() + def start(self): if not self.active: return @@ -95,16 +95,9 @@ class RefreshController(SubiquityTuiController): return change_id = await self.start_update(context=context) while True: - try: - change = await self.get_progress(change_id) - except requests.exceptions.RequestException as e: - raise e - if change['status'] == 'Done': - # Clearly if we got here we didn't get restarted by - # snapd/systemctl (dry-run mode or logged in via SSH) - self.app.restart(remove_last_screen=False) - if change['status'] not in ['Do', 'Doing']: - raise Exception("update failed") + change = await self.get_progress(change_id) + if change['status'] not in ['Do', 'Doing', 'Done']: + raise Exception("update failed: %s", change['status']) await asyncio.sleep(0.1) @with_context() @@ -137,8 +130,6 @@ class RefreshController(SubiquityTuiController): def get_refresh_channel(self): """Return the channel we should refresh subiquity to.""" - if 'channel' in self.answers: - return self.answers['channel'] prefix = "subiquity-channel=" for arg in self.app.kernel_cmdline: if arg.startswith(prefix): @@ -209,39 +200,20 @@ class RefreshController(SubiquityTuiController): async def get_progress(self, change): result = await self.app.snapd.get('v2/changes/{}'.format(change)) - return result['result'] + change = result['result'] + if change['status'] == 'Done': + # Clearly if we got here we didn't get restarted by + # snapd/systemctl (dry-run mode) + self.app.restart() + return change - def make_ui(self, index=1): - if self.app.updated: - raise Skip() - show = False - if index == 1: - if self.status.availability == RefreshCheckState.AVAILABLE: - show = True - self.offered_first_time = True - elif index == 2: - if not self.offered_first_time: - if self.status.availability in [RefreshCheckState.UNKNOWN, - RefreshCheckState.AVAILABLE]: - show = True - else: - raise AssertionError("unexpected index {}".format(index)) - if show: - return RefreshView(self) - else: - raise Skip() - - async def wait_for_check(self): - await self.check_task.task + async def GET(self, wait: bool = False) -> RefreshStatus: + if wait: + await self.check_task.wait() return self.status - def run_answers(self): - # Handled in the view - pass + async def POST(self, context) -> int: + return await self.start_update(context=context) - def done(self, sender=None): - log.debug("RefreshController.done next_screen") - self.app.next_screen() - - def cancel(self, sender=None): - self.app.prev_screen() + async def progress_GET(self, change_id: int) -> dict: + return await self.get_progress(change_id) diff --git a/subiquity/server/server.py b/subiquity/server/server.py index da839479..196b7284 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -119,6 +119,7 @@ class SubiquityServer(Application): "Package", "Debconf", "Locale", + "Refresh", "Install", "Late", ] diff --git a/subiquity/ui/views/refresh.py b/subiquity/ui/views/refresh.py index 06ac64d0..5b679001 100644 --- a/subiquity/ui/views/refresh.py +++ b/subiquity/ui/views/refresh.py @@ -17,7 +17,7 @@ import asyncio import json import logging -import requests +import aiohttp from urwid import ( ProgressBar, @@ -246,15 +246,11 @@ class RefreshView(BaseView): async def _update(self): try: change_id = await self.controller.start_update() - except requests.exceptions.RequestException as e: + except aiohttp.ClientError as e: self.update_failed(exc_message(e)) return while True: - try: - change = await self.controller.get_progress(change_id) - except requests.exceptions.RequestException as e: - self.update_failed(exc_message(e)) - return + change = await self.controller.get_progress(change_id) if change['status'] == 'Done': # Clearly if we got here we didn't get restarted by # snapd/systemctl (dry-run mode or logged in via SSH)