Skip to content

Commit

Permalink
Merge pull request #1925 from Chris-Peterson444/non-reportable-errors
Browse files Browse the repository at this point in the history
Support for Non Reportable Errors
  • Loading branch information
Chris-Peterson444 authored Mar 7, 2024
2 parents 01e4f63 + bae102e commit 8721395
Show file tree
Hide file tree
Showing 15 changed files with 493 additions and 61 deletions.
64 changes: 38 additions & 26 deletions subiquity/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,16 @@
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
from subiquity.common.types import (
ApplicationState,
ErrorReportKind,
ErrorReportRef,
NonReportableError,
)
from subiquity.journald import journald_listen
from subiquity.server.server import POSTINSTALL_MODEL_NAMES
from subiquity.ui.frame import SubiquityUI
from subiquity.ui.views.error import ErrorReportStretchy
from subiquity.ui.views.error import ErrorReportStretchy, NonReportableErrorStretchy
from subiquity.ui.views.help import HelpMenu, ssh_help_texts
from subiquity.ui.views.installprogress import InstallConfirmation
from subiquity.ui.views.welcome import CloudInitFail
Expand Down Expand Up @@ -317,30 +322,33 @@ def header_func():
status = await self.connect()
self.interactive = status.interactive
if self.interactive:
if self.opts.ssh:
ssh_info = await self.client.meta.ssh_info.GET()
texts = ssh_help_texts(ssh_info)
for line in texts:
import urwid

if isinstance(line, urwid.Widget):
line = "\n".join(
[
line.decode("utf-8").rstrip()
for line in line.render((1000,)).text
]
)
print(line)
return

# Get the variant from the server and reload desired
# controllers if an override exists
variant = await self.client.meta.client_variant.GET()
if variant != self.variant:
self.variant = variant
controllers = self.variant_to_controllers.get(variant)
if controllers:
self.load_controllers(controllers)
# The server could end up in an error state before we get here
# so skip to allow urwid to come up and show an error screen
if status.state != ApplicationState.ERROR:
if self.opts.ssh:
ssh_info = await self.client.meta.ssh_info.GET()
texts = ssh_help_texts(ssh_info)
for line in texts:
import urwid

if isinstance(line, urwid.Widget):
line = "\n".join(
[
line.decode("utf-8").rstrip()
for line in line.render((1000,)).text
]
)
print(line)
return

# Get the variant from the server and reload desired
# controllers if an override exists
variant = await self.client.meta.client_variant.GET()
if variant != self.variant:
self.variant = variant
controllers = self.variant_to_controllers.get(variant)
if controllers:
self.load_controllers(controllers)

await super().start()
# Progress uses systemd to collect and display the installation
Expand Down Expand Up @@ -609,3 +617,7 @@ def show_error_report(self, error_ref):
# Don't show an error if already looking at one.
return
self.add_global_overlay(ErrorReportStretchy(self, error_ref))

def show_nonreportable_error(self, error: NonReportableError):
log.debug("show_non_reportable_error %r", error.cause)
self.add_global_overlay(NonReportableErrorStretchy(self, error))
28 changes: 22 additions & 6 deletions subiquity/client/controllers/progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@

import asyncio
import logging
from typing import Optional

import aiohttp

from subiquity.client.controller import SubiquityTuiController
from subiquity.common.types import ApplicationState, ShutdownMode
from subiquity.common.types import ApplicationState, ApplicationStatus, ShutdownMode
from subiquity.ui.views.installprogress import InstallRunning, ProgressView
from subiquitycore.async_helpers import run_bg_task
from subiquitycore.context import with_context
Expand All @@ -32,6 +33,7 @@ def __init__(self, app):
super().__init__(app)
self.progress_view = ProgressView(self)
self.app_state = None
self.has_nonreportable_error: Optional[bool] = None
self.crash_report_ref = None
self.answers = app.answers.get("InstallProgress", {})

Expand Down Expand Up @@ -75,16 +77,14 @@ async def _wait_status(self, context):
await asyncio.sleep(1)
continue
self.app_state = app_status.state
self.has_nonreportable_error = app_status.nonreportable_error is not None

self.progress_view.update_for_state(self.app_state)
if self.ui.body is self.progress_view:
self.ui.set_header(self.progress_view.title)

if app_status.error is not None:
if self.crash_report_ref is None:
self.crash_report_ref = app_status.error
self.ui.set_body(self.progress_view)
self.app.show_error_report(self.crash_report_ref)
if self.app_state == ApplicationState.ERROR:
self._handle_error_state(app_status)

if self.app_state == ApplicationState.NEEDS_CONFIRMATION:
if self.showing:
Expand All @@ -105,6 +105,22 @@ async def _wait_status(self, context):
if self.answers.get("reboot", False):
self.click_reboot()

def _handle_error_state(self, app_status: ApplicationStatus):
if (error := app_status.error) is not None:
if self.crash_report_ref is None:
self.crash_report_ref = error
self.ui.set_body(self.progress_view)
self.app.show_error_report(error)

elif (error := app_status.nonreportable_error) is not None:
self.ui.set_body(self.progress_view)
self.app.show_nonreportable_error(error)

else:
# There is the case that both are None but still in an error state
# but this is likely a bug so raise an Exception
raise Exception("Server in ERROR state but no error received")

def make_ui(self):
if self.app_state == ApplicationState.NEEDS_CONFIRMATION:
self.app.show_confirm_install()
Expand Down
95 changes: 95 additions & 0 deletions subiquity/client/controllers/tests/test_progress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Copyright 2024 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 unittest
from unittest.mock import AsyncMock, Mock

from subiquity.client.controllers.progress import ProgressController
from subiquity.common.types import ApplicationState, ApplicationStatus
from subiquitycore.tests.mocks import make_app


class TestProgressController(unittest.IsolatedAsyncioTestCase):
def setUp(self):
app = make_app()
app.client = AsyncMock()
app.show_error_report = Mock()
app.show_nonreportable_error = Mock()

self.controller = ProgressController(app)

async def test_handle_error_state(self):
# Reportable error case

status: ApplicationStatus = ApplicationStatus(
state=ApplicationState.ERROR,
confirming_tty="",
error=Mock(),
nonreportable_error=None,
cloud_init_ok=Mock(),
interactive=Mock(),
echo_syslog_id=Mock(),
log_syslog_id=Mock(),
event_syslog_id=Mock(),
)

self.controller._handle_error_state(status)
self.controller.app.show_error_report.assert_called_once()
self.controller.app.show_nonreportable_error.assert_not_called()

# Reset mocks between cases
self.controller.app.show_error_report.reset_mock()
self.controller.app.show_nonreportable_error.reset_mock()

# Non Reportable error case

status: ApplicationStatus = ApplicationStatus(
state=ApplicationState.ERROR,
confirming_tty="",
error=None,
nonreportable_error=Mock(),
cloud_init_ok=Mock(),
interactive=Mock(),
echo_syslog_id=Mock(),
log_syslog_id=Mock(),
event_syslog_id=Mock(),
)

self.controller._handle_error_state(status)
self.controller.app.show_error_report.assert_not_called()
self.controller.app.show_nonreportable_error.assert_called_once()

# Reset mocks between cases
self.controller.app.show_error_report.reset_mock()
self.controller.app.show_nonreportable_error.reset_mock()

# Bug case

status: ApplicationStatus = ApplicationStatus(
state=ApplicationState.ERROR,
confirming_tty="",
error=None,
nonreportable_error=None,
cloud_init_ok=Mock(),
interactive=Mock(),
echo_syslog_id=Mock(),
log_syslog_id=Mock(),
event_syslog_id=Mock(),
)

with self.assertRaises(Exception):
self.controller._handle_error_state(status)
self.controller.app.show_error_report.assert_not_called()
self.controller.app.show_nonreportable_error.assert_not_called()
1 change: 1 addition & 0 deletions subiquity/common/apidef.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ def POST(tty: str) -> None:
"""Confirm that the installation should proceed."""

class restart:
@allowed_before_start
def POST() -> None:
"""Restart the server process."""

Expand Down
17 changes: 17 additions & 0 deletions subiquity/common/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import attr

from subiquity.server.nonreportable import NonReportableException
from subiquitycore.models.network import NetDevInfo


Expand Down Expand Up @@ -55,6 +56,21 @@ class ErrorReportRef:
oops_id: Optional[str]


@attr.s(auto_attribs=True)
class NonReportableError:
cause: str
message: str
details: Optional[str]

@classmethod
def from_exception(cls, exc: NonReportableException):
return cls(
cause=type(exc).__name__,
message=str(exc),
details=exc.details,
)


class ApplicationState(enum.Enum):
"""Represents the state of the application at a given time."""

Expand Down Expand Up @@ -87,6 +103,7 @@ class ApplicationStatus:
state: ApplicationState
confirming_tty: str
error: Optional[ErrorReportRef]
nonreportable_error: Optional[NonReportableError]
cloud_init_ok: Optional[bool]
interactive: Optional[bool]
echo_syslog_id: str
Expand Down
8 changes: 6 additions & 2 deletions subiquity/server/autoinstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,23 @@
# 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 logging
from typing import Optional

from subiquity.server.nonreportable import NonReportableException

log = logging.getLogger("subiquity.server.autoinstall")


class AutoinstallError(Exception):
class AutoinstallError(NonReportableException):
pass


class AutoinstallValidationError(AutoinstallError):
def __init__(
self,
owner: str,
details: Optional[str] = None,
):
self.message = f"Malformed autoinstall in {owner!r} section"
self.owner = owner
super().__init__(self.message)
super().__init__(self.message, details=details)
25 changes: 25 additions & 0 deletions subiquity/server/nonreportable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2024 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 logging
from typing import Optional

log = logging.getLogger("subiquity.server.nonreportable")


class NonReportableException(Exception):
def __init__(self, message: str, details: Optional[str] = None):
self.message: str = message
self.details: Optional[str] = None
super().__init__(message)
Loading

0 comments on commit 8721395

Please sign in to comment.