diff --git a/instances/forms.py b/instances/forms.py index 010a26e3..5e8f5a40 100644 --- a/instances/forms.py +++ b/instances/forms.py @@ -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): diff --git a/instances/models.py b/instances/models.py index a9aede16..d48d8a72 100644 --- a/instances/models.py +++ b/instances/models.py @@ -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) diff --git a/instances/templates/instances/settings_tab.html b/instances/templates/instances/settings_tab.html index 0655902d..08753a2e 100644 --- a/instances/templates/instances/settings_tab.html +++ b/instances/templates/instances/settings_tab.html @@ -630,23 +630,64 @@ {% endif %} {% if can_manage_console %}
-
- {% csrf_token %} + {% csrf_token %} {% if instance.status != 5 %} -
- {% trans "To change console settings, shutdown the instance." %} -
+

{% trans "The running console keeps its current settings; changes here apply after the VM is shut down and started." %}

{% endif %} - {% bootstrap_form console_form layout='horizontal' %} -
- {% if instance.status != 5 %} - +
+

{% trans "Console" %}

+ + {{ console_form.console_type }} + {% if console_form.keymap %} +

{% trans "The settings below apply to a VNC console." %}

{% else %} - +

{% trans "The VNC settings appear once the console type VNC is saved." %}

{% endif %} +
+ {% if console_form.keymap %} +
+
+
+
+

{% bs_icon 'display' %} {% trans "VNC connection" %}

+
+ + {{ console_form.listen_on }} +

+ {% if not request.user.is_superuser %}{% trans "Only administrators can change this." %} {% endif %}{% trans "Beyond localhost the console is reachable from the network." %} +

+
+ + {{ console_form.keymap }} +
+
+
+
+
+
+

{% bs_icon 'key' %} {% trans "VNC password" %}

+
+ {{ console_form.password_action.label }} +

{% if instance.config_console_passwd %}{% trans "A password is set." %}{% else %}{% trans "No password is set." %}{% endif %}

+ {% for choice in console_form.password_action %} +
+ {{ choice.tag }} + +
+ {% endfor %} +
+ + {{ console_form.password }} +

{% trans "Up to 8 characters (an accented letter counts as two), used with \"Set a new one\"." %}

+
+
+
+
+ {% endif %} +
+
-
{% endif %} {% if can_clone %} diff --git a/instances/tests/test_console_listener.py b/instances/tests/test_console_listener.py index 87fe9ca3..c6d37344 100644 --- a/instances/tests/test_console_listener.py +++ b/instances/tests/test_console_listener.py @@ -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("") diff --git a/instances/tests/test_console_settings_access.py b/instances/tests/test_console_settings_access.py index 30e9927b..885ac086 100644 --- a/instances/tests/test_console_settings_access.py +++ b/instances/tests/test_console_settings_access.py @@ -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) diff --git a/instances/tests/test_pending_edits.py b/instances/tests/test_pending_edits.py index 1d978413..21632ec3 100644 --- a/instances/tests/test_pending_edits.py +++ b/instances/tests/test_pending_edits.py @@ -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", "")], diff --git a/instances/tests/test_update_console.py b/instances/tests/test_update_console.py index 9e626d7b..ea58dfdf 100644 --- a/instances/tests/test_update_console.py +++ b/instances/tests/test_update_console.py @@ -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() diff --git a/instances/tests/test_views.py b/instances/tests/test_views.py index 1c48a3e2..2a920537 100644 --- a/instances/tests/test_views.py +++ b/instances/tests/test_views.py @@ -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) diff --git a/instances/tests/test_vm_audit_rows.py b/instances/tests/test_vm_audit_rows.py index 2ddf8b58..6491d147 100644 --- a/instances/tests/test_vm_audit_rows.py +++ b/instances/tests/test_vm_audit_rows.py @@ -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() diff --git a/instances/views.py b/instances/views.py index 1aa58d46..117bfdcb 100755 --- a/instances/views.py +++ b/instances/views.py @@ -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" diff --git a/vrtManager/create.py b/vrtManager/create.py index 1e37580d..72e371d9 100644 --- a/vrtManager/create.py +++ b/vrtManager/create.py @@ -352,7 +352,7 @@ class wvmCreate(wvmConnect): xml += """""" 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 + "'" diff --git a/vrtManager/instance.py b/vrtManager/instance.py index 54c7e499..046f4c07 100644 --- a/vrtManager/instance.py +++ b/vrtManager/instance.py @@ -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):