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 <jvalleroy@mailbox.org>
Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
James Valleroy 2023-11-19 08:15:06 -05:00 committed by Sunil Mohan Adapa
parent 3fcd6b9e58
commit 27284fe888
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
5 changed files with 98 additions and 25 deletions

View File

@ -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

View File

@ -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

View File

@ -16,7 +16,7 @@
value="{% trans "Run Diagnostics" %}"/>
</form>
{% if results %}
{% if results_available %}
<a class="btn btn-default" role="button"href="{% url 'diagnostics:full' %}">
{% trans "View Results" %}
</a>

View File

@ -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

View File

@ -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