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:
parent
20cfc25367
commit
301b73155d
12 changed files with 248 additions and 126 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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("")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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", "")],
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 + "'"
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue