1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00

feat(instances): a VM's console settings in two cards, with a password action

Settings > Console was one horizontal form, and on a running VM its
Update button was disabled. The owner chose this layout from three
rendered options: the console type on top, then a "VNC connection" card
(listen address, keymap) and a "VNC password" card.

- The password is changed by an action: keep it, generate a new one,
  set a new one or remove it. This replaces the Generate and Clear
  boxes and their precedence, and an emptied field no longer removes
  the password silently. The pane says whether a password is set; the
  form no longer carries the password itself.
- A VNC password takes at most 8 bytes: libvirt refuses a longer one
  ("VNC password is 9 characters long, only 8 permitted"). Generate and
  a new VM's random password now make 8 characters; they made 12, which
  libvirt 11 refuses. A set password keeps its spaces.
- Every check is in the form, so a refused save changes nothing; before,
  a refused password removal still saved the other settings.
- The settings can be saved while the VM runs: they are in the
  persistent definition. A line says the running console keeps its
  settings until the VM is shut down and started.
- The console type shown and compared is the persistent one; it was the
  running VM's, so a pending switch to Serial showed VNC and a later
  save could switch again.
- While the saved type is Serial, the VNC settings are not shown or
  taken; choosing Serial ignores the posted VNC settings.
This commit is contained in:
catborise 2026-10-30 12:26:22 +03:00
parent 20cfc25367
commit 301b73155d
12 changed files with 248 additions and 126 deletions

View file

@ -21,24 +21,35 @@ class FlavorForm(forms.ModelForm):
class ConsoleForm(forms.Form):
console_type = forms.ChoiceField(label=_("Console type"), required=False)
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)
password = forms.CharField(
label=_("Password"),
widget=forms.PasswordInput(render_value=True),
required=False
console_type = forms.ChoiceField(
label=_("Console type"), required=False, widget=forms.Select(attrs={"class": "form-select"})
)
password_action = forms.ChoiceField(
label=_("Password"),
choices=[
("keep", _("Keep the password")),
("generate", _("Generate a new one")),
("set", _("Set a new one")),
("none", _("No password")),
],
widget=forms.RadioSelect(attrs={"class": "form-check-input"}),
)
password = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(attrs={"class": "form-control", "autocomplete": "new-password"}),
required=False,
strip=False,
)
keymap = forms.ChoiceField(label=_("Keymap"), required=False)
def __init__(self, *args, instance, can_set_listener, **kwargs):
def __init__(self, *args, instance, is_superuser, **kwargs):
# the VM's settings as the initial values: a save changes only the fields edited
self.instance = instance
self.is_superuser = is_superuser
listener = instance.config_console_listener_address
kwargs["initial"] = {
"console_type": instance.console_type,
"console_type": instance.config_console_type,
"listen_on": listener,
"password": instance.config_console_passwd,
"password_action": "keep",
"keymap": instance.config_console_keymap or "auto",
}
super(ConsoleForm, self).__init__(*args, **kwargs)
@ -46,6 +57,11 @@ class ConsoleForm(forms.Form):
if settings.SERIAL_CONSOLE_ENABLED:
types.append(("pty", _("Serial (xterm; the guest needs a getty on ttyS0)")))
self.fields["console_type"].choices = types
# the VNC settings are shown and saved while the console is VNC; a
# switch to Serial ignores them
if "pty" in (instance.config_console_type, self.data.get("console_type")):
del self.fields["password_action"], self.fields["password"]
return
keymap_choices = [("auto", _("Auto"))] + list((c, c) for c in QEMU_KEYMAPS)
# Only administrators choose the listen address: beyond localhost the
# console is reachable from the network. A disabled field ignores
@ -57,10 +73,32 @@ class ConsoleForm(forms.Form):
self.fields["listen_on"] = forms.ChoiceField(
label=_("Listen on"),
choices=listeners,
disabled=not can_set_listener,
required=can_set_listener,
disabled=not is_superuser,
required=is_superuser,
widget=forms.Select(attrs={"class": "form-select"}),
)
self.fields["keymap"] = forms.ChoiceField(label=_("Keymap"), choices=keymap_choices)
self.fields["keymap"] = forms.ChoiceField(
label=_("Keymap"), choices=keymap_choices, widget=forms.Select(attrs={"class": "form-select"})
)
def clean(self):
data = super().clean()
action = data.get("password_action")
if action == "set":
password = data.get("password", "")
if not password:
self.add_error("password", _("Enter the new password."))
# VNC uses 8 bytes; libvirt refuses a longer password
elif len(password.encode()) > 8:
self.add_error("password", _("A VNC password takes at most 8 bytes."))
# A console that listens beyond localhost (set by an administrator)
# keeps its password unless an administrator removes it.
if action == "none" and not self.is_superuser and not self.instance.proxy.console_listens_locally():
self.add_error(
"password_action",
_("The console listens beyond localhost; only an administrator can remove its password."),
)
return data
class NewVMForm(forms.ModelForm):

