From 591429b0c9531aafc7cd786f77d069255e1b0866 Mon Sep 17 00:00:00 2001 From: catborise Date: Sat, 26 Sep 2026 12:10:35 +0300 Subject: [PATCH 001/765] feat(console): add comprehensive test suite and expand send keys menu - Add 23 unit and integration tests covering console views, authorization, URL/path normalization, and novncd daemon token extraction - Refactor console/novncd arg parsing into __main__ block for clean test imports - Expand Send Key(s) menu into categorized sections (System, Navigation/Desktop, Terminal Control, Virtual Consoles) - Implement non-destructive LIFO sendKeyCombo using public rfb.sendKey API, eliminating vendor debt - Wire Send Key actions across Lite, Full, and PTY (xterm) console templates - Add modern navigator.keyboard.lock support during fullscreen sessions --- console/novncd | 21 +- console/templates/console-base.html | 53 ++-- console/templates/console-vnc-full.html | 110 ++++++++ console/templates/console-vnc-lite.html | 158 ++++++++--- console/templates/console-xterm.html | 32 ++- console/tests.py | 354 +++++++++++++++++++++++- console/views.py | 31 ++- 7 files changed, 679 insertions(+), 80 deletions(-) diff --git a/console/novncd b/console/novncd index 402e3cdc..d16806fe 100755 --- a/console/novncd +++ b/console/novncd @@ -65,16 +65,6 @@ def get_parser(default_host=WS_HOST, default_port=WS_PORT or 6080, default_cert= return parser -options = get_parser().parse_args() - -FORMAT = "%(asctime)s - %(name)s - %(levelname)s : %(message)s" -if options.debug: - logging.basicConfig(level=logging.DEBUG, format=FORMAT) - options.verbose = True -elif options.verbose: - logging.basicConfig(level=logging.INFO, format=FORMAT) -else: - logging.basicConfig(level=logging.WARNING, format=FORMAT) try: from websockify import WebSocketProxy @@ -302,6 +292,17 @@ else: if __name__ == "__main__": + options = get_parser().parse_args() + + FORMAT = "%(asctime)s - %(name)s - %(levelname)s : %(message)s" + if options.debug: + logging.basicConfig(level=logging.DEBUG, format=FORMAT) + options.verbose = True + elif options.verbose: + logging.basicConfig(level=logging.INFO, format=FORMAT) + else: + logging.basicConfig(level=logging.WARNING, format=FORMAT) + if USE_HANDLER: # Create the WebSocketProxy with NovaProxyRequestHandler handler server = WebSocketProxy( diff --git a/console/templates/console-base.html b/console/templates/console-base.html index 11e38fd4..ecb8ed21 100644 --- a/console/templates/console-base.html +++ b/console/templates/console-base.html @@ -35,7 +35,7 @@
-
- - - -
- - -
-
- -
-
Connect
+
+
-
- -
-
-
-
    -
  • - - -
  • -
  • - - {% if perms.instances.passwordless_console %} - - {% else %} - - {% endif %} -
  • -
  • - -
  • -
-
+ +
+ + +
+
+ +
+ +
+
+
+ + +
+
+
+ {% trans 'Server identity' %} +
+
+ {% trans 'The server has provided the following identifying information:' %} +
+
+ {% trans 'Fingerprint:' %} + +
+
+ {% trans 'Please verify that the information is correct and press "Approve". Otherwise press "Reject".' %} +
+
+ + +
+
+
+ + +
+
+
+ {% trans 'Credentials' %} +
+
+ + +
+
+ + {% if perms.instances.passwordless_console %} + + {% else %} + + {% endif %} +
+
+ +
+
-
diff --git a/console/templates/console-vnc-lite.html b/console/templates/console-vnc-lite.html index 509e799a..d4d0c063 100755 --- a/console/templates/console-vnc-lite.html +++ b/console/templates/console-vnc-lite.html @@ -23,13 +23,6 @@ - - - - - - + + + + + + +
+
+
noVNC encountered an error:
+
+
+
+
+ + +
+ +
+
+ +
+ +

no
VNC

+ +
+ + + + + +
+ +
+ + + +
+
+ + + + + + +
+
+ + + +
+
+
+ Power +
+ + + +
+
+ + + +
+
+
+ Clipboard +
+

+ Edit clipboard content in the textarea below. +

+ +
+
+ + + + + + +
+
+
+ Settings +
+
    +
  • + +
  • +
  • + +
  • +

  • +
  • + +
  • +
  • + + +
  • +

  • +
  • +
    Advanced
    +
      +
    • + + +
    • +
    • + + +
    • +

    • +
    • + + +
    • +
    • +
      WebSocket
      +
        +
      • + +
      • +
      • + + +
      • +
      • + + +
      • +
      • + + +
      • +
      +
    • +

    • +
    • + +
    • +
    • + + +
    • +

    • +
    • + +
    • +

    • + +
    • + +
    • +
    +
  • +

  • +
  • + Version: + +
  • +
+
+
+ + + + +
+
+ +
+ +
+
+
+
+ + +
+ + +
+
+ +
+ +
+
+
+ + +
+
+
+ Server identity +
+
+ The server has provided the following identifying information: +
+
+ Fingerprint: + +
+
+ Please verify that the information is correct and press + "Approve". Otherwise press "Reject". +
+
+ + +
+
+
+ + +
+
+
+ Credentials +
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
+
+ +
+
+
+ + +
+ + +
+ + + + diff --git a/static/js/novnc/vnc_lite.html b/static/js/novnc/vnc_lite.html new file mode 100644 index 00000000..79d48146 --- /dev/null +++ b/static/js/novnc/vnc_lite.html @@ -0,0 +1,180 @@ + + + + + + noVNC + + + + + + + +
+
Loading
+
Send CtrlAltDel
+
+
+ +
+ + From f19c30840eb42326c7db3c896f63442df7a31fb6 Mon Sep 17 00:00:00 2001 From: catborise Date: Sat, 26 Sep 2026 19:28:22 +0300 Subject: [PATCH 006/765] fix(console): prevent DOM-based XSS in console views and escape JS strings - Validate and coerce console query parameters (view_only, scale, resize_session, clip_viewport) to strict booleans via to_bool in console/views.py - Sanitize view parameter to strictly 'lite' or 'full' - Apply escapejs filter to all JS string interpolations in console-vnc-full.html, console-vnc-lite.html, console-spice-lite.html, and console-xterm.html - Replace unsafe '| safe' filter in console-spice-lite.html with escapejs - Add unit tests for XSS query param mitigation and escapejs character encoding --- console/templates/console-spice-lite.html | 11 +++-- console/templates/console-vnc-full.html | 14 +++---- console/templates/console-vnc-lite.html | 20 ++++----- console/templates/console-xterm.html | 6 +-- console/tests.py | 50 +++++++++++++++++++++++ console/views.py | 36 ++++++++++++---- 6 files changed, 102 insertions(+), 35 deletions(-) diff --git a/console/templates/console-spice-lite.html b/console/templates/console-spice-lite.html index 469c9715..d6ddf3df 100644 --- a/console/templates/console-spice-lite.html +++ b/console/templates/console-spice-lite.html @@ -65,7 +65,7 @@ // By default, use the host and port of server that served this file // host = spice_query_var('host', window.location.hostname); - host = '{{ ws_host| safe }}'; + host = '{{ ws_host|escapejs }}'; // Note that using the web server port only makes sense // if your web server has a reverse proxy to relay the WebSocket @@ -80,7 +80,7 @@ } } //port = spice_query_var('port', default_port); - port = '{{ ws_port| safe }}'; + port = '{{ ws_port|escapejs }}'; if (window.location.protocol == 'https:') { scheme = "wss://"; } @@ -93,17 +93,16 @@ } if (password === undefined) { - {% if perms.instances.passwordless_console %} - password = '{{ console_passwd }}'; + {% if perms.instances.passwordless_console and console_passwd %} + password = '{{ console_passwd|escapejs }}'; {% else %} password = prompt('{% trans "Password" %}'); {% endif %} - //password = '{{ console_passwd | safe }}'; } if (password === 'None') password = ''; //var path = spice_query_var('path', 'websockify'); - var path = spice_query_var('path', '{{ ws_path }}'); + var path = spice_query_var('path', '{{ ws_path|escapejs }}'); if ((!host) || (!port)) { console.log('{% trans "must specify host and port in URL" %}'); diff --git a/console/templates/console-vnc-full.html b/console/templates/console-vnc-full.html index 714ab036..aedeb2b5 100755 --- a/console/templates/console-vnc-full.html +++ b/console/templates/console-vnc-full.html @@ -176,24 +176,24 @@ bindKeyAction('ctrlaltf' + (i + 1), () => sendCtrlAltFN(i)); } - let fullPath = '{{ ws_path }}'; - const token = '{{ token }}'; + let fullPath = '{{ ws_path|escapejs }}'; + const token = '{{ token|escapejs }}'; if (token) { fullPath += (fullPath.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(token); } const defaults = { autoconnect: true, - resize: '{{ scale }}' === 'true' ? 'scale' : 'off', - view_only: {{ view_only }}, - view_clip: {{ clip_viewport }}, + resize: {{ scale|yesno:"'scale','off'" }}, + view_only: {{ view_only|yesno:"true,false" }}, + view_clip: {{ clip_viewport|yesno:"true,false" }}, {% if perms.instances.passwordless_console and console_passwd %} - password: '{{ console_passwd }}', + password: '{{ console_passwd|escapejs }}', {% endif %} }; const mandatory = { - host: '{{ ws_host }}', + host: '{{ ws_host|escapejs }}', port: {{ ws_port }}, path: fullPath, }; diff --git a/console/templates/console-vnc-lite.html b/console/templates/console-vnc-lite.html index d4d0c063..0fbeb745 100755 --- a/console/templates/console-vnc-lite.html +++ b/console/templates/console-vnc-lite.html @@ -260,17 +260,17 @@ // Read parameters specified in the URL query string // By default, use the host and port of server that served this file - const host = readQueryVariable('host', '{{ ws_host }}'); - let port = readQueryVariable('port', '{{ ws_port }}'); + const host = readQueryVariable('host', '{{ ws_host|escapejs }}'); + let port = readQueryVariable('port', '{{ ws_port|escapejs }}'); - {% if perms.instances.passwordless_console %} - const password = '{{ console_passwd }}'; + {% if perms.instances.passwordless_console and console_passwd %} + const password = '{{ console_passwd|escapejs }}'; {% else %} const password = readQueryVariable('password'); {% endif %} //const path = readQueryVariable('path', 'websockify'); - const path = readQueryVariable('path', '{{ ws_path }}'); + const path = readQueryVariable('path', '{{ ws_path|escapejs }}'); // | | | | | | // | | | Connect | | | @@ -294,7 +294,7 @@ } else { url += '/'; } - const token = readQueryVariable('token', '{{ token }}'); + const token = readQueryVariable('token', '{{ token|escapejs }}'); if (token) { url += (url.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(token); } @@ -310,10 +310,10 @@ rfb.addEventListener("desktopname", updateDesktopName); // Set parameters that can be changed on an active connection - rfb.scaleViewport = {{ scale }}; - rfb.viewOnly = {{ view_only }}; - rfb.resizeSession = {{ resize_session }}; - rfb.clipViewport = {{ clip_viewport }}; + rfb.scaleViewport = {{ scale|yesno:"true,false" }}; + rfb.viewOnly = {{ view_only|yesno:"true,false" }}; + rfb.resizeSession = {{ resize_session|yesno:"true,false" }}; + rfb.clipViewport = {{ clip_viewport|yesno:"true,false" }}; {% endblock %} diff --git a/console/templates/console-xterm.html b/console/templates/console-xterm.html index 9fd1a853..3bae98f1 100644 --- a/console/templates/console-xterm.html +++ b/console/templates/console-xterm.html @@ -22,9 +22,9 @@ ")[0] + for s in content_full.split(" 'off' + url = reverse("console") + f"?token={self.token}&view=full" + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + content = resp.content.decode("utf-8") + self.assertIn("resize: 'off',", content) + self.assertNotIn("'off'", content) + self.assertNotIn("'off'", content) + + # scale=True, resize_session=False -> 'scale' + url_scale = reverse("console") + f"?token={self.token}&view=full&scale=true" + resp_scale = self.client.get(url_scale) + self.assertEqual(resp_scale.status_code, 200) + content_scale = resp_scale.content.decode("utf-8") + self.assertIn("resize: 'scale',", content_scale) + + # resize_session=True -> 'remote' + url_remote = reverse("console") + f"?token={self.token}&view=full&resize_session=true" + resp_remote = self.client.get(url_remote) + self.assertEqual(resp_remote.status_code, 200) + content_remote = resp_remote.content.decode("utf-8") + self.assertIn("resize: 'remote',", content_remote) + @patch("console.views.wvmInstance") def test_console_libvirt_error_fallback(self, mock_wvm): From 4b55630b2d489411cf8cebbaaf13064a17a0ff30 Mon Sep 17 00:00:00 2001 From: catborise Date: Sat, 26 Sep 2026 21:45:54 +0300 Subject: [PATCH 008/765] style(console): fix flake8 E303 blank lines and line length in console tests --- console/tests.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/console/tests.py b/console/tests.py index 4077c38e..8106daa6 100644 --- a/console/tests.py +++ b/console/tests.py @@ -247,7 +247,8 @@ class ConsoleViewsTestCase(TestCase): # Attempt XSS injection via query parameters url = ( reverse("console") - + f"?token={self.token}&view=lite&view_only=alert('xss')&scale=true&clip_viewport=1" + + f"?token={self.token}" + + "&view=lite&view_only=alert('xss')&scale=true&clip_viewport=1" ) response = self.client.get(url) @@ -320,13 +321,14 @@ class ConsoleViewsTestCase(TestCase): self.assertIn("resize: 'scale',", content_scale) # resize_session=True -> 'remote' - url_remote = reverse("console") + f"?token={self.token}&view=full&resize_session=true" + url_remote = ( + reverse("console") + f"?token={self.token}&view=full&resize_session=true" + ) resp_remote = self.client.get(url_remote) self.assertEqual(resp_remote.status_code, 200) content_remote = resp_remote.content.decode("utf-8") self.assertIn("resize: 'remote',", content_remote) - @patch("console.views.wvmInstance") def test_console_libvirt_error_fallback(self, mock_wvm): mock_wvm.side_effect = libvirtError("Connection to libvirtd failed") From e980aca2ffc0977342727776333b6ff57575d851 Mon Sep 17 00:00:00 2001 From: catborise Date: Sat, 26 Sep 2026 22:34:13 +0300 Subject: [PATCH 009/765] feat(console): remove deprecated SPICE support in favor of noVNC - Remove obsolete spice-html5 static assets and SPICE console templates - Restrict default console types to VNC with migration 0012 - Filter out SPICE in VM creation form and backend capabilities - Return friendly deprecation notice if legacy VM is opened with SPICE - Clean up docs, architecture references, and libvirt-bootstrap scripts --- .gitignore | 1 + README.md | 6 +- .../0012_remove_spice_qemu_console_type.py | 38 + conf/test-vm.xml | 2 +- console/templates/console-spice-full.html | 231 - console/templates/console-spice-lite.html | 231 - console/tests.py | 19 +- console/views.py | 9 +- doc/architecture.md | 6 +- instances/templates/create_instance_w2.html | 6 + static/js/spice-html5/atKeynames.js | 189 - static/js/spice-html5/bitmap.js | 64 - static/js/spice-html5/cursor.js | 141 - static/js/spice-html5/display.js | 1272 - static/js/spice-html5/enums.js | 372 - static/js/spice-html5/filexfer.js | 94 - static/js/spice-html5/inputs.js | 335 - static/js/spice-html5/lz.js | 188 - static/js/spice-html5/main.js | 520 - static/js/spice-html5/playback.js | 409 - static/js/spice-html5/png.js | 262 - static/js/spice-html5/port.js | 94 - static/js/spice-html5/quic.js | 1345 - static/js/spice-html5/resize.js | 90 - static/js/spice-html5/simulatecursor.js | 208 - static/js/spice-html5/spice.css | 115 - static/js/spice-html5/spicearraybuffer.js | 58 - static/js/spice-html5/spiceconn.js | 526 - static/js/spice-html5/spicedataview.js | 124 - static/js/spice-html5/spicemsg.js | 1371 - static/js/spice-html5/spicetype.js | 497 - .../browser-es-module-loader/.npmignore | 0 .../browser-es-module-loader/README.md | 15 - .../dist/babel-worker.js | 55799 ---------------- .../dist/browser-es-module-loader.js | 1486 - .../dist/browser-es-module-loader.js.map | 1 - .../browser-es-module-loader/rollup.config.js | 16 - .../src/babel-worker.js | 25 - .../src/browser-es-module-loader.js | 280 - static/js/spice-html5/thirdparty/jsbn.js | 593 - static/js/spice-html5/thirdparty/prng4.js | 84 - static/js/spice-html5/thirdparty/rng.js | 107 - static/js/spice-html5/thirdparty/rsa.js | 153 - static/js/spice-html5/thirdparty/sha1.js | 351 - static/js/spice-html5/ticket.js | 260 - static/js/spice-html5/utils.js | 360 - static/js/spice-html5/webm.js | 673 - static/js/spice-html5/wire.js | 131 - 48 files changed, 62 insertions(+), 69095 deletions(-) create mode 100644 appsettings/migrations/0012_remove_spice_qemu_console_type.py delete mode 100644 console/templates/console-spice-full.html delete mode 100644 console/templates/console-spice-lite.html delete mode 100644 static/js/spice-html5/atKeynames.js delete mode 100644 static/js/spice-html5/bitmap.js delete mode 100644 static/js/spice-html5/cursor.js delete mode 100644 static/js/spice-html5/display.js delete mode 100644 static/js/spice-html5/enums.js delete mode 100644 static/js/spice-html5/filexfer.js delete mode 100644 static/js/spice-html5/inputs.js delete mode 100644 static/js/spice-html5/lz.js delete mode 100644 static/js/spice-html5/main.js delete mode 100644 static/js/spice-html5/playback.js delete mode 100644 static/js/spice-html5/png.js delete mode 100644 static/js/spice-html5/port.js delete mode 100644 static/js/spice-html5/quic.js delete mode 100644 static/js/spice-html5/resize.js delete mode 100644 static/js/spice-html5/simulatecursor.js delete mode 100755 static/js/spice-html5/spice.css delete mode 100644 static/js/spice-html5/spicearraybuffer.js delete mode 100644 static/js/spice-html5/spiceconn.js delete mode 100644 static/js/spice-html5/spicedataview.js delete mode 100644 static/js/spice-html5/spicemsg.js delete mode 100644 static/js/spice-html5/spicetype.js delete mode 100644 static/js/spice-html5/thirdparty/browser-es-module-loader/.npmignore delete mode 100644 static/js/spice-html5/thirdparty/browser-es-module-loader/README.md delete mode 100644 static/js/spice-html5/thirdparty/browser-es-module-loader/dist/babel-worker.js delete mode 100644 static/js/spice-html5/thirdparty/browser-es-module-loader/dist/browser-es-module-loader.js delete mode 100644 static/js/spice-html5/thirdparty/browser-es-module-loader/dist/browser-es-module-loader.js.map delete mode 100644 static/js/spice-html5/thirdparty/browser-es-module-loader/rollup.config.js delete mode 100644 static/js/spice-html5/thirdparty/browser-es-module-loader/src/babel-worker.js delete mode 100644 static/js/spice-html5/thirdparty/browser-es-module-loader/src/browser-es-module-loader.js delete mode 100644 static/js/spice-html5/thirdparty/jsbn.js delete mode 100644 static/js/spice-html5/thirdparty/prng4.js delete mode 100644 static/js/spice-html5/thirdparty/rng.js delete mode 100644 static/js/spice-html5/thirdparty/rsa.js delete mode 100644 static/js/spice-html5/thirdparty/sha1.js delete mode 100644 static/js/spice-html5/ticket.js delete mode 100644 static/js/spice-html5/utils.js delete mode 100644 static/js/spice-html5/webm.js delete mode 100644 static/js/spice-html5/wire.js diff --git a/.gitignore b/.gitignore index bc289ab1..935ec575 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ htmlcov *.log templates/webvirtcloud.code-workspace DEPENDENCY_UPGRADE_PLAN.md +.agents diff --git a/README.md b/README.md index 22c81c92..99992ae8 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ## Description -WebVirtCloud is a virtualization web interface for administrators and users. It allows delegating virtual machines to users with role-based permissions. A built-in noVNC / SPICE console presents a full graphical interface to the guest domain. KVM is currently the supported hypervisor. +WebVirtCloud is a virtualization web interface for administrators and users. It allows delegating virtual machines to users with role-based permissions. A built-in noVNC console presents a full graphical interface to the guest domain. KVM is currently the supported hypervisor. ## Features * QEMU/KVM Hypervisor Management @@ -14,7 +14,7 @@ WebVirtCloud is a virtualization web interface for administrators and users. It * Manage Multiple QEMU/KVM Hypervisors * Manage Hypervisor Datastore pools and storage volumes * Manage Hypervisor Networks and interfaces -* Instance Console Access with Web Browsers (noVNC & SPICE) +* Instance Console Access with Web Browsers (noVNC & xterm.js) * Libvirt API-based web management UI * User-based Authorization, Authentication, and 2FA (OTP) * User can add SSH public key to root in Instance @@ -322,7 +322,7 @@ sudo zypper install -y dmidecode && sudo systemctl restart libvirtd ``` > **Security Notice (Compute Node Firewall):** -> Libvirt compute nodes listen on VNC/SPICE ports (`5900`–`65535`) to allow WebVirtCloud to proxy graphical consoles. Ensure your firewall (`ufw`, `firewalld`, or `iptables`) restricts these ports to accept connections **only** from the WebVirtCloud panel IP, and never exposes them directly to public networks. +> Libvirt compute nodes listen on VNC ports (`5900`–`65535`) to allow WebVirtCloud to proxy graphical consoles. Ensure your firewall (`ufw`, `firewalld`, or `iptables`) restricts these ports to accept connections **only** from the WebVirtCloud panel IP, and never exposes them directly to public networks. --- diff --git a/appsettings/migrations/0012_remove_spice_qemu_console_type.py b/appsettings/migrations/0012_remove_spice_qemu_console_type.py new file mode 100644 index 00000000..2c9f356a --- /dev/null +++ b/appsettings/migrations/0012_remove_spice_qemu_console_type.py @@ -0,0 +1,38 @@ +from django.db import migrations + + +def update_console_type(apps, schema_editor): + AppSettings = apps.get_model("appsettings", "AppSettings") + db_alias = schema_editor.connection.alias + setting = ( + AppSettings.objects.using(db_alias) + .filter(key="QEMU_CONSOLE_DEFAULT_TYPE") + .first() + ) + if setting: + setting.value = "vnc" + setting.choices = "vnc" + setting.save(using=db_alias) + + +def reverse_console_type(apps, schema_editor): + AppSettings = apps.get_model("appsettings", "AppSettings") + db_alias = schema_editor.connection.alias + setting = ( + AppSettings.objects.using(db_alias) + .filter(key="QEMU_CONSOLE_DEFAULT_TYPE") + .first() + ) + if setting: + setting.choices = "vnc,spice" + setting.save(using=db_alias) + + +class Migration(migrations.Migration): + dependencies = [ + ("appsettings", "0011_alter_appsettings_id"), + ] + + operations = [ + migrations.RunPython(update_console_type, reverse_console_type), + ] diff --git a/conf/test-vm.xml b/conf/test-vm.xml index d14f25f9..9750668d 100644 --- a/conf/test-vm.xml +++ b/conf/test-vm.xml @@ -105,7 +105,7 @@ - +
diff --git a/virtsecrets/tests/__init__.py b/virtsecrets/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/virtsecrets/tests/test_create_secret.py b/virtsecrets/tests/test_create_secret.py new file mode 100644 index 00000000..7ed716db --- /dev/null +++ b/virtsecrets/tests/test_create_secret.py @@ -0,0 +1,28 @@ +"""A secret's usage value goes into its XML as text, whatever it holds: a +volume path, an iSCSI IQN or a Ceph name, and characters XML gives a meaning.""" + +from unittest.mock import patch + +from django.test import SimpleTestCase +from lxml import etree +from vrtManager.virtsecrets import wvmSecrets + + +class CreateSecretTests(SimpleTestCase): + def defined_xml(self, usage_type, data): + with patch("vrtManager.connection.connection_manager.get_connection") as get: + wvmSecrets("host", "login", "pw", 1).create_secret("no", "yes", usage_type, data) + return etree.fromstring(get.return_value.secretDefineXML.call_args.args[0]) + + def test_the_usage_value_is_text_of_its_element(self): + for usage_type, element, data in ( + ("volume", "volume", "/var/lib/libvirt/images/vm-1.qcow2"), + ("iscsi", "target", "iqn.2026-10.test.wvc:lun"), + ("ceph", "name", "client.libvirt secret"), + ("volume", "volume", "/a'b&d\""), + ): + with self.subTest(usage_type=usage_type, data=data): + secret = self.defined_xml(usage_type, data) + self.assertEqual((secret.get("ephemeral"), secret.get("private")), ("no", "yes")) + self.assertEqual(secret.find("usage").get("type"), usage_type) + self.assertEqual(secret.findtext(f"usage/{element}"), data) diff --git a/virtsecrets/tests/test_live_secrets.py b/virtsecrets/tests/test_live_secrets.py new file mode 100644 index 00000000..f55bc7f4 --- /dev/null +++ b/virtsecrets/tests/test_live_secrets.py @@ -0,0 +1,52 @@ +"""Live check that the secrets page creates a secret for each usage type +with a real usage value: a volume path, an iSCSI IQN and a Ceph name. Runs +only with TEST_LIBVIRT_HOST set.""" + +import os +import unittest + +import libvirt +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.tests import livetest +from vrtManager.connection import connection_manager + +P = livetest.PREFIX +USAGES = { + libvirt.VIR_SECRET_USAGE_TYPE_VOLUME: ("volume", f"/var/lib/libvirt/{P}secrets/vm-1.qcow2"), + libvirt.VIR_SECRET_USAGE_TYPE_ISCSI: ("iscsi", f"iqn.2026-10.test.wvc:{P}secret"), + libvirt.VIR_SECRET_USAGE_TYPE_CEPH: ("ceph", f"client.{P}secret secret"), +} + + +@unittest.skipUnless(livetest.enabled(), "Set TEST_LIBVIRT_HOST to run the live libvirt tests") +class LiveSecretsTestCase(TestCase): + def setUp(self): + args = dict( + hostname=os.environ["TEST_LIBVIRT_HOST"], + login=os.environ.get("TEST_LIBVIRT_LOGIN", ""), + password=os.environ.get("TEST_LIBVIRT_PASSWORD", ""), + type=int(os.environ.get("TEST_LIBVIRT_TYPE", 4)), + ) + self.conn = connection_manager.get_connection(*args.values()) + self.remove_secrets() + self.addCleanup(self.remove_secrets) + self.compute = Compute.objects.create(name="live-secrets", **args) + self.client.force_login(get_user_model().objects.create_superuser("live-secrets", "ls@example.com", "x")) + + def remove_secrets(self): + for secret in self.conn.listAllSecrets(): + if (secret.usageType(), secret.usageID()) in {(t, v) for t, (_, v) in USAGES.items()}: + secret.undefine() + + def test_each_usage_type_takes_a_real_value(self): + for usage_type, (name, value) in USAGES.items(): + with self.subTest(name): + response = self.client.post( + reverse("virtsecrets", args=[self.compute.id]), + {"create": "", "ephemeral": "no", "private": "no", "usage_type": name, "data": value}, + ) + self.assertEqual(response.status_code, 302) + self.assertEqual(self.conn.secretLookupByUsage(usage_type, value).usageID(), value) diff --git a/vrtManager/virtsecrets.py b/vrtManager/virtsecrets.py index c8772823..1117e7e5 100644 --- a/vrtManager/virtsecrets.py +++ b/vrtManager/virtsecrets.py @@ -1,21 +1,19 @@ import base64 +from lxml import etree from vrtManager.connection import wvmConnect +# the element of that holds the value, per usage type +USAGE_ELEMENTS = {"ceph": "name", "volume": "volume", "iscsi": "target"} + class wvmSecrets(wvmConnect): def create_secret(self, ephemeral, private, secret_type, data): - xml = f""" - """ - if secret_type == "ceph": - xml += f"""{data}""" - if secret_type == "volume": - xml += f"""{data}""" - if secret_type == "iscsi": - xml += f"""{data}""" - xml += """ - """ - self.wvm.secretDefineXML(xml) + # built with lxml: data is text, whatever characters it holds + secret = etree.Element("secret", ephemeral=ephemeral, private=private) + usage = etree.SubElement(secret, "usage", type=secret_type) + etree.SubElement(usage, USAGE_ELEMENTS[secret_type]).text = data + self.wvm.secretDefineXML(etree.tostring(secret, encoding="unicode")) def get_secret_value(self, uuid): secrt = self.get_secret(uuid) From ca74c02c4fcccadd0c72df9cd83f07d97e88e888 Mon Sep 17 00:00:00 2001 From: catborise Date: Mon, 19 Oct 2026 16:50:58 +0300 Subject: [PATCH 314/765] fix(templates): the fields of the create modals are named by their labels An accessibility scan with every modal opened found 49 fields without a name: the labels of the instance create forms (custom, template and each flavor's modal), the interface, network, secret and storage pool modals did not point at their fields. Each label now has a for= and its field an id, prefixed per form (and per flavor), so a click on the label focuses the field too. The "Advanced" collapse buttons and the memory and processing bars are named. The VCPU config select had id="vcpu_mode" in each of these forms, nine times on the create page; the unique-ID test did not see it as the test driver reports no CPU modes. The id goes (nothing used it), and the test now renders that branch. --- instances/templates/create_inst_block.html | 4 +- instances/templates/create_instance_w2.html | 192 +++++++++--------- interfaces/templates/create_iface_block.html | 42 ++-- networks/templates/create_net_block.html | 34 ++-- storages/templates/create_stg_block.html | 78 +++---- templates/pleasewaitdialog.html | 2 +- .../templates/create_secret_block.html | 16 +- webvirtcloud/tests/test_unique_ids.py | 1 + 8 files changed, 185 insertions(+), 184 deletions(-) diff --git a/instances/templates/create_inst_block.html b/instances/templates/create_inst_block.html index fcb51a83..555fc4a1 100644 --- a/instances/templates/create_inst_block.html +++ b/instances/templates/create_inst_block.html @@ -34,7 +34,7 @@ {{ compute.cpu_count }}
-
@@ -43,7 +43,7 @@ {{ compute.ram_size|filesizeformat }}
-
{{ compute.ram_usage }}%
diff --git a/instances/templates/create_instance_w2.html b/instances/templates/create_instance_w2.html index 1c481742..b5f2ee09 100644 --- a/instances/templates/create_instance_w2.html +++ b/instances/templates/create_instance_w2.html @@ -104,9 +104,9 @@ {% csrf_token %}{% submission_key %}