1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2024-10-31 19:44:16 +00:00

Implement Libvirt Serial Console as Console on WebVirtCloud

This commit is contained in:
Gaëtan Ferez 2022-07-17 13:25:47 +02:00
parent f6915ac51f
commit 65dce10ce1
10 changed files with 398 additions and 2 deletions

100
conf/daemon/consolecallback Executable file
View file

@ -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:///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()

View file

@ -28,8 +28,17 @@ server {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /socket.io/ {
proxy_pass http://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;
}

View file

@ -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

View file

@ -13,3 +13,11 @@ user=www-data
autostart=true
autorestart=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
console/socketiod Executable file
View file

@ -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)

View file

@ -0,0 +1,75 @@
{% extends "console-base.html" %}
{% load i18n %}
{% load static %}
{% block head %}
<title>WebVirtCloud - XTerm</title>
<link rel="stylesheet" href="https://unpkg.com/xterm@3.6.0/dist/xterm.css" />
<script src="https://unpkg.com/xterm@3.6.0/dist/xterm.js"></script>
<script src="https://unpkg.com/xterm@3.6.0/dist/addons/fit/fit.js"></script>
<script src="https://unpkg.com/xterm@3.6.0/dist/addons/fullscreen/fullscreen.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.0/socket.io.js"></script>
{% endblock %}
{% block content %}
<div style="background: white; padding-bottom: 5px;">
<span style="font-size: small;">Status: <span style="font-size: small;" id="status">connecting...</span></span>
<button id="button"; type="button"; onclick="myFunction()";>Connect</button>
</div>
<div style="width: 100%; height:100%;" id="terminal"></div>
<script crossorigin="anonymous">
Terminal.applyAddon(fit)
var socket = io.connect({transports: ["websocket", "polling"]});
const status = document.getElementById("status")
const button = document.getElementById("button")
var term = new Terminal({
cursorBlink: true,
});
term.open(document.getElementById('terminal'));
term.on('key', (key, ev) => {
console.log("pressed key", key)
socket.emit("pty_input", {"input": key})
});
socket.on("pty_output", function(output){
console.log(output["output"])
term.write(output["output"])
})
socket.on("connect", () => {
status.innerHTML = '<span style="background-color: lightgreen;">connected</span>'
button.innerHTML = 'Disconnect'
})
socket.on("disconnect", () => {
status.innerHTML = '<span style="background-color: #ff8383;">disconnected</span>'
button.innerHTML = 'Connect'
})
function myFunction(){
if (button.innerHTML =='Connect'){
location.reload();
}
else if (button.innerHTML == "Disconnect"){
socket.emit("disconnect_request")
}
}
function resize(){
term.fit()
socket.emit("resize", {"cols": term.cols, "rows": term.rows})
}
window.onresize = resize
window.onload = resize
</script>
{% endblock %}

View file

@ -79,6 +79,8 @@ def console(request):
if console_type == "vnc" or console_type == "spice":
console_page = "console-" + console_type + "-" + view_type + ".html"
response = render(request, console_page, locals())
elif console_type == "pty":
response = render(request, "console-xterm.html", locals())
else:
if console_type is None:
console_error = _("Fail to get console. Please check the console configuration of your VM.")

View file

@ -1019,6 +1019,8 @@ class wvmInstance(wvmConnect):
def get_console_type(self):
console_type = util.get_xml_path(self._XMLDesc(0), "/domain/devices/graphics/@type")
if console_type is None:
console_type = util.get_xml_path(self._XMLDesc(0), "/domain/devices/console/@type")
return console_type
def set_console_type(self, console_type):

View file

@ -254,6 +254,7 @@ install_webvirtcloud () {
pip3 install -r conf/requirements.txt -q
cp "$APP_PATH/conf/daemon/consolecallback" "$APP_PATH/venv/bin/consolecallback"
chown -R "$nginx_group":"$nginx_group" "$APP_PATH"

View file

@ -198,6 +198,9 @@ WS_PUBLIC_PATH = "/novncd/"
# Websock Certificate for SSL
WS_CERT = None
SOCKETIO_PORT = 6081
SOCKETIO_HOST = '0.0.0.0'
# List of console listen addresses
QEMU_CONSOLE_LISTEN_ADDRESSES = (
("127.0.0.1", "Localhost"),