View file

@ -225,6 +225,10 @@ class Instance(models.Model):
which their forms change."""
return self.proxy.get_video_model(config=True)
@cached_property
def config_console_type(self) -> str | None:
return self.proxy.get_console_type(config=True)
@cached_property
def config_console_listener_address(self) -> str:
return self.proxy.get_console_listener_addr(config=True)

View file

@ -630,23 +630,64 @@
{% endif %}
{% if can_manage_console %}
<div role="tabpanel" class="tab-pane" id="vncsettings">
<form method="post" action="{% url 'instances:update_console' instance.id %}">
{% csrf_token %}
<form method="post" action="{% url 'instances:update_console' instance.id %}" aria-label="{% trans "Console settings" %}">{% csrf_token %}
{% if instance.status != 5 %}
<div class="alert alert-warning">
{% trans "To change console settings, shutdown the instance." %}
</div>
<p class="small text-body-secondary">{% trans "The running console keeps its current settings; changes here apply after the VM is shut down and started." %}</p>
{% endif %}
{% bootstrap_form console_form layout='horizontal' %}
<div class="float-end">
{% if instance.status != 5 %}
<button class="btn btn-primary" disabled>{% trans "Update" %}</button>
<section class="mb-4" aria-labelledby="console-heading">
<h2 class="h6" id="console-heading">{% trans "Console" %}</h2>
<label class="form-label" for="{{ console_form.console_type.id_for_label }}">{{ console_form.console_type.label }}</label>
{{ console_form.console_type }}
{% if console_form.keymap %}
<p class="small text-body-secondary mt-1 mb-0">{% trans "The settings below apply to a VNC console." %}</p>
{% else %}
<button type="submit" class="btn btn-primary ">{% trans "Update" %}</button>
<p class="small text-body-secondary mt-1 mb-0">{% trans "The VNC settings appear once the console type VNC is saved." %}</p>
{% endif %}
</section>
{% if console_form.keymap %}
<div class="row g-3 mb-3">
<div class="col-12 col-lg-6">
<section class="card h-100" aria-labelledby="console-connection">
<div class="card-body">
<h2 class="h6 mb-3" id="console-connection">{% bs_icon 'display' %} {% trans "VNC connection" %}</h2>
<div class="mb-4">
<label class="form-label" for="{{ console_form.listen_on.id_for_label }}">{{ console_form.listen_on.label }}</label>
{{ console_form.listen_on }}
<p class="small text-body-secondary mt-1 mb-0">
{% if not request.user.is_superuser %}{% trans "Only administrators can change this." %} {% endif %}{% trans "Beyond localhost the console is reachable from the network." %}
</p>
</div>
<label class="form-label" for="{{ console_form.keymap.id_for_label }}">{{ console_form.keymap.label }}</label>
{{ console_form.keymap }}
</div>
</section>
</div>
<div class="col-12 col-lg-6">
<section class="card h-100" aria-labelledby="console-password">
<div class="card-body">
<h2 class="h6 mb-3" id="console-password">{% bs_icon 'key' %} {% trans "VNC password" %}</h2>
<fieldset>
<legend class="fs-6 mb-1">{{ console_form.password_action.label }}</legend>
<p class="small text-body-secondary mb-2">{% if instance.config_console_passwd %}{% trans "A password is set." %}{% else %}{% trans "No password is set." %}{% endif %}</p>
{% for choice in console_form.password_action %}
<div class="form-check">
{{ choice.tag }}
<label class="form-check-label" for="{{ choice.id_for_label }}">{{ choice.choice_label }}</label>
</div>
{% endfor %}
</fieldset>
<label class="form-label mt-3" for="{{ console_form.password.id_for_label }}">{{ console_form.password.label }}</label>
{{ console_form.password }}
<p class="small text-body-secondary mt-1 mb-0">{% trans "Up to 8 characters (an accented letter counts as two), used with \"Set a new one\"." %}</p>
</div>
</section>
</div>
</div>
{% endif %}
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-sm btn-primary">{% trans "Save console settings" %}</button>
</div>
</form>
<div class="clearfix"></div>
</div>
{% endif %}
{% if can_clone %}

View file

@ -1,6 +1,6 @@
"""Only administrators choose where a VM's console listens. Other users who may
manage the console cannot make it listen beyond localhost, and cannot remove
the password of a console that does."""
the password of a console that does; such a refusal saves nothing."""
from unittest.mock import PropertyMock, patch
@ -8,7 +8,7 @@ from accounts.models import UserInstance
from computes.models import Compute
from django.contrib.auth import get_user_model
from django.contrib.messages import get_messages
from django.test import TestCase
from django.test import TestCase, override_settings
from django.urls import reverse
from instances.models import Instance
from vrtManager.instance import PERSISTENT_XML, wvmInstance
@ -36,24 +36,33 @@ class ConsoleListenerTests(TestCase):
return proxy, [str(m) for m in get_messages(response.wsgi_request)]
def test_owner_cannot_change_the_listen_address(self):
proxy, _ = self.post(self.owner, {"listen_on": "0.0.0.0", "keymap": "auto"})
proxy, _ = self.post(self.owner, {"listen_on": "0.0.0.0", "keymap": "auto", "password_action": "keep"})
proxy.set_console_listener_addr.assert_not_called()
def test_owner_cannot_remove_the_password_of_a_console_listening_beyond_localhost(self):
proxy, messages = self.post(self.owner, {"clear_password": "on", "keymap": "auto"}, local=False)
proxy, messages = self.post(self.owner, {"password_action": "none", "keymap": "de"}, local=False)
proxy.set_console_passwd.assert_not_called()
proxy.set_console_keymap.assert_not_called()
self.assertTrue(any("password" in m.lower() for m in messages), messages)
@override_settings(SERIAL_CONSOLE_ENABLED=True)
def test_owner_can_switch_a_console_listening_beyond_localhost_to_serial(self):
proxy, messages = self.post(self.owner, {"console_type": "pty", "password_action": "none", "keymap": "auto"},
local=False)
self.assertEqual(messages, [])
proxy.set_console_type.assert_called_once_with("pty")
proxy.set_console_passwd.assert_not_called()
def test_owner_can_remove_the_password_of_a_local_console(self):
proxy, _ = self.post(self.owner, {"clear_password": "on", "keymap": "auto"})
proxy, _ = self.post(self.owner, {"password_action": "none", "keymap": "auto"})
proxy.set_console_passwd.assert_called_once_with("")
def test_owner_can_set_a_password_of_any_console(self):
proxy, _ = self.post(self.owner, {"password": "s3cret-pass", "keymap": "auto"}, local=False)
proxy.set_console_passwd.assert_called_once_with("s3cret-pass")
proxy, _ = self.post(self.owner, {"password_action": "set", "password": "s3cret-1", "keymap": "auto"}, local=False)
proxy.set_console_passwd.assert_called_once_with("s3cret-1")
def test_superuser_can_open_the_console_to_all_interfaces(self):
proxy, _ = self.post(self.admin, {"listen_on": "0.0.0.0", "clear_password": "on", "keymap": "auto"})
proxy, _ = self.post(self.admin, {"listen_on": "0.0.0.0", "password_action": "none", "keymap": "auto"})
proxy.set_console_listener_addr.assert_called_once_with("0.0.0.0")
proxy.set_console_passwd.assert_called_once_with("")

