mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
Engine.IO accepts a connection, and holds one of socketiod's 40 threads for it, from its first request; the console token is checked only in Socket.IO's connect(), which a client need never send. Anyone who could reach the panel, without an account, could open 40 websockets, answer their pings, and no serial console would open for anybody (checked: the 41st handshake got no answer until they were closed). The daemon's WSGI app now refuses a request whose session cookie names no signed-in user, with 403, before Engine.IO sees it; the page always sends that cookie. A token without its session no longer connects either, as for novncd. Checked again: 60 such websockets are each answered 403 at once and hold nothing. The process tests sign their client in with a session cookie, as a browser; python-socketio sends a custom Cookie header only with its first request, so the client gets a cookie jar instead.
96 lines
4.9 KiB
Python
96 lines
4.9 KiB
Python
"""Live serial console: socketiod opens a running wvc-test VM's console on the
|
|
TCP host (TEST_LIBVIRT_HOST, with its password) and on the SSH host
|
|
(TEST_LIBVIRT_SSH_HOST). The VM has no graphics and no OS; SeaBIOS writes its
|
|
banner to the serial port, again on each reboot after it finds no boot disk.
|
|
Runs only with TEST_LIBVIRT_HOST set."""
|
|
|
|
import os
|
|
import time
|
|
import unittest
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.test import TransactionTestCase
|
|
from vrtManager.connection import connection_manager
|
|
|
|
from accounts.models import UserInstance
|
|
from computes.models import Compute
|
|
from console import tokens
|
|
from console.tests.test_socketiod_child import consoles, console, session_key, start_socketiod, socketio
|
|
from instances.models import Instance
|
|
from vrtManager.tests import livetest
|
|
|
|
P = livetest.PREFIX
|
|
|
|
|
|
def wait_for(condition, seconds=30):
|
|
deadline = time.monotonic() + seconds
|
|
while not condition() and time.monotonic() < deadline:
|
|
time.sleep(0.2)
|
|
|
|
|
|
def serial_vm(conn, name):
|
|
"""A running VM without graphics whose BIOS writes to the serial console in a loop:
|
|
a console opened late still gets the banner."""
|
|
dom = conn.defineXML(
|
|
f"<domain type='kvm'><name>{name}</name><memory unit='MiB'>64</memory><vcpu>1</vcpu>"
|
|
"<os><type arch='x86_64' machine='q35'>hvm</type><bios useserial='yes' rebootTimeout='0'/></os>"
|
|
"<devices><serial type='pty'><target port='0'/></serial><console type='pty'/></devices></domain>"
|
|
)
|
|
dom.create()
|
|
return dom
|
|
|
|
|
|
@unittest.skipIf(socketio is None, "python-socketio is not installed")
|
|
class LiveSerialConsoleTestCase(TransactionTestCase):
|
|
def setUp(self):
|
|
if not livetest.enabled():
|
|
self.skipTest("Set TEST_LIBVIRT_HOST to run the live libvirt tests")
|
|
self.user = get_user_model().objects.create_user("serial-live", password="pw")
|
|
self.hosts = [(os.environ["TEST_LIBVIRT_HOST"], os.environ.get("TEST_LIBVIRT_LOGIN", ""),
|
|
os.environ.get("TEST_LIBVIRT_PASSWORD", ""), int(os.environ.get("TEST_LIBVIRT_TYPE", 4)))]
|
|
if os.environ.get("TEST_LIBVIRT_SSH_HOST"):
|
|
self.hosts.append((os.environ["TEST_LIBVIRT_SSH_HOST"], os.environ.get("TEST_LIBVIRT_SSH_LOGIN", "root"), "", 2))
|
|
self.server, self.port = start_socketiod(self)
|
|
|
|
def token(self, host, login, password, conn_type):
|
|
compute = Compute.objects.create(name=f"{P}{host}-{conn_type}", hostname=host, login=login, password=password, type=conn_type)
|
|
conn = connection_manager.get_connection(host, login, password, conn_type)
|
|
before = livetest.inventory(conn)
|
|
self.addCleanup(lambda: self.assertEqual(livetest.inventory(conn), before))
|
|
self.addCleanup(livetest.cleanup, conn)
|
|
dom = serial_vm(conn, f"{P}serial-{conn_type}")
|
|
vm = Instance.objects.create(compute=compute, name=dom.name(), uuid=dom.UUIDString())
|
|
UserInstance.objects.create(user=self.user, instance=vm)
|
|
return tokens.sign(compute.id, vm.uuid, self.user.id, "pty"), dom
|
|
|
|
def admin_token(self, token):
|
|
host, uuid, _user, _type = tokens.SIGNER.unsign(token).split(":")
|
|
self.admin, _ = get_user_model().objects.get_or_create(username="serial-live-admin", is_superuser=True)
|
|
return tokens.sign(host, uuid, self.admin.id, "pty")
|
|
|
|
def test_the_console_of_a_running_vm_opens_once(self):
|
|
for args in self.hosts:
|
|
with self.subTest(host=args[0], type=args[3]):
|
|
token, dom = self.token(*args)
|
|
client, received = console(self.port, token, session_key(self.user))
|
|
deadline = time.monotonic() + 30
|
|
while "SeaBIOS" not in "".join(received["output"]) and time.monotonic() < deadline:
|
|
time.sleep(0.2)
|
|
self.assertIn("SeaBIOS", "".join(received["output"]))
|
|
# one client per VM; a superuser takes a console left open over
|
|
with self.assertRaises(socketio.exceptions.ConnectionError):
|
|
console(self.port, token, session_key(self.user))
|
|
admin_client, admin_received = console(self.port, self.admin_token(token), session_key(self.admin))
|
|
wait_for(lambda: received["taken_over"])
|
|
self.assertEqual(received["taken_over"], ["serial-live-admin"])
|
|
client.disconnect()
|
|
# the superuser's console is attached before the VM stops
|
|
wait_for(lambda: "SeaBIOS" in "".join(admin_received["output"]))
|
|
self.assertIn("SeaBIOS", "".join(admin_received["output"]))
|
|
# the VM stops: the terminal says so
|
|
dom.destroy()
|
|
wait_for(lambda: "not running" in "".join(admin_received["output"]))
|
|
self.assertIn("The VM is not running", "".join(admin_received["output"]))
|
|
admin_client.disconnect()
|
|
wait_for(lambda: not consoles(self.server.pid))
|
|
self.assertEqual(consoles(self.server.pid), [])
|