diff --git a/computes/tests/test_unreachable_host_pages.py b/computes/tests/test_unreachable_host_pages.py new file mode 100644 index 00000000..b4706605 --- /dev/null +++ b/computes/tests/test_unreachable_host_pages.py @@ -0,0 +1,32 @@ +"""A host page whose list could not be read says why, and not that the host has none.""" + +from unittest.mock import patch + +from django.contrib.auth.models import User +from django.test import TestCase +from django.urls import reverse +from vrtManager.util import ConnectionFailed + +from computes.models import Compute + +EMPTY = { + "storages": "This host has no storage pools.", + "networks": "This host has no networks.", + "interfaces": "libvirt lists no interfaces of this host.", + "nwfilters": "This host has no network filters.", + "virtsecrets": "This host has no secrets.", +} + + +class UnreachableHostPagesTestCase(TestCase): + def setUp(self): + self.compute = Compute.objects.create(name="c", hostname="192.0.2.10", login="u", password="p", type=1) + self.client.force_login(User.objects.create_superuser("hostadmin", "hostadmin@example.com", "pw")) + + def test_the_connection_error_shows_without_an_empty_list(self): + failed = ConnectionFailed("Connection Failed: %(host)s did not answer within 5 s", host="192.0.2.10") + for page, empty in EMPTY.items(): + with self.subTest(page), patch("vrtManager.connection.connection_manager.get_connection", side_effect=failed): + content = self.client.get(reverse(page, args=[self.compute.id])).content.decode() + self.assertIn("192.0.2.10 did not answer within 5 s", content) + self.assertNotIn(empty, content) diff --git a/console/tests/test_views.py b/console/tests/test_views.py index e9139b2c..c9c9c294 100644 --- a/console/tests/test_views.py +++ b/console/tests/test_views.py @@ -14,7 +14,7 @@ from django.conf import settings from django.test import TestCase, override_settings from django.urls import reverse from instances.models import Instance -from libvirt import libvirtError +from vrtManager.util import ConnectionFailed from lxml import html @@ -323,16 +323,26 @@ class ConsoleViewsTestCase(TestCase): self.assertEqual("This console listens only on its host" in response.content.decode(), refused) @patch("console.views.wvmInstance") - def test_console_libvirt_error_fallback(self, mock_wvm): - mock_wvm.side_effect = libvirtError("Connection to libvirtd failed") + def test_a_host_that_cannot_be_reached_is_named_as_the_cause_and_logged(self, mock_wvm): + # not "check the console configuration of your VM": the VM was never read + mock_wvm.side_effect = ConnectionFailed("Connection Failed: %(host)s did not answer within 5 s", host="192.0.2.10") + for user, shown in ((self.admin_user, "Connection Failed: 192.0.2.10 did not answer within 5 s"), + (self.alice, "The host of this virtual machine cannot be reached right now.")): + with self.subTest(user.username): + self.client.force_login(user) + with self.assertLogs("console.views", "WARNING") as logs: + response = self.client.get(self.url) + self.assertTemplateUsed(response, "console-error.html") + self.assertEqual(str(response.context["console_error"]), shown) + self.assertIn("Console of vm-alpha on", logs.output[0]) + self.assertIn("192.0.2.10 did not answer", logs.output[0]) + @patch("console.views.wvmInstance") + def test_a_vm_without_a_readable_console_points_to_its_console_settings(self, mock_wvm): + mock_wvm.return_value.get_console_type.side_effect = IndexError self.client.force_login(self.admin_user) - url = self.url - response = self.client.get(url) - - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, "console-error.html") - self.assertIsNotNone(response.context["console_error"]) + response = self.client.get(self.url) + self.assertIn("check the console configuration", str(response.context["console_error"])) def test_an_unknown_vm_gives_the_superuser_an_error_page(self): self.client.force_login(self.admin_user) diff --git a/console/views.py b/console/views.py index c584550d..3e10bc1a 100644 --- a/console/views.py +++ b/console/views.py @@ -2,6 +2,7 @@ import functools import json +import logging from pathlib import Path from appsettings.settings import app_settings @@ -12,9 +13,13 @@ from django.utils.translation import gettext_lazy as _ from instances.models import Instance from instances.utils import can_open_console from libvirt import libvirtError +from operations.utils import safe_text from vrtManager.connection import CONN_TCP, CONN_TLS from vrtManager.instance import wvmInstance from vrtManager.util import is_loopback +from webvirtcloud.middleware import error_text + +logger = logging.getLogger(__name__) # one file a layout, written by dev/console_layout.py LAYOUTS_DIR = Path(__file__).resolve().parent / "layouts" @@ -60,7 +65,11 @@ def console(request, compute_id, uuid): unreachable = instance.compute.type in (CONN_TCP, CONN_TLS) and bool( conn.get_console_socket() or is_loopback(conn.get_console_listener_addr()) ) - except (libvirtError, ValueError, IndexError): + except libvirtError as err: + # the host or the VM could not be read: the page says that, not to check the console settings + logger.warning("Console of %s on %s: %s", instance.name, instance.compute.name, safe_text(err)) + return render(request, "console-error.html", {"instance": instance, "console_error": error_text(request, err)}) + except (ValueError, IndexError): running = unreachable = False console_type = None console_passwd = None diff --git a/interfaces/templates/interfaces.html b/interfaces/templates/interfaces.html index 2f110010..de16d7b0 100644 --- a/interfaces/templates/interfaces.html +++ b/interfaces/templates/interfaces.html @@ -15,7 +15,7 @@ {% include "computes/host_nav.html" with section="interfaces" %} {% if changeable is False %}{% include 'interfaces_unchangeable.html' %}{% endif %} - {% if not ifaces_all %} + {% if ifaces_all is None %}{# not read: the error above says why #}{% elif not ifaces_all %}