View file

@ -1,9 +1,9 @@
"""
The console settings form on the VM detail page carries the VNC password
(PasswordInput(render_value=True)). It must only be rendered for users who may
change console settings, which is the rule update_console enforces: a
superuser, or an owner with both is_change and is_vnc. In particular, is_staff
and the global view_instances permission must not reveal the VNC password.
The console settings form is rendered only for users who may change console
settings, which is the rule update_console enforces: a superuser, or an owner
with both is_change and is_vnc; not for is_staff or the global view_instances
permission. The form never carries the VNC password: it says whether one is
set.
"""
from unittest.mock import PropertyMock, patch
@ -75,14 +75,11 @@ class ConsoleSettingsAccessTestCase(TestCase):
self.assertEqual(response.status_code, 200)
return response.content.decode()
def test_staff_with_view_instances_does_not_see_vnc_password(self):
self.assertFalse(VNC_PASSWORD in self._detail_page(self.staff_viewer), "VNC password leaked")
def test_staff_owner_without_is_vnc_does_not_see_vnc_password(self):
self.assertFalse(VNC_PASSWORD in self._detail_page(self.staff_owner), "VNC password leaked")
def test_owner_with_is_change_and_is_vnc_sees_console_settings(self):
self.assertTrue(VNC_PASSWORD in self._detail_page(self.vnc_owner), "console settings missing")
def test_superuser_sees_console_settings(self):
self.assertTrue(VNC_PASSWORD in self._detail_page(self.superuser), "console settings missing")
def test_only_users_who_may_change_the_console_see_its_settings(self):
form = reverse("instances:update_console", args=[self.instance.id])
for user, shown in ((self.staff_viewer, False), (self.staff_owner, False),
(self.vnc_owner, True), (self.superuser, True)):
with self.subTest(user.username):
page = self._detail_page(user)
self.assertEqual(form in page, shown)
self.assertNotIn(VNC_PASSWORD, page)

