-
Notifications
You must be signed in to change notification settings - Fork 362
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement Libvirt Serial Console as Console on WebVirtCloud
- Loading branch information
Showing
10 changed files
with
398 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.