Skip to content

Commit

Permalink
Implement Libvirt Serial Console as Console on WebVirtCloud
Browse files Browse the repository at this point in the history
  • Loading branch information
GaetanF committed Jul 17, 2022
1 parent f6915ac commit 65dce10
Show file tree
Hide file tree
Showing 10 changed files with 398 additions and 2 deletions.
100 changes: 100 additions & 0 deletions conf/daemon/consolecallback
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
# consolecallback - provide a persistent console that survives guest reboots

import os
import logging
import libvirt
import tty
import termios
import atexit
from argparse import ArgumentParser
from typing import Optional # noqa F401


def reset_term() -> None:
termios.tcsetattr(0, termios.TCSADRAIN, attrs)


def error_handler(unused, error) -> None:
# The console stream errors on VM shutdown; we don't care
if error[0] == libvirt.VIR_ERR_RPC and error[1] == libvirt.VIR_FROM_STREAMS:
return
logging.warn(error)


class Console(object):
def __init__(self, uri: str, uuid: str) -> None:
self.uri = uri
self.uuid = uuid
self.connection = libvirt.open(uri)
self.domain = self.connection.lookupByUUIDString(uuid)
self.state = self.domain.state(0)
self.connection.domainEventRegister(lifecycle_callback, self)
self.stream = None # type: Optional[libvirt.virStream]
self.run_console = True
self.stdin_watch = -1
logging.info("%s initial state %d, reason %d",
self.uuid, self.state[0], self.state[1])


def check_console(console: Console) -> bool:
if (console.state[0] == libvirt.VIR_DOMAIN_RUNNING or console.state[0] == libvirt.VIR_DOMAIN_PAUSED):
if console.stream is None:
console.stream = console.connection.newStream(libvirt.VIR_STREAM_NONBLOCK)
console.domain.openConsole(None, console.stream, 0)
console.stream.eventAddCallback(libvirt.VIR_STREAM_EVENT_READABLE, stream_callback, console)
else:
if console.stream:
console.stream.eventRemoveCallback()
console.stream = None

return console.run_console


def stdin_callback(watch: int, fd: int, events: int, console: Console) -> None:
readbuf = os.read(fd, 1024)
if readbuf.startswith(b""):
console.run_console = False
return
if console.stream:
console.stream.send(readbuf)


def stream_callback(stream: libvirt.virStream, events: int, console: Console) -> None:
try:
assert console.stream
received_data = console.stream.recv(1024)
except Exception:
return
os.write(0, received_data)


def lifecycle_callback(connection: libvirt.virConnect, domain: libvirt.virDomain, event: int, detail: int, console: Console) -> None:
console.state = console.domain.state(0)
logging.info("%s transitioned to state %d, reason %d",
console.uuid, console.state[0], console.state[1])


# main
parser = ArgumentParser(epilog="Example: %(prog)s 'qemu:https:///system' '32ad945f-7e78-c33a-e96d-39f25e025d81'")
parser.add_argument("uri")
parser.add_argument("uuid")
args = parser.parse_args()

print("Escape character is ^]")
logging.basicConfig(filename='msg.log', level=logging.DEBUG)
logging.info("URI: %s", args.uri)
logging.info("UUID: %s", args.uuid)

libvirt.virEventRegisterDefaultImpl()
libvirt.registerErrorHandler(error_handler, None)

atexit.register(reset_term)
attrs = termios.tcgetattr(0)
tty.setraw(0)

console = Console(args.uri, args.uuid)
console.stdin_watch = libvirt.virEventAddHandle(0, libvirt.VIR_EVENT_HANDLE_READABLE, stdin_callback, console)

while check_console(console):
libvirt.virEventRunDefaultImpl()
11 changes: 10 additions & 1 deletion conf/nginx/webvirtcloud.conf
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,17 @@ server {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /socket.io/ {
proxy_pass http:https://wssocketiod;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}

upstream wsnovncd {
server 127.0.0.1:6080;
}
}
upstream wssocketiod {
server 127.0.0.1:6081;
}
1 change: 1 addition & 0 deletions conf/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ rwlock==0.0.7
websockify==0.10.0
zipp==3.6.0
ldap3==2.9.1
python-socketio==5.7.0
10 changes: 9 additions & 1 deletion conf/supervisor/webvirtcloud.conf
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,12 @@ directory=/srv/webvirtcloud
user=www-data
autostart=true
autorestart=true
redirect_stderr=true
redirect_stderr=true

[program:socketiod]
command=/srv/webvirtcloud/venv/bin/python3 /srv/webvirtcloud/console/socketiod -d
directory=/srv/webvirtcloud
user=www-data
autostart=true
autorestart=true
redirect_stderr=true
195 changes: 195 additions & 0 deletions console/socketiod
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#!/usr/bin/env python3
import os
import sys
import logging
import django

DIR_PATH = os.path.dirname(os.path.abspath(__file__))
ROOT_PATH = os.path.abspath(os.path.join(DIR_PATH, '..', ''))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'webvirtcloud.settings')
CERT = DIR_PATH + '/cert.pem'