View file

@ -156,7 +156,9 @@ class PendingEditsTestCase(TestCase):
self.assertEqual(doc.xpath("//select[@name='video_model']/option[@selected]/@value"), ["virtio"])
self.assertEqual(doc.xpath("//select[@name='keymap']/option[@selected]/@value"), ["de"])
self.assertEqual(doc.xpath("//select[@name='listen_on']/option[@selected]/@value"), ["0.0.0.0"])
self.assertEqual(doc.xpath("//input[@name='password']/@value"), ["pending-pw"])
# the pending password is not shown, only that one is set
self.assertEqual(doc.xpath("//input[@name='password']/@value"), [])
self.assertIn("A password is set.", doc.xpath("string(//div[@id='vncsettings']//fieldset)"))
def test_a_boot_order_by_device_type_is_named_and_no_device_is_checked(self):
doc = self.page(live=[disk("vda", "default", "")], config=[disk("vda", "default", "")],

View file

@ -1,11 +1,19 @@
"""Console settings: each change is saved to the persistent definition, the
password only by the action chosen for it, and the VNC settings only while
the saved console type is VNC."""
from unittest.mock import patch
from computes.models import Compute
from django.contrib.auth import get_user_model
from django.contrib.messages import get_messages
from django.test import TestCase, override_settings
from django.urls import reverse
from instances.forms import ConsoleForm
from instances.models import Instance
VNC = {"console_type": "vnc", "listen_on": "127.0.0.1", "keymap": "auto", "password_action": "keep"}
class UpdateConsoleTestCase(TestCase):
def setUp(self):
@ -16,27 +24,67 @@ class UpdateConsoleTestCase(TestCase):
compute=compute, name="console-vm", uuid="11111111-2222-3333-4444-555555555555"
)
def post(self, data):
def post(self, data, console_type="vnc"):
"""console_type: the persistent definition's; the live one is VNC."""
with patch("instances.models.wvmInstance") as wvm:
self.client.post(reverse("instances:update_console", args=[self.instance.id]), data)
return wvm.return_value
proxy = wvm.return_value
proxy.get_console_type.side_effect = lambda config=False: console_type if config else "vnc"
proxy.get_console_listener_addr.return_value = "127.0.0.1"
proxy.get_console_keymap.return_value = ""
proxy.set_console_passwd.return_value = True
response = self.client.post(reverse("instances:update_console", args=[self.instance.id]), data)
return proxy, [str(m) for m in get_messages(response.wsgi_request)]
def test_console_settings_are_applied(self):
proxy = self.post({"listen_on": "127.0.0.1", "password": "", "keymap": "de"})
proxy, _ = self.post({**VNC, "listen_on": "0.0.0.0", "keymap": "de"})
proxy.set_console_keymap.assert_called_once_with("de")
proxy.set_console_listener_addr.assert_called_once_with("127.0.0.1")
proxy.set_console_listener_addr.assert_called_once_with("0.0.0.0")
proxy.set_console_passwd.assert_not_called()
def test_the_password_changes_by_the_action_chosen(self):
for action, password in (("set", " a b c "), ("set", "éééé"), ("none", "")):
with self.subTest(action, password=password):
proxy, _ = self.post({**VNC, "password_action": action, "password": password})
proxy.set_console_passwd.assert_called_once_with(password)
proxy, _ = self.post({**VNC, "password_action": "generate"})
(generated,), _ = proxy.set_console_passwd.call_args
self.assertEqual(len(generated), 8)
def test_a_password_vnc_cannot_take_is_refused_and_nothing_is_saved(self):
for password in ("", "123456789", "ééééé"): # libvirt counts bytes: é is two
with self.subTest(password=password):
proxy, messages = self.post({**VNC, "keymap": "de", "password_action": "set", "password": password})
proxy.set_console_passwd.assert_not_called()
proxy.set_console_keymap.assert_not_called()
self.assertTrue(messages[0].startswith("Console settings were not saved"), messages)
@override_settings(SERIAL_CONSOLE_ENABLED=True)
def test_a_new_console_type_and_its_vnc_settings(self):
data = {"listen_on": "0.0.0.0", "keymap": "de"}
for old, new, vnc_settings in (("vnc", "pty", False), ("pty", "vnc", True)):
with self.subTest(new), patch("instances.models.wvmInstance") as wvm:
wvm.return_value.get_console_type.return_value = old
self.client.post(reverse("instances:update_console", args=[self.instance.id]), dict(data, console_type=new))
wvm.return_value.set_console_type.assert_called_once_with(new)
self.assertEqual(wvm.return_value.set_console_listener_addr.called, vnc_settings)
def test_a_new_serial_console_ignores_the_vnc_settings(self):
# even ones that would be refused: an empty new password, an unknown keymap
for vnc in ({"keymap": "de", "password_action": "none"},
{"keymap": "xx", "password_action": "set", "password": ""}):
with self.subTest(vnc):
proxy, messages = self.post({**VNC, **vnc, "console_type": "pty"})
self.assertEqual(messages, [])
proxy.set_console_type.assert_called_once_with("pty")
proxy.set_console_keymap.assert_not_called()
proxy.set_console_passwd.assert_not_called()
@override_settings(SERIAL_CONSOLE_ENABLED=True)
def test_a_saved_serial_console_shows_and_takes_no_vnc_settings(self):
# the running VM still has VNC: the type to compare with is the saved one
proxy, messages = self.post({"console_type": "pty"}, console_type="pty")
self.assertEqual(messages, [])
proxy.set_console_type.assert_not_called()
proxy, _ = self.post({"console_type": "vnc", "keymap": "de"}, console_type="pty")
proxy.set_console_type.assert_called_once_with("vnc")
proxy.set_console_keymap.assert_not_called()
with patch("instances.models.wvmInstance") as wvm:
wvm.return_value.get_console_type.side_effect = lambda config=False: "pty" if config else "vnc"
form = ConsoleForm(instance=Instance.objects.get(pk=self.instance.pk), is_superuser=True)
self.assertEqual((form["console_type"].value(), sorted(form.fields)), ("pty", ["console_type"]))
@override_settings(SERIAL_CONSOLE_ENABLED=False)
def test_serial_is_not_offered_while_the_serial_console_is_off(self):
proxy = self.post({"console_type": "pty", "listen_on": "127.0.0.1", "keymap": "de"})
proxy, _ = self.post({**VNC, "console_type": "pty"})
proxy.set_console_type.assert_not_called()

