mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
feat(console): VNC is the only console type
SPICE was removed earlier; its leftovers and the console type choice go too: - the console settings form, update_console and the backend no longer offer or change the type; an invalid console form now shows its errors instead of being ignored; - the create form, the API serializer and create_instance no longer take a graphics type, new VMs get VNC; get_graphics_types and the graphics entries of the domain capabilities are gone; - migration appsettings.0013 deletes QEMU_CONSOLE_DEFAULT_TYPE; - a VM defined elsewhere with another graphics type gets "Console type ... is not supported". The locale catalogs still reference the deleted SPICE templates in source comments; regenerating them is left to the translation work.
This commit is contained in:
parent
9f86330552
commit
e5fda79f5a
18 changed files with 74 additions and 127 deletions
|
|
@ -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),
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -216,20 +216,6 @@
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% if dom_caps.graphics_support == 'yes' %}
|
||||
<div class="row">
|
||||
<label class="col-sm-4 col-form-label">{% trans "Graphics" %}</label>
|
||||
<div class="col-sm-7">
|
||||
<select class="form-select" name="graphics">
|
||||
{% for graphics in dom_caps.graphics_types %}
|
||||
{% if graphics != 'spice' %}
|
||||
<option value="{{ graphics }}" {% if default_graphics == graphics %}selected{% endif %}>{{ graphics }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<label class="col-sm-4 col-form-label">{% trans "Video" %}</label>
|
||||
<div class="col-sm-7">
|
||||
|
|
@ -483,20 +469,6 @@
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% if dom_caps.graphics_support == 'yes' %}
|
||||
<div class="row">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Graphics" %}</label>
|
||||
<div class="col-sm-7">
|
||||
<select class="form-select" name="graphics">
|
||||
{% for graphics in dom_caps.graphics_types %}
|
||||
{% if graphics != 'spice' %}
|
||||
<option value="{{ graphics }}" {% if default_graphics == graphics %}selected{% endif %}>{{ graphics }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Video" %}</label>
|
||||
<div class="col-sm-7">
|
||||
|
|
@ -737,20 +709,6 @@
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% if dom_caps.graphics_support == 'yes' %}
|
||||
<div class="row">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Graphics" %}</label>
|
||||
<div class="col-sm-7">
|
||||
<select class="form-select" name="graphics">
|
||||
{% for graphics in dom_caps.graphics_types %}
|
||||
{% if graphics != 'spice' %}
|
||||
<option value="{{ graphics }}" {% if default_graphics == graphics %}selected{% endif %}>{{ graphics }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Video" %}</label>
|
||||
<div class="col-sm-7">
|
||||
|
|
|
|||
|
|
@ -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 }}";
|
||||
|
|
|
|||
|
|
@ -723,7 +723,7 @@
|
|||
{% bootstrap_form console_form layout='horizontal' %}
|
||||
<div class="float-end">
|
||||
{% if instance.status != 5 %}
|
||||
<button class="btn btn-success" name="set_console_type" disabled>{% trans "Update" %}</button>
|
||||
<button class="btn btn-success" disabled>{% trans "Update" %}</button>
|
||||
{% else %}
|
||||
<button type="submit" class="btn btn-success ">{% trans "Update" %}</button>
|
||||
{% endif %}
|
||||
|
|
|
|||
27
instances/test_update_console.py
Normal file
27
instances/test_update_console.py
Normal file
|
|
@ -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")
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 += """<input type='tablet'/>"""
|
||||
|
||||
xml += f"""
|
||||
<graphics type='{graphics}' port='-1' autoport='yes' {console_pass} listen='{listener_addr}'/>
|
||||
<graphics type='vnc' port='-1' autoport='yes' {console_pass} listen='{listener_addr}'/>
|
||||
<console type='pty'/> """
|
||||
|
||||
if qemu_ga and virtio:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ if not settings.configured:
|
|||
from lxml import etree
|
||||
from vrtManager.instance import PERSISTENT_XML, wvmInstance
|
||||
|
||||
LIVE = "<domain><devices><graphics type='vnc' passwd='live'><listen type='address'/></graphics></devices></domain>"
|
||||
PERSISTENT = "<domain><devices><graphics type='spice' passwd='old'><listen type='address'/></graphics></devices></domain>"
|
||||
LIVE = "<domain><devices><graphics type='vnc' passwd='live' keymap='en-us'><listen type='address'/></graphics></devices></domain>"
|
||||
PERSISTENT = "<domain><devices><graphics type='vnc' passwd='old'><listen type='address'/></graphics></devices></domain>"
|
||||
|
||||
|
||||
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__":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue