mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
Its worker asked libvirt for the VM's state five times a second, up to 100 calls a second with 20 consoles open, only to notice a start or a stop. Once a second is enough: output and keys do not wait for it.
154 lines
5.9 KiB
Python
Executable file
154 lines
5.9 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 instances.models import Instance # noqa: E402
|
|
from console.tokens import ACCESS_CHECK, may_still_open # noqa: E402
|
|
from operations.utils import public_error_text # noqa: E402
|
|
from vrtManager.connection import connection_manager # noqa: E402
|
|
|
|
SEND_WAIT = 5 # seconds a paste may wait for the VM to take it
|
|
|
|
|
|
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 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").filter(pk=int(sys.argv[1])).first()
|
|
if instance is None: # deleted since socketiod accepted the token
|
|
say("The VM is no longer in the panel")
|
|
sys.exit(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:
|
|
# a VM's start or stop shows within a second; its output and the
|
|
# user's keys travel through libvirt's event loop, not this one
|
|
while console.running:
|
|
time.sleep(1)
|
|
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()
|