View file

@ -765,7 +765,7 @@ class InstancesTestCase(TestCase):
def test_console(self):
response = self.client.post(
reverse("instances:update_console", args=[self.instance.id]),
{"listen_on": "0.0.0.0", "password": "", "keymap": "auto"},
{"listen_on": "0.0.0.0", "password_action": "keep", "keymap": "auto"},
HTTP_REFERER=reverse("index"),
)
self.assertEqual(response.status_code, 302)

View file

@ -96,11 +96,11 @@ class VMRowTests(TestCase):
self.proxy.get_console_type.return_value = "vnc"
self.proxy.console_listens_locally.return_value = True
self.proxy.set_console_passwd.return_value = False # no graphics device
form = {"listen_on": "127.0.0.1", "keymap": "auto"}
self.post("update_console", {**form, "password": "Vnc-pass-1"})
form = {"listen_on": "127.0.0.1", "keymap": "auto", "password_action": "set"}
self.post("update_console", {**form, "password": "Vnc-pw-1"})
self.proxy.set_console_passwd.return_value = True
self.post("update_console", {**form, "password": "Vnc-pass-2"})
self.assertNotIn("Vnc-pass", str(self.rows()))
self.post("update_console", {**form, "password": "Vnc-pw-2"})
self.assertNotIn("Vnc-pw", str(self.rows()))
rows = [row for row in self.rows() if "password" in row[0]]
self.assertEqual(rows, [("VM vm: set console password: The VM has no graphics device", False),
("VM vm: set console password", True)])
@ -112,10 +112,11 @@ class VMRowTests(TestCase):
self.proxy.get_console_keymap.return_value = ""
self.proxy.set_console_keymap.return_value = False # no graphics device
self.proxy.set_console_listener_addr.return_value = False # a socket, no address
self.post("update_console", {"listen_on": "0.0.0.0", "keymap": "de"})
form = {"listen_on": "0.0.0.0", "keymap": "de", "password_action": "keep"}
self.post("update_console", form)
self.proxy.set_console_keymap.return_value = None # the define's result
self.proxy.set_console_listener_addr.return_value = None
self.post("update_console", {"listen_on": "0.0.0.0", "keymap": "de"})
self.post("update_console", form)
self.assertEqual(self.rows(), [("VM vm: set console keymap de", True),
("VM vm: set console listen address 0.0.0.0", True)])
@ -126,7 +127,7 @@ class VMRowTests(TestCase):
self.proxy.get_console_passwd.return_value = ""
self.proxy.get_console_keymap.return_value = "de"
self.proxy.set_console_keymap.return_value = None
self.post("update_console", {"listen_on": "127.0.0.1", "keymap": "auto"})
self.post("update_console", {"listen_on": "127.0.0.1", "keymap": "auto", "password_action": "keep"})
self.proxy.set_console_keymap.assert_called_once_with("auto")
self.assertEqual(self.rows(), [("VM vm: remove console keymap", True)])
@ -135,10 +136,10 @@ class VMRowTests(TestCase):
self.proxy.get_console_type.return_value = "vnc"
self.proxy.get_console_listener_addr.return_value = "192.0.2.10"
self.proxy.get_console_keymap.return_value = "de"
self.proxy.get_console_passwd.return_value = "Vnc-pass-1"
self.proxy.get_console_passwd.return_value = "Vnc-pw-1"
form = {"listen_on": "192.0.2.10", "keymap": "de"}
self.post("update_console", {**form, "password": "Vnc-pass-1"})
self.post("update_console", {**form, "password": "Vnc-pass-2"})
self.post("update_console", {**form, "password_action": "keep"})
self.post("update_console", {**form, "password_action": "set", "password": "Vnc-pw-2"})
self.assertEqual(self.rows(), [("VM vm: set console password", True)])
self.proxy.set_console_listener_addr.assert_not_called()