if ROOT_PATH not in sys.path:
sys.path.append(ROOT_PATH)

django.setup()

import re
import tempfile
import io
import socket
import socketio
import pty
import select
import subprocess
import struct
import fcntl
import termios
import signal
import eventlet
import atexit
import tty
import termios
import libvirt

from six.moves import http_cookies as Cookie
from webvirtcloud.settings import SOCKETIO_PORT, SOCKETIO_HOST
from vrtManager.connection import CONN_SSH, CONN_SOCKET
from optparse import OptionParser

parser = OptionParser()

parser.add_option("-v",
"--verbose",
dest="verbose",
action="store_true",
help="Verbose mode",
default=False)

parser.add_option("-d",
"--debug",
dest="debug",
action="store_true",
help="Debug mode",
default=False)

parser.add_option("-H",
"--host",
dest="host",
action="store",
help="Listen host",
default=SOCKETIO_HOST)

parser.add_option("-p",
"--port",
dest="port",
action="store",
help="Listen port",
default=SOCKETIO_PORT or 6081)

(options, args) = 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)

async_mode = "eventlet"
sio = socketio.Server(async_mode=async_mode, cors_allowed_origins="https://vmm.cyborgside.net")

fd = None
child_pid = None

def get_connection_infos(token):
from instances.models import Instance
from vrtManager.instance import wvmInstance

try:
temptoken = token.split('-', 1)
host = int(temptoken[0])
uuid = temptoken[1]
instance = Instance.objects.get(compute_id=host, uuid=uuid)
conn = wvmInstance(instance.compute.hostname,
instance.compute.login,
instance.compute.password,
instance.compute.type,
instance.name)
except Exception as e:
logging.error(
'Fail to retrieve console connection infos for token %s : %s' % (token, e))
raise
return (instance, conn)

def set_winsize(fd, row, col, xpix=0, ypix=0):
winsize = struct.pack("HHHH", row, col, xpix, ypix)
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)


def read_and_forward_pty_output():
global fd
max_read_bytes = 1024 * 20
while True:
sio.sleep(0.01)
if fd:
timeout_sec = 0
(data_ready, _, _) = select.select([fd], [], [], timeout_sec)
if data_ready:
output = os.read(fd, max_read_bytes).decode()
sio.emit("pty_output", {"output": output})
else:
return


@sio.event
def resize(sid, message):
global fd
if fd:
set_winsize(fd, message["rows"], message["cols"])

@sio.event
def pty_input(sid, message):
global fd
if fd:
os.write(fd, message["input"].encode())

@sio.event
def disconnect_request(sid):
sio.disconnect(sid)

@sio.event
def connect(sid, environ):
global fd
global child_pid

hcookie = environ.get('HTTP_COOKIE')
if hcookie:
cookie = Cookie.SimpleCookie()
for hcookie_part in hcookie.split(';'):
hcookie_part = hcookie_part.lstrip()
try:
cookie.load(hcookie_part)
except Cookie.CookieError:
logging.warn('Found malformed cookie')
else:
if 'token' in cookie:
token = cookie['token'].value

if child_pid:
# already started child process, don't start another
# write a new line so that when a client refresh the shell prompt is printed
fd.write("\n")
return

# create child process attached to a pty we can read from and write to
(child_pid, fd) = pty.fork()

if child_pid == 0:
(instance, conn) = get_connection_infos(token)
uuid = conn.get_uuid()
uri = conn.wvm.getURI()
subprocess.run(["consolecallback", uri, uuid])
else:
# this is the parent process fork.
sio.start_background_task(target=read_and_forward_pty_output)

@sio.event
def disconnect(sid):

global fd
global child_pid

# kill pty process
os.kill(child_pid,signal.SIGKILL)
os.wait()

# reset the variables
fd = None
child_pid = None

app = socketio.WSGIApp(sio)
import eventlet
eventlet.wsgi.server(eventlet.listen((options.host,int(options.port))), app)
Loading

0 comments on commit 65dce10

Please sign in to comment.