mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
When a VM stopped, its console's stream could end before the worker's next check saw the VM shut off. The check then found no stream to drop and said nothing, or, seeing the VM still running, opened the console again, which libvirt refused, and the session ended. The worker now remembers whether the VM ran at its last check, and a console that cannot be opened again because the VM stopped meanwhile is a stop: the terminal says the VM is not running and the console opens again when it starts. A new stream is the console's before its callback is registered, which may end it at once. The live serial test's VM now repeats its BIOS banner (it reboots right after finding no boot disk), so a console attached late still gets one, and the test waits for the superuser's console to show it before the VM stops. A run of the whole live suite over the socket missed the banner of the paused-then-resumed VM before.
165 lines
6.4 KiB
Python
Executable file
165 lines
6.4 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# consolecallback <instance id> <user id> - the serial console of one VM on stdin/stdout
|
|
# (socketiod runs it in a pty, one per client). It opens the console through
|
|
# the compute's connection (its stored credentials); the connection's event
|
|
# loop thread runs the stream callbacks. The console survives guest reboots.
|
|
# socketiod made the terminal raw before starting it (setting it here would
|
|
# drop what the user typed meanwhile).
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
import django
|
|
|
|
ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
|
|
if ROOT_PATH not in sys.path:
|
|
sys.path.append(ROOT_PATH)
|
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webvirtcloud.settings")
|
|
django.setup()
|
|
|
|
import libvirt # noqa: E402
|
|
from django import db # noqa: E402
|
|
from django.contrib.auth import get_user_model # noqa: E402
|
|
|
|
from instances.models import Instance # noqa: E402
|
|
from instances.utils import can_open_console # noqa: E402
|
|
from operations.utils import public_error_text # noqa: E402
|
|
from vrtManager.connection import connection_manager # noqa: E402
|
|
|
|
# libvirt prints its errors on stderr, which is the user's terminal: they are
|
|
# raised and told in one line instead
|
|
libvirt.registerErrorHandler(lambda context, error: None, None)
|
|
SEND_WAIT = 5 # seconds a paste may wait for the VM to take it
|
|
ACCESS_CHECK = 10 # seconds between checks that the user may still use the console
|
|
|
|
|
|
log = logging.getLogger("consolecallback")
|
|
|
|
|
|
def say(text):
|
|
os.write(1, ("\r\n[%s]\r\n" % text).encode())
|
|
|
|
|
|
class Console:
|
|
def __init__(self, connection, uuid):
|
|
self.domain = connection.lookupByUUIDString(uuid)
|
|
self.connection = connection
|
|
self.stream = None
|
|
self.active = False # the VM ran at the last check
|
|
self.running = True
|
|
|
|
def check(self):
|
|
"""Open the stream while the VM runs, drop it while it does not. The
|
|
stream may have ended first: the VM's stop is still reported."""
|
|
active = self.domain.state()[0] in (libvirt.VIR_DOMAIN_RUNNING, libvirt.VIR_DOMAIN_PAUSED)
|
|
if active and self.stream is None:
|
|
stream = self.connection.newStream(libvirt.VIR_STREAM_NONBLOCK)
|
|
try:
|
|
self.domain.openConsole(None, stream, 0) # a console in use is refused
|
|
except libvirt.libvirtError:
|
|
if self.domain.isActive():
|
|
raise
|
|
active = False # it stopped meanwhile
|
|
else:
|
|
self.stream = stream # first: the callback may end it at once
|
|
stream.eventAddCallback(libvirt.VIR_STREAM_EVENT_READABLE, from_vm, self)
|
|
if not active and self.active:
|
|
self.close_stream()
|
|
say("The VM is not running: its console opens again when it starts")
|
|
self.active = active
|
|
|
|
def close_stream(self):
|
|
stream, self.stream = self.stream, None
|
|
if stream is None: # the other thread closed it
|
|
return
|
|
try:
|
|
stream.eventRemoveCallback()
|
|
stream.abort()
|
|
except libvirt.libvirtError:
|
|
pass
|
|
|
|
|
|
def from_client(watch, fd, events, console):
|
|
try:
|
|
data = os.read(fd, 1024)
|
|
except OSError:
|
|
data = b""
|
|
if not data: # the client is gone
|
|
console.running = False
|
|
return
|
|
stream = console.stream # the main thread may drop it meanwhile
|
|
deadline = time.monotonic() + SEND_WAIT
|
|
try:
|
|
# a non-blocking stream takes part of a paste, or none (-2) while the VM is busy
|
|
while data and stream is not None and time.monotonic() < deadline:
|
|
sent = stream.send(data)
|
|
if sent <= 0: # -2: busy; 0 cannot advance either
|
|
time.sleep(0.01)
|
|
else:
|
|
data = data[sent:]
|
|
except libvirt.libvirtError:
|
|
console.close_stream()
|
|
|
|
|
|
def from_vm(stream, events, console):
|
|
try:
|
|
data = stream.recv(1024)
|
|
except libvirt.libvirtError: # the VM shut down
|
|
data = None
|
|
if not data or data == -2: # -2: nothing to read now
|
|
if data != -2:
|
|
console.close_stream()
|
|
return
|
|
os.write(1, data)
|
|
|
|
|
|
def may_still_open(user_id, instance):
|
|
"""The user still may open the console: an owner may have been removed or
|
|
the user deactivated since the session started."""
|
|
try:
|
|
user = get_user_model().objects.filter(pk=user_id).first()
|
|
return user is not None and can_open_console(user, instance)
|
|
finally:
|
|
db.connection.close() # a session may last for days: no idle connection meanwhile
|
|
|
|
|
|
def main():
|
|
# what the operator reads goes to socketiod's log (the journal, docker
|
|
# logs), on the descriptor it names; the user's terminal gets a short line
|
|
if os.environ.get("CONSOLECALLBACK_LOG_FD"):
|
|
log.addHandler(logging.StreamHandler(os.fdopen(int(os.environ["CONSOLECALLBACK_LOG_FD"]), "w", buffering=1)))
|
|
log.propagate = False
|
|
instance = Instance.objects.select_related("compute").get(pk=int(sys.argv[1]))
|
|
user_id = int(sys.argv[2])
|
|
c = instance.compute
|
|
try:
|
|
console = Console(connection_manager.get_connection(c.hostname, c.login, c.password, c.type), instance.uuid)
|
|
console.check()
|
|
except libvirt.libvirtError as err:
|
|
log.warning("serial console of VM %s did not open: %s", instance.name, err)
|
|
say("Cannot open the console of %s: %s" % (instance.name, public_error_text(err)))
|
|
sys.exit(1)
|
|
if console.stream is None:
|
|
say("The VM is not running: its console opens when it starts")
|
|
libvirt.virEventAddHandle(0, libvirt.VIR_EVENT_HANDLE_READABLE, from_client, console)
|
|
checked = time.monotonic()
|
|
try:
|
|
while console.running:
|
|
time.sleep(0.2)
|
|
console.check()
|
|
if time.monotonic() - checked > ACCESS_CHECK:
|
|
if not may_still_open(user_id, instance):
|
|
log.info("serial console of VM %s closed: its user may no longer open it", instance.name)
|
|
say("You may no longer open this console")
|
|
sys.exit(1)
|
|
checked = time.monotonic()
|
|
except libvirt.libvirtError as err: # the host went away, the VM was undefined or migrated
|
|
log.warning("serial console of VM %s closed: %s", instance.name, err)
|
|
say("The console of %s closed: %s" % (instance.name, public_error_text(err)))
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|