View file

@ -247,7 +247,7 @@ def _instance_page(request, pk, instance, compute):
can_manage_console = utils.can_manage_console(request.user, instance)
console_form = None
if can_manage_console:
console_form = ConsoleForm(instance=instance, can_set_listener=request.user.is_superuser)
console_form = ConsoleForm(instance=instance, is_superuser=request.user.is_superuser)
console_listener_addresses = settings.QEMU_CONSOLE_LISTENER_ADDRESSES
ipv4_addresses = [address for net in instance.networks for address in net["ipv4"] or []]
try:
@ -2107,47 +2107,34 @@ def update_console(request, pk):
if not utils.can_manage_console(request.user, instance):
raise PermissionDenied
form = ConsoleForm(request.POST, instance=instance, can_set_listener=request.user.is_superuser)
if form.is_valid() and form.cleaned_data["console_type"] not in ("", instance.console_type):
with audit_vm(request, instance, f"set console type {form.cleaned_data['console_type']}"):
instance.proxy.set_console_type(form.cleaned_data["console_type"])
# the other fields are VNC settings; a new VNC console gets them too
if form.is_valid() and form.cleaned_data["console_type"] != "pty":
if (
"generate_password" in form.changed_data
or "clear_password" in form.changed_data
or "password" in form.changed_data
):
if form.cleaned_data["generate_password"]:
password = randomPasswd()
elif form.cleaned_data["clear_password"]:
password = ""
else:
form = ConsoleForm(request.POST, instance=instance, is_superuser=request.user.is_superuser)
if not form.is_valid():
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")
console_type = form.cleaned_data["console_type"]
if console_type not in ("", instance.config_console_type):
with audit_vm(request, instance, f"set console type {console_type}"):
instance.proxy.set_console_type(console_type)
# the VNC settings, if the form takes them
if "keymap" in form.fields:
action = form.cleaned_data["password_action"]
if action != "keep":
if action == "generate":
password = randomPasswd(8) # the length VNC allows
elif action == "set":
password = form.cleaned_data["password"]
# A console that listens beyond localhost (set by an
# administrator) keeps its password unless an administrator
# removes it. The definition being changed is the persistent one.
if (
not password
and not request.user.is_superuser
and not instance.proxy.console_listens_locally()
):
messages.error(
request,
_("The console listens beyond localhost; only an administrator can remove its password."),
)
else:
try:
with audit_vm(request, instance, "set console password" if password else "remove console password"):
if not instance.proxy.set_console_passwd(password):
raise util.OperationError("The VM has no graphics device")
except util.OperationError:
msg = _(
"Error setting console password. "
+ "You should check that your instance have an graphic device."
)
messages.error(request, msg)
password = ""
try:
with audit_vm(request, instance, "set console password" if password else "remove console password"):
if not instance.proxy.set_console_passwd(password):
raise util.OperationError("The VM has no graphics device")
except util.OperationError:
msg = _(
"Error setting console password. "
+ "You should check that your instance have an graphic device."
)
messages.error(request, msg)
if "keymap" in form.changed_data:
# "auto" removes the keymap: the client's keys go through as they are
@ -2159,8 +2146,6 @@ def update_console(request, pk):
if "listen_on" in form.changed_data:
with audit_vm(request, instance, f"set console listen address {form.cleaned_data['listen_on']}") as entry:
entry.skip = instance.proxy.set_console_listener_addr(form.cleaned_data["listen_on"]) is False
if not form.is_valid():
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"

View file

@ -352,7 +352,7 @@ class wvmCreate(wvmConnect):
xml += """</interface>"""
if console_pass == "random":
console_pass = "passwd='" + util.randomPasswd() + "'"
console_pass = "passwd='" + util.randomPasswd(8) + "'" # the length VNC allows
elif console_pass != "":
console_pass = "passwd='" + console_pass + "'"

View file

@ -1530,14 +1530,11 @@ class wvmInstance(wvmConnect):
socket = util.get_xml_path(self._XMLDesc(0), "/domain/devices/graphics/@socket")
return socket
def get_console_type(self):
console_type = util.get_xml_path(
self._XMLDesc(0), "/domain/devices/graphics/@type"
)
def get_console_type(self, config=False):
xml = self._XMLDesc(PERSISTENT_XML if config else 0)
console_type = util.get_xml_path(xml, "/domain/devices/graphics/@type")
if console_type is None:
console_type = util.get_xml_path(
self._XMLDesc(0), "/domain/devices/console/@type"
)
console_type = util.get_xml_path(xml, "/domain/devices/console/@type")
return console_type
def get_console_port(self, console_type=None):