From 27284fe888dbc9793a8823f59e9ab4041de7184d Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Sun, 19 Nov 2023 08:15:06 -0500 Subject: [PATCH] diagnostics: Store results of full run in database Tests: - Run diagnostics. Restart plinth, and check that the diagnostics results are still available to view. Signed-off-by: James Valleroy Reviewed-by: Sunil Mohan Adapa --- plinth/modules/diagnostics/__init__.py | 48 +++++++++++++++++-- plinth/modules/diagnostics/check.py | 34 +++++++++++-- .../diagnostics/templates/diagnostics.html | 2 +- .../modules/diagnostics/tests/test_check.py | 21 +++++++- plinth/modules/diagnostics/views.py | 18 +------ 5 files changed, 98 insertions(+), 25 deletions(-) diff --git a/plinth/modules/diagnostics/__init__.py b/plinth/modules/diagnostics/__init__.py index 0285270a2..5608d2037 100644 --- a/plinth/modules/diagnostics/__init__.py +++ b/plinth/modules/diagnostics/__init__.py @@ -4,22 +4,24 @@ FreedomBox app for system diagnostics. """ import collections +import json import logging import pathlib import threading +from copy import deepcopy import psutil from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_noop from plinth import app as app_module -from plinth import daemon, glib, menu +from plinth import daemon, glib, kvstore, menu from plinth import operation as operation_module from plinth.modules.apache.components import diagnose_url_on_all from plinth.modules.backups.components import BackupRestore from . import manifest -from .check import Result +from .check import CheckJSONDecoder, CheckJSONEncoder, Result, translate_checks _description = [ _('The system diagnostic test will run a number of checks on your ' @@ -117,8 +119,7 @@ def _run_on_all_enabled_modules(): continue apps.append((app.app_id, app)) - app_name = app.info.name or app.app_id - current_results['results'][app.app_id] = {'name': app_name} + current_results['results'][app.app_id] = {'id': app.app_id} current_results['apps'] = apps @@ -266,6 +267,9 @@ def _run_diagnostics(): _run_on_all_enabled_modules() with results_lock: results = current_results['results'] + # Store the most recent results in the database. + kvstore.set('diagnostics_results', + json.dumps(results, cls=CheckJSONEncoder)) issue_count = 0 severity = 'warning' @@ -309,3 +313,39 @@ def _run_diagnostics(): message=message, actions=actions, data=data, group='admin') note.dismiss(False) + + +def are_results_available(): + """Return whether diagnostic results are available.""" + with results_lock: + results = current_results + + if not results: + results = kvstore.get_default('diagnostics_results', '{}') + results = json.loads(results) + + return bool(results) + + +def get_results(): + """Return the latest results of full diagnostics.""" + with results_lock: + results = deepcopy(current_results) + + # If no results are available in memory, then load from database. + if not results: + results = kvstore.get_default('diagnostics_results', '{}') + results = json.loads(results, cls=CheckJSONDecoder) + results = {'results': results, 'progress_percentage': 100} + + # Translate and format diagnostic check descriptions for each app + for app_id in results['results']: + app = app_module.App.get(app_id) + app_name = app.info.name or app_id + results['results'][app_id]['name'] = app_name + if 'diagnosis' in results['results'][app_id]: + diagnosis = results['results'][app_id]['diagnosis'] + results['results'][app_id]['diagnosis'] = translate_checks( + diagnosis) + + return results diff --git a/plinth/modules/diagnostics/check.py b/plinth/modules/diagnostics/check.py index 0c257d5f0..e72cfdcdb 100644 --- a/plinth/modules/diagnostics/check.py +++ b/plinth/modules/diagnostics/check.py @@ -1,8 +1,10 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """Diagnostic check data type.""" +import dataclasses from dataclasses import dataclass, field from enum import StrEnum +import json from django.utils.translation import gettext @@ -18,9 +20,6 @@ class Result(StrEnum): ERROR = 'error' -# TODO: Description should not be translated until we need to display it. - - @dataclass class DiagnosticCheck: """A diagnostic check and optional result and parameters.""" @@ -44,3 +43,32 @@ def translate(check: DiagnosticCheck) -> DiagnosticCheck: def translate_checks(checks: list[DiagnosticCheck]) -> list[DiagnosticCheck]: """Translate and format diagnostic checks.""" return [translate(check) for check in checks] + + +class CheckJSONEncoder(json.JSONEncoder): + """Encode objects that include DiagnosticChecks.""" + + def default(self, o): + """Add class tag to DiagnosticChecks.""" + if isinstance(o, DiagnosticCheck): + o = dataclasses.asdict(o) + o.update({'__class__': 'DiagnosticCheck'}) + return o + + return super().default(o) + + +class CheckJSONDecoder(json.JSONDecoder): + """Decode objects that include DiagnosticChecks.""" + + def __init__(self): + json.JSONDecoder.__init__(self, object_hook=CheckJSONDecoder.from_dict) + + @staticmethod + def from_dict(data): + """Convert tagged data to DiagnosticCheck.""" + if data.get('__class__') == 'DiagnosticCheck': + return DiagnosticCheck(data['check_id'], data['description'], + data['result'], data['parameters']) + + return data diff --git a/plinth/modules/diagnostics/templates/diagnostics.html b/plinth/modules/diagnostics/templates/diagnostics.html index ef102596f..03412c641 100644 --- a/plinth/modules/diagnostics/templates/diagnostics.html +++ b/plinth/modules/diagnostics/templates/diagnostics.html @@ -16,7 +16,7 @@ value="{% trans "Run Diagnostics" %}"/> - {% if results %} + {% if results_available %} {% trans "View Results" %} diff --git a/plinth/modules/diagnostics/tests/test_check.py b/plinth/modules/diagnostics/tests/test_check.py index a41148606..d92ac02b1 100644 --- a/plinth/modules/diagnostics/tests/test_check.py +++ b/plinth/modules/diagnostics/tests/test_check.py @@ -1,9 +1,13 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """Tests for diagnostic check data type.""" +import json import pytest -from plinth.modules.diagnostics.check import DiagnosticCheck, Result, translate +from plinth.modules.diagnostics.check import (DiagnosticCheck, + CheckJSONEncoder, + CheckJSONDecoder, Result, + translate) def test_result(): @@ -57,3 +61,18 @@ def test_translate(): Result.PASSED, {'key': 'value'}) translated = translate(check) assert translated.description == 'sample check ?missing?' + + +def test_json_encoder_decoder(): + """Test encoding and decoding as JSON.""" + check = DiagnosticCheck('some-check-id', 'sample check', Result.PASSED) + check_json = json.dumps(check, cls=CheckJSONEncoder) + for string in [ + '"check_id": "some-check-id"', '"description": "sample check"', + '"result": "passed"', '"parameters": {}', + '"__class__": "DiagnosticCheck"' + ]: + assert string in check_json + + decoded_check = json.loads(check_json, cls=CheckJSONDecoder) + assert decoded_check == check diff --git a/plinth/modules/diagnostics/views.py b/plinth/modules/diagnostics/views.py index 2591bf264..b712dc7eb 100644 --- a/plinth/modules/diagnostics/views.py +++ b/plinth/modules/diagnostics/views.py @@ -3,7 +3,6 @@ FreedomBox app for running diagnostics. """ -from copy import deepcopy import logging from django.http import Http404, HttpResponseRedirect @@ -36,12 +35,9 @@ class DiagnosticsView(AppView): def get_context_data(self, **kwargs): """Return additional context for rendering the template.""" - with diagnostics.results_lock: - results = diagnostics.current_results - context = super().get_context_data(**kwargs) context['has_diagnostics'] = False - context['results'] = results + context['results_available'] = diagnostics.are_results_available() return context @@ -62,19 +58,9 @@ class DiagnosticsFullView(TemplateView): except KeyError: is_task_running = False - with diagnostics.results_lock: - results = deepcopy(diagnostics.current_results) - - # Translate and format diagnostic check descriptions for each app - for app_id in results['results']: - if 'diagnosis' in results['results'][app_id]: - diagnosis = results['results'][app_id]['diagnosis'] - results['results'][app_id]['diagnosis'] = translate_checks( - diagnosis) - context = super().get_context_data(**kwargs) context['is_task_running'] = is_task_running - context['results'] = results + context['results'] = diagnostics.get_results() context['refresh_page_sec'] = 3 if is_task_running else None return context