diff --git a/appsettings/migrations/0013_remove_qemu_console_default_type.py b/appsettings/migrations/0013_remove_qemu_console_default_type.py new file mode 100644 index 00000000..5ea0e4d0 --- /dev/null +++ b/appsettings/migrations/0013_remove_qemu_console_default_type.py @@ -0,0 +1,25 @@ +from django.db import migrations + + +def remove_console_type(apps, schema_editor): + """VNC is the only console type; the setting has nothing left to choose.""" + AppSettings = apps.get_model("appsettings", "AppSettings") + AppSettings.objects.using(schema_editor.connection.alias).filter(key="QEMU_CONSOLE_DEFAULT_TYPE").delete() + + +def restore_console_type(apps, schema_editor): + AppSettings = apps.get_model("appsettings", "AppSettings") + AppSettings.objects.using(schema_editor.connection.alias).get_or_create( + key="QEMU_CONSOLE_DEFAULT_TYPE", + defaults={"name": "VM Console Type", "value": "vnc", "choices": "vnc", "description": "Default console type"}, + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("appsettings", "0012_remove_spice_qemu_console_type"), + ] + + operations = [ + migrations.RunPython(remove_console_type, restore_console_type), + ] diff --git a/console/sshtunnels.py b/console/sshtunnels.py index 7f0376e0..44f7a53d 100644 --- a/console/sshtunnels.py +++ b/console/sshtunnels.py @@ -14,7 +14,7 @@ import threading class _TunnelScheduler(object): """ - If the user is using Spice + SSH URI + no SSH keys, we need to + If the user is using an SSH URI without SSH keys, we need to serialize connection opening otherwise ssh-askpass gets all angry. This handles the locking and scheduling. It's only instantiated once for the whole app, because we serialize @@ -181,7 +181,7 @@ class SSHTunnels(object): self._tunnels.append(t) # socket FDs are closed when the object is garbage collected. This - # can close an FD behind spice/vnc's back which causes crashes. + # can close an FD behind the VNC client's back which causes crashes. # # Dup a bare FD for the viewer side of things, but keep the high # level socket object for the SSH side, since it simplifies things diff --git a/console/tests.py b/console/tests.py index fb783a01..5181e9d0 100644 --- a/console/tests.py +++ b/console/tests.py @@ -108,9 +108,10 @@ class ConsoleViewsTestCase(TestCase): self.assertEqual(response.context["ws_port"], 5900) @patch("console.views.wvmInstance") - def test_console_superuser_spice_deprecated(self, mock_wvm): + def test_console_unsupported_graphics_type(self, mock_wvm): + # Only VNC is supported; a VM defined elsewhere may use another type. mock_conn = MagicMock() - mock_conn.get_console_type.return_value = "spice" + mock_conn.get_console_type.return_value = "rdp" mock_conn.get_console_websocket_port.return_value = None mock_wvm.return_value = mock_conn @@ -120,7 +121,7 @@ class ConsoleViewsTestCase(TestCase): self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "console-vnc-lite.html") - self.assertIn("SPICE", str(response.context["console_error"])) + self.assertIn("'rdp' is not supported", str(response.context["console_error"])) @override_settings(SERIAL_CONSOLE_ENABLED=True) @patch("console.views.wvmInstance") diff --git a/console/views.py b/console/views.py index 3a770556..83ba5e03 100644 --- a/console/views.py +++ b/console/views.py @@ -156,15 +156,10 @@ def console(request): console_error = _( "Fail to get console. Please check the console configuration of your VM." ) - elif console_type == "spice": - console_error = _( - "SPICE console protocol is deprecated and no longer supported. " - "Please switch VM graphics to VNC." - ) else: - console_error = _("Console type '%(type)s' has not support") % { - "type": console_type - } + console_error = _( + "Console type '%(type)s' is not supported. Please switch the VM graphics to VNC." + ) % {"type": console_type} response = render(request, "console-vnc-lite.html", locals()) # Only novncd reads this cookie; JavaScript gets the token from the page. diff --git a/instances/api/serializers.py b/instances/api/serializers.py index ee4c2272..999cfa03 100644 --- a/instances/api/serializers.py +++ b/instances/api/serializers.py @@ -52,7 +52,6 @@ class FlavorSerializer(serializers.ModelSerializer): class CreateInstanceSerializer(serializers.ModelSerializer): - graphics = serializers.CharField(initial="vnc") video = serializers.CharField(initial="vga") storage = serializers.CharField(initial="default") cache_mode = serializers.CharField(initial="none") @@ -76,7 +75,6 @@ class CreateInstanceSerializer(serializers.ModelSerializer): "virtio", "qemu_ga", "console_pass", - "graphics", "video", "listener_addr", ] diff --git a/instances/api/viewsets.py b/instances/api/viewsets.py index ce23e375..fd299c4c 100644 --- a/instances/api/viewsets.py +++ b/instances/api/viewsets.py @@ -302,7 +302,6 @@ class CreateInstanceViewSet(viewsets.ViewSet): volumes=volume_list, networks=serializer.validated_data["networks"], nwfilter=serializer.validated_data["nwfilter"], - graphics=serializer.validated_data["graphics"], virtio=serializer.validated_data["virtio"], listener_addr=serializer.validated_data["listener_addr"], video=serializer.validated_data["video"], diff --git a/instances/forms.py b/instances/forms.py index 58ed0496..38867454 100644 --- a/instances/forms.py +++ b/instances/forms.py @@ -1,6 +1,5 @@ import re -from appsettings.models import AppSettings from django import forms from django.utils.translation import gettext_lazy as _ from webvirtcloud.settings import QEMU_CONSOLE_LISTENER_ADDRESSES, QEMU_KEYMAPS @@ -15,7 +14,6 @@ class FlavorForm(forms.ModelForm): class ConsoleForm(forms.Form): - type = forms.ChoiceField(label=_("Type")) listen_on = forms.ChoiceField(label=_("Listen on")) generate_password = forms.BooleanField(label=_("Generate password"), required=False) clear_password = forms.BooleanField(label=_("Clear password"), required=False) @@ -29,12 +27,7 @@ class ConsoleForm(forms.Form): def __init__(self, *args, **kwargs): super(ConsoleForm, self).__init__(*args, **kwargs) - type_choices = ( - (c, c) - for c in AppSettings.objects.get(key="QEMU_CONSOLE_DEFAULT_TYPE").choices_as_list() - ) keymap_choices = [("auto", _("Auto"))] + list((c, c) for c in QEMU_KEYMAPS) - self.fields["type"] = forms.ChoiceField(label=_("Type"), choices=type_choices) self.fields["listen_on"] = forms.ChoiceField( label=_("Listen on"), choices=QEMU_CONSOLE_LISTENER_ADDRESSES diff --git a/instances/models.py b/instances/models.py index 99485724..dbeade0e 100644 --- a/instances/models.py +++ b/instances/models.py @@ -273,9 +273,6 @@ class CreateInstance(models.Model): console_pass = models.CharField(max_length=64, blank=True) add_cdrom = models.CharField(max_length=16) add_input = models.CharField(max_length=16) - graphics = models.CharField( - max_length=16, error_messages={"required": _("Please select a graphics type")} - ) video = models.CharField( max_length=16, error_messages={"required": _("Please select a video driver")} ) diff --git a/instances/templates/create_instance_w2.html b/instances/templates/create_instance_w2.html index c9593c8e..e70260c3 100644 --- a/instances/templates/create_instance_w2.html +++ b/instances/templates/create_instance_w2.html @@ -216,20 +216,6 @@ - {% if dom_caps.graphics_support == 'yes' %} -
- -
- -
-
- {% endif %}
@@ -483,20 +469,6 @@
- {% if dom_caps.graphics_support == 'yes' %} -
- -
- -
-
- {% endif %}
@@ -737,20 +709,6 @@
- {% if dom_caps.graphics_support == 'yes' %} -
- -
- -
-
- {% endif %}
diff --git a/instances/templates/instance.html b/instances/templates/instance.html index bbaa2e36..21625131 100644 --- a/instances/templates/instance.html +++ b/instances/templates/instance.html @@ -266,13 +266,6 @@ $("#console_select_keymap option[value='" + keymap + "']").prop('selected', true); } }); - $(document).ready(function () { - // set current console type or fall back to default - var console_type = "{{ instance.console_type }}"; - if (console_type != '') { - $("#console_select_type option[value='" + console_type + "']").prop('selected', true); - } - }); $(document).ready(function () { // set current console listen address or fall back to default var console_listener_address = "{{ instance.console_listener_address }}"; diff --git a/instances/templates/instances/settings_tab.html b/instances/templates/instances/settings_tab.html index c75cb029..ea932f19 100644 --- a/instances/templates/instances/settings_tab.html +++ b/instances/templates/instances/settings_tab.html @@ -723,7 +723,7 @@ {% bootstrap_form console_form layout='horizontal' %}
{% if instance.status != 5 %} - + {% else %} {% endif %} diff --git a/instances/test_update_console.py b/instances/test_update_console.py new file mode 100644 index 00000000..c36de08d --- /dev/null +++ b/instances/test_update_console.py @@ -0,0 +1,27 @@ +from unittest.mock import patch + +from computes.models import Compute +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.urls import reverse +from instances.models import Instance + + +class UpdateConsoleTestCase(TestCase): + def setUp(self): + admin = get_user_model().objects.create_superuser("console_admin", "c@example.com", "pw") + self.client.force_login(admin) + compute = Compute.objects.create(name="console-compute", hostname="127.0.0.1", login="", password="", type=4) + self.instance = Instance.objects.create( + compute=compute, name="console-vm", uuid="11111111-2222-3333-4444-555555555555" + ) + + def post(self, data): + with patch("instances.models.wvmInstance") as wvm: + self.client.post(reverse("instances:update_console", args=[self.instance.id]), data) + return wvm.return_value + + def test_console_settings_are_applied(self): + proxy = self.post({"listen_on": "127.0.0.1", "password": "", "keymap": "de"}) + proxy.set_console_keymap.assert_called_once_with("de") + proxy.set_console_listener_addr.assert_called_once_with("127.0.0.1") diff --git a/instances/tests.py b/instances/tests.py index 65e1b18d..288557e0 100644 --- a/instances/tests.py +++ b/instances/tests.py @@ -780,7 +780,7 @@ class InstancesTestCase(TestCase): def test_console(self): response = self.client.post( reverse("instances:update_console", args=[self.instance.id]), - {"type": "spice", "listen_on": "0.0.0.0", "password": "", "keymap": "auto"}, + {"listen_on": "0.0.0.0", "password": "", "keymap": "auto"}, HTTP_REFERER=reverse("index"), ) self.assertEqual(response.status_code, 302) diff --git a/instances/views.py b/instances/views.py index 448396eb..bed07f02 100755 --- a/instances/views.py +++ b/instances/views.py @@ -78,9 +78,6 @@ def instance(request, pk): users = User.objects.all().order_by("username") publickeys = UserSSHKey.objects.filter(user_id=request.user.id) keymaps = settings.QEMU_KEYMAPS - console_types = AppSettings.objects.get( - key="QEMU_CONSOLE_DEFAULT_TYPE" - ).choices_as_list() # The console form renders the VNC password, so only users who may manage # console settings get the form (and the password) at all. can_manage_console = utils.can_manage_console(request.user, instance) @@ -88,7 +85,6 @@ def instance(request, pk): if can_manage_console: console_form = ConsoleForm( initial={ - "type": instance.console_type, "listen_on": instance.console_listener_address, "password": instance.console_passwd, "keymap": instance.console_keymap, @@ -1889,19 +1885,14 @@ def update_console(request, pk): request.user.username, instance.compute.name, instance.name, msg ) - if "type" in form.changed_data: - instance.proxy.set_console_type(form.cleaned_data["type"]) - msg = _("Set VNC type") - addlogmsg( - request.user.username, instance.compute.name, instance.name, msg - ) - if "listen_on" in form.changed_data: instance.proxy.set_console_listener_addr(form.cleaned_data["listen_on"]) msg = _("Set VNC listen address") addlogmsg( request.user.username, instance.compute.name, instance.name, msg ) + else: + messages.error(request, _("Console settings were not saved: %(errors)s") % {"errors": form.errors.as_text()}) return get_safe_redirect( request, default=reverse("instances:instance", args=[instance.id]) + "#vncsettings" @@ -2085,7 +2076,6 @@ def create_instance(request, compute_id, arch, machine): net_models_host = conn.get_network_models() default_nic_type = app_settings.INSTANCE_NIC_DEFAULT_TYPE storages = sorted(conn.get_storages(only_actives=True)) - default_graphics = app_settings.QEMU_CONSOLE_DEFAULT_TYPE default_cdrom = app_settings.INSTANCE_CDROM_ADD input_device_buses = ["default", "virtio", "usb"] default_input_device_bus = app_settings.INSTANCE_INPUT_DEFAULT_DEVICE @@ -2265,7 +2255,6 @@ def create_instance(request, compute_id, arch, machine): listener_addr=data["listener_addr"], nwfilter=data["nwfilter"], net_model=data["net_model"], - graphics=data["graphics"], video=data["video"], console_pass=data["console_pass"], mac=data["mac"], diff --git a/vrtManager/connection.py b/vrtManager/connection.py index e2dfaed0..093b5ef0 100644 --- a/vrtManager/connection.py +++ b/vrtManager/connection.py @@ -418,10 +418,6 @@ class wvmConnect(object): result["disk_devices"] = self.get_disk_device_types(arch, machine) result["disk_bus"] = self.get_disk_bus_types(arch, machine) - result["graphics_support"] = util.get_xml_path(xml, "/domainCapabilities/devices/graphics/@supported") - if result["graphics_support"] == "yes": - result["graphics_types"] = self.get_graphics_types(arch, machine) - result["video_support"] = util.get_xml_path(xml, "/domainCapabilities/devices/video/@supported") if result["video_support"] == "yes": result["video_types"] = self.get_video_models(arch, machine) @@ -677,18 +673,6 @@ class wvmConnect(object): # return [ 'disk', 'cdrom', 'floppy', 'lun' ] return util.get_xml_path(self.get_dom_cap_xml(arch, machine), func=get_device_list) - def get_graphics_types(self, arch, machine): - """ - :param arch: architecture - :param machine: - :return: available graphics types - """ - - def get_graphics_list(ctx): - return [v.text for v in ctx.xpath("/domainCapabilities/devices/graphics/enum[@name='type']/value")] - - return util.get_xml_path(self.get_dom_cap_xml(arch, machine), func=get_graphics_list) - def get_cpu_modes(self, arch, machine): """ :param arch: architecture diff --git a/vrtManager/create.py b/vrtManager/create.py index 90cd5113..3304ed03 100644 --- a/vrtManager/create.py +++ b/vrtManager/create.py @@ -158,7 +158,6 @@ class wvmCreate(wvmConnect): volumes, networks, nwfilter, - graphics, virtio, listener_addr, net_model="virtio", @@ -337,7 +336,7 @@ class wvmCreate(wvmConnect): xml += """""" xml += f""" - + """ if qemu_ga and virtio: diff --git a/vrtManager/instance.py b/vrtManager/instance.py index 2ea0bd52..a0054f62 100644 --- a/vrtManager/instance.py +++ b/vrtManager/instance.py @@ -1130,19 +1130,6 @@ class wvmInstance(wvmConnect): ) return console_type - def set_console_type(self, console_type): - if console_type == "": - return False - root = ElementTree.fromstring(self._XMLDesc(PERSISTENT_XML)) - graphic = root.find("devices/graphics") - if graphic is None: - return False - if graphic.get("type") == console_type: - return True - graphic.set("type", console_type) - newxml = ElementTree.tostring(root).decode() - self._defineXML(newxml) - def get_console_port(self, console_type=None): if console_type is None: console_type = self.get_console_type() diff --git a/vrtManager/test_console_setters.py b/vrtManager/test_console_setters.py index ef6808b8..52588f25 100644 --- a/vrtManager/test_console_setters.py +++ b/vrtManager/test_console_setters.py @@ -9,11 +9,11 @@ if not settings.configured: from lxml import etree from vrtManager.instance import PERSISTENT_XML, wvmInstance -LIVE = "" -PERSISTENT = "" +LIVE = "" +PERSISTENT = "" -def running_vm_with_pending_graphics_type(): +def running_vm_with_pending_changes(): vm = wvmInstance.__new__(wvmInstance) vm.wvm = MagicMock() vm._XMLDesc = MagicMock(side_effect=lambda flags: PERSISTENT if flags == PERSISTENT_XML else LIVE) @@ -31,13 +31,15 @@ class ConsoleSettersTestCase(unittest.TestCase): "set_console_passwd": (lambda vm: vm.set_console_passwd("new"), "passwd", "new"), "set_console_keymap": (lambda vm: vm.set_console_keymap("de"), "keymap", "de"), "set_console_listener_addr": (lambda vm: vm.set_console_listener_addr("127.0.0.1"), "listen", "127.0.0.1"), - "set_console_type": (lambda vm: vm.set_console_type("vnc"), "type", "vnc"), } for name, (edit, attr, value) in edits.items(): with self.subTest(name): - vm = running_vm_with_pending_graphics_type() + vm = running_vm_with_pending_changes() edit(vm) - self.assertEqual(self.defined_graphics(vm).get(attr), value) + graphics = self.defined_graphics(vm) + self.assertEqual(graphics.get(attr), value) + # Built from the persistent device, not the live one + self.assertNotEqual(graphics.get("passwd"), "live") if __name__ == "__main__":