vmm/vmmd

2617 lines
96 KiB
Plaintext
Raw Normal View History

2021-04-22 02:50:33 +02:00
#!/usr/bin/python3
# API:
# - GET /api/user
# - GET /api/user?id=#
# - POST /api/user (id=#, action=<edit|delete|...> ...)
# - POST /api/user/create
#
# - GET /api/image
# - GET /api/image?id=#
# - POST /api/image (id=# actions: delete)
# - POST /api/image/create (name, ...)
# - PUT /api/image/create (header: X-Image-Name)
#
# - GET /api/vm
# - GET /api/vm?id=#
# - POST /api/vm (id=# actions: delete, start, reset, suspend, restore, poweroff, kill)
# - POST /api/vm/create (arch, cpus, mem, disk, ...)
# Global configuration:
# network.mode=<none | manual | bridge> { manual }
# network.bridge.name=<name> { br0 }
# network.bridge.mode=<manual | subnet | nat> { subnet |
# network.bridge.addr=<ipaddr> { 172.16.1.1/24 }
# network.dhcp.start=<ipaddr> { based on bridge addr }
# network.dhcp.end=<ipaddr> { based on bridge addr }
#
# nic.mac.start=<macaddr> { 52:54:00:00:00:00 }
#
# iso.storage.location=<path> { /var/lib/vmm/isos }
# image.storage.location=<path> { /var/lib/vmm/images }
#
# disk.storage.type=<dir | lvm> { dir }
# disk.storage.location=<path | name> { /var/lib/vmm/disk | vg0 }
# disk.storage.name=<format> { %u.%n }
#
# vm.nesting=<bool> { true }
# vm.max_cpu { none }
# vm.max_mem { none }
# vm.max_disk { none }
#
# user.max_cpu { none }
# user.max_mem { none }
# user.max_disk { none }
#
# Notes:
# Storage:
# For storage.name has the following replacments:
# %n = vm name
# %u = user name
# %g = group name
# %U = user id
# %G = group id
#
# Limits:
# Max VM's per host: 16k (somewhat arbitrary)
# Max ports available for VNC is about 60000.
# At 4gb per VM, 16k VM's takes 64tb.
#
# TODO:
# Need either daemon or suid script to manage VM IDs and resource limits.
#
# Daemon
# ======
# name: vmmd (Virtual Machine Manager Daemon)
# config: /etc/vmm/config
# database: /var/lib/vmm/database.json (json, xml, pickle, ...?)
# state: /var/lib/vmm/state.json (json, xml, pickle, ...?)
#
# VM configuration in database:
# - ID
# - CPU count
# - Memory
# - Other hardware? USB, NIC model, cdrom image, ...?
import os
import sys
import platform
import errno
import getopt
import pwd
import hashlib
import random
2021-04-22 19:22:24 +02:00
import string
2021-04-22 02:50:33 +02:00
import threading
import subprocess
import signal
import time
import syslog
import socket
import select
import json
import http.server
import ssl
import urllib.parse
import urllib.request
import email.parser
import email.policy
2021-04-26 06:26:40 +02:00
import sqlite3
2021-04-22 02:50:33 +02:00
import traceback
ONE_KB = 1 << 10
ONE_MB = 1 << 20
ONE_GB = 1 << 30
ONE_TB = 1 << 40
### Globals ###
log_dir = '/var/log'
log_debug = False
log_verbose = 0
state_dir = '/var/lib/vmm'
run_dir = '/var/run/vmm'
options = None
config = None
uid = os.getuid()
gid = os.getgid()
pid = os.getpid()
tmp_dir = os.environ.get('TMPDIR', '/tmp')
2021-04-26 06:26:40 +02:00
users_table = None
images_table = None
vms_table = None
leases = {}
2021-04-22 02:50:33 +02:00
running = True
2021-04-22 02:50:33 +02:00
### Misc utilities ###
def is_int(v):
try:
int(v)
except ValueError:
return False
return True
def to_int(v):
return int(v)
num_suffixes = {
'k': 1 << 10, 'K': 1 << 10,
'm': 1 << 20, 'M': 1 << 20,
'g': 1 << 30, 'G': 1 << 30,
't': 1 << 40, 'T': 1 << 40
}
def is_num(v):
idx = 0
while idx < len(v) and v[idx].isdigit():
idx += 1
if idx < len(v):
return v[idx:] in num_suffixes
return True
def to_num(v):
idx = 0
while idx < len(v) and v[idx].isdigit():
idx += 1
if idx < len(v):
num = int(v[:idx])
suffix = v[idx:]
if not suffix in num_suffixes:
raise RuntimeError('Bad numeric suffix')
return num * num_suffixes[suffix]
return int(v)
def is_float(v):
try:
float(v)
except ValueError:
return False
return True
def to_float(v):
return float(v)
bool_t_values = [ 'true', 'yes', 'on' ]
bool_f_values = [ 'false', 'no', 'off' ]
def is_bool(v):
return v.lower() in bool_t_values + bool_f_values
def to_bool(v):
return v.lower() in bool_t_values
def autotype(v):
if not v:
return ''
if is_int(v):
return to_int(v)
if is_num(v):
return to_num(v)
if is_float(v):
return to_float(v)
if is_bool(v):
return to_bool(v)
return str(v)
def readable_size(val, scale=1):
if val is None:
return 'Unknown'
n = val * scale
if n < 1000 * (1 << 10):
d = (1 << 10)
m = 'KB'
elif n < 1000 * (1 << 20):
d = (1 << 20)
m = 'MB'
elif n < 1000 * (1 << 30):
d = (1 << 30)
m = 'GB'
else:
d = (1 << 40)
m = 'TB'
i = int(n / d)
f = int((n - (i * d)) / (d / 100))
2021-04-22 02:50:33 +02:00
return "%d.%02d %s" % (i, f, m)
def readable_time(val):
h = val / 3600
val -= h * 3600
m = val / 60
val -= m * 60
s = val
if h > 0:
return "%d:%02d:%02d" % (h, m, s)
elif m > 0:
return "%02d:%02d" % (m, s)
else:
return ":%02d" % (s)
def parse_num(strval):
idx = 0
2021-04-22 18:42:15 +02:00
while idx < len(strval) and (strval[idx] in '0123456789.'):
2021-04-22 02:50:33 +02:00
idx += 1
if idx < len(strval):
factors = {
'k': 1 << 10, 'kb': 1 << 10,
'm': 1 << 20, 'mb': 1 << 20,
'g': 1 << 30, 'gb': 1 << 30,
't': 1 << 40, 'tb': 1 << 40,
}
2021-04-22 18:42:15 +02:00
num = float(strval[:idx])
2021-04-22 02:50:33 +02:00
suffix = strval[idx:].lower().strip()
if not suffix in factors:
raise RuntimeError('Bad numeric suffix')
2021-04-22 18:42:15 +02:00
return int(num * factors[suffix])
2021-04-22 02:50:33 +02:00
return int(strval)
def mkdir_p(path, mode=None):
parent = os.path.dirname(path)
if parent and parent != '/':
mkdir_p(parent, mode)
try:
if mode is None:
os.mkdir(path)
else:
os.mkdir(path, mode)
except OSError as e:
if e.errno != errno.EEXIST:
raise
def sha256_bytes(val):
hasher = hashlib.sha256()
hasher.update(val)
return hasher.hexdigest()
def sha256_string(val):
return sha256_bytes(val.encode())
2021-04-22 19:22:24 +02:00
def pwgen(pwlen=8):
pw = ''
while len(pw) < pwlen:
pw += random.choice(string.ascii_letters + string.digits + '-.')
return pw
2021-04-22 02:50:33 +02:00
def file_write(pathname, buf):
f = open(pathname, 'w')
f.write(buf)
f.close()
def file_delete(pathname):
try:
os.unlink(pathname)
except OSError as e:
if e.errno != errno.ENOENT:
raise
def file_install(pathname, buf, mode=None):
mkdir_p(os.path.dirname(pathname))
f = open(pathname, 'w')
f.write(buf)
f.close()
if not mode is None:
os.chmod(pathname, mode)
def file_remove(pathname):
try:
os.unlink(pathname)
except OSError as e:
if e.errno != errno.ENOENT:
raise
def file_wait_exists(pathname, timeout, msg=None):
if msg is None:
msg = "Timeout out waiting for %s" % (pathname)
elapsed = 0.0
while elapsed < timeout:
if os.path.exists(pathname):
return
time.sleep(0.1)
elapsed += 0.1
raise RuntimeError(msg)
2021-04-22 02:50:33 +02:00
def image_pathname(root, name, ext):
pathname = "%s/%s" % (root, name)
if not pathname.endswith(ext):
pathname += ext
return pathname
2021-04-24 01:52:30 +02:00
def image_info(pathname):
physical_size = None
virtual_size = None
(root, ext) = os.path.splitext(pathname)
format = ext[1:]
try:
sb = os.stat(pathname)
physical_size = (sb.st_blocks * 512) / ONE_MB
virtual_size = sb.st_size / ONE_MB
except OSError as e:
pass
out = ''
try:
argv = ['qemu-img', 'info', '-U', pathname]
2021-04-24 01:52:30 +02:00
(out, err) = cmd_run(argv)
except RuntimeError as e:
pass
for line in out.rstrip('\n').split('\n'):
if line.find(':') == -1:
continue
(k, v) = line.split(':', 1)
v = v.strip()
if k == 'file format':
format = v
if v == 'raw':
f = open(pathname, 'rb')
f.seek(0x8000)
buf = f.read(6)
f.close()
if buf == b'\x01CD001':
format = 'iso'
if k == 'virtual size':
i1 = v.find('(') + 1
i2 = v.find(' ', i1)
virtual_size = int(v[i1:i2]) / ONE_MB
return (physical_size, virtual_size, format)
2021-04-22 02:50:33 +02:00
# XXX: cache results
def find_in_path(name):
for dir in os.environ["PATH"].split(":"):
path = "%s/%s" % (dir, name)
if os.path.exists(path):
return path
raise OSError("%s not found in %s" % (name, os.environ["PATH"]))
### Process utilities ###
daemonized = False
def daemonize():
global daemonized
pid = os.fork()
if pid > 0:
os._exit(0)
os.chdir('/')
os.umask(0o022)
nullfd = os.open('/dev/null', os.O_RDWR)
os.dup2(nullfd, 0)
os.dup2(nullfd, 1)
os.dup2(nullfd, 2)
os.close(nullfd)
pid = os.fork()
if pid > 0:
os._exit(0)
syslog.openlog(logoption=syslog.LOG_PID)
daemonized = True
def pid_exists(pid):
if pid is None or pid < 1:
return False
try:
os.kill(pid, 0)
except OSError as e:
if e.errno == errno.EPERM:
return True
return False
return True
def pid_kill(pid, nofail=None, sig=None):
if nofail is None:
nofail = False
if sig is None:
sig = signal.SIGTERM
if pid <= 0:
return
try:
os.kill(pid, sig)
except OSError as e:
if not nofail:
raise
def pidfile_read(name, path=None):
if path is None:
path = run_dir
pathname = "%s/%s.pid" % (path, name)
pid = 0
try:
f = open(pathname, 'r+')
line = f.readline()
f.close()
pid = int(line)
except:
pass
return pid
def pidfile_write(name, path=None):
if path is None:
path = run_dir
pathname = "%s/%s.pid" % (path, name)
oldpid = 0
f = open(pathname, 'a+')
try:
line = f.readline()
oldpid = int(line)
except:
pass
if oldpid != 0 and pid_exists(oldpid):
f.close()
raise RuntimeError("Cannot write pidfile, %s already running with pid %d" % (name, oldpid))
f.seek(0)
f.truncate()
f.write("%d\n" % (os.getpid()))
f.close()
def pidfile_remove(name, path=None):
if path is None:
path = run_dir
pathname = "%s/%s.pid" % (path, name)
try:
os.remove(pathname)
except:
pass
def cmd_run(args, stdin=None):
logi("cmd_run: %s\n" % (args))
if not args[0].startswith('/'):
args[0] = find_in_path(args[0])
2021-04-22 02:50:33 +02:00
child = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if not stdin is None:
child.stdin.write(stdin)
(out, err) = child.communicate()
rc = child.returncode
if rc != 0:
raise RuntimeError("Failed to run \"%s\"" % (' '.join(args)))
return (out.decode(), err.decode())
def fork_child(args):
logi("fork_child: %s" % (args))
cmd = args[0] if args[0].startswith('/') else find_in_path(args[0])
2021-04-22 02:50:33 +02:00
pid = os.fork()
if pid == 0:
try:
os.execv(cmd, args)
2021-04-22 02:50:33 +02:00
except BaseException as e:
sys.stderr.write("os.execv raised %s\n" % (e))
sys.stderr.write("os.execv returned unexpectedly\n")
os._exit(0)
return pid
### Logging utilities ###
def logx(level, msg):
now = time.strftime("%Y-%m-%d %H:%M:%S")
if not msg.endswith('\n'):
msg += '\n'
if log_debug:
sys.stderr.write(msg)
else:
f = open("%s/vmmd.log" % (log_dir), "a")
f.write("%s [%d] [%s] %s" % (now, os.getpid(), level, msg))
f.close()
def logi(msg):
logx('info', msg)
def logw(msg):
logx('warn', msg)
def loge(msg):
logx('error', msg)
def logv(msg):
if log_verbose > 0:
logx('debug', msg)
### Signal handlers ###
def sig_term(signum, frame):
global running
running = False
2021-04-22 02:50:33 +02:00
def sig_chld(signum, frame):
try:
while True:
(pid, status) = os.waitpid(0, os.WNOHANG)
except:
pass
2021-04-22 02:50:33 +02:00
### Config ###
class Config:
def __init__(self, pathname=None, defaults=None):
if defaults:
self._config = defaults
else:
self._config = {}
if pathname:
self.load(pathname)
def load(self, pathname):
try:
f = open(pathname, 'r')
except IOError as e:
if e.errno == errno.ENOENT:
return
raise
for line in f:
i = line.find('#')
if i != -1:
line = line[:i]
line = line.strip()
if not line:
continue
append = False
if line.find('+=') != -1:
fields = line.split('+=', 1)
append = True
elif line.find('=') != -1:
fields = line.split('=', 1)
else:
fields = [line, 'true']
k = fields[0].strip()
v = autotype(fields[1].strip())
if append:
if k in self._config:
if is_int(self._config[k]):
self._config[k] += v
else:
self._config[k] += " %s" % (v)
else:
self._config[k] = v
else:
self._config[k] = v
def __contains__(self, k):
return k in self._config
def __getitem__(self, k):
if not k in self._config:
raise KeyError(k)
return self._config[k]
def get(self, k, default=None):
if not k in self._config:
return default
return self._config[k]
### ScopedLocker ###
class ScopedLocker:
def __init__(self, lock):
self._lock = lock
self._lock.acquire()
def __del__(self):
self._lock.release()
### DbObject ###
class DbObject:
2021-04-26 06:26:40 +02:00
def __init__(self, row=None):
self._dict = {}
self._changed = set()
if row:
for k in row.keys():
self._dict[k] = row[k]
def keys(self):
return self._dict.keys()
def values(self):
return self._dict.values()
2021-05-13 20:10:17 +02:00
def __contains__(self, name):
return name in self._dict
2021-04-26 06:26:40 +02:00
def __getitem__(self, name):
return self._dict[name]
def get(self, name, default):
if name in self._dict:
return self._dict[name]
return default
def __setitem__(self, name, val):
self._changed.add(name)
self._dict[name] = val
class DbTable:
def __init__(self, dbconn, name, sql):
self._dbconn = dbconn
self._name = name
2021-04-22 02:50:33 +02:00
self._lock = threading.Lock()
2021-04-26 06:26:40 +02:00
query = "CREATE TABLE IF NOT EXISTS %s (%s)" % (self._name, sql)
self._dbconn.execute(query)
2021-04-22 02:50:33 +02:00
2021-04-29 01:25:46 +02:00
def _sql_val_str(self, v):
return 'NULL' if v is None else "'%s'" % (v)
2021-04-26 06:26:40 +02:00
def empty(self):
query = "SELECT COUNT(*) FROM %s" % (self._name)
cursor = self._dbconn.execute(query)
row = cursor.fetchone()
return row[0] == 0
2021-04-22 02:50:33 +02:00
2021-04-26 06:26:40 +02:00
def select_all(self):
locker = ScopedLocker(self._lock)
2021-04-30 07:05:06 +02:00
query = "SELECT * FROM %s" % (self._name)
2021-04-26 06:26:40 +02:00
return dbconn.execute(query)
def select_where(self, wherestr):
locker = ScopedLocker(self._lock)
2021-04-30 07:05:06 +02:00
query = "SELECT * FROM %s WHERE %s" % (self._name, wherestr)
res = dbconn.execute(query)
if res is None:
res = []
return res
def delete_where(self, wherestr):
locker = ScopedLocker(self._lock)
query = "DELETE FROM %s WHERE %s" % (self._name, wherestr)
2021-04-26 06:26:40 +02:00
res = dbconn.execute(query)
if res is None:
res = []
return res
def select_one_where(self, wherestr):
res = self.select_where(wherestr)
row = None
n = 0
for row in res:
n += 1
if n != 1:
raise RuntimeError("Unexpected row count")
return row
def select_by_id(self, id):
res = self.select_where("id=%d" % (id))
2021-04-26 06:26:40 +02:00
if not res:
return None
return res.fetchone()
def select_by_name(self, name):
res = self.select_where("name='%s'" % (name))
if not res:
return None
return res.fetchone()
2021-04-22 02:50:33 +02:00
def insert(self, obj):
2021-04-26 06:26:40 +02:00
locker = ScopedLocker(self._lock)
keystr = ','.join(obj.keys())
2021-04-29 01:25:46 +02:00
valstr = ','.join([self._sql_val_str(v) for v in obj.values()])
2021-04-26 06:26:40 +02:00
query = "INSERT INTO %s (%s) values (%s)" % (self._name, keystr, valstr)
cursor = dbconn.execute(query)
obj['id'] = cursor.lastrowid
def update(self, obj):
if not obj._changed:
return
locker = ScopedLocker(self._lock)
query = "UPDATE %s SET " % (self._name)
change_vec = []
for k in obj._changed:
if k == 'id':
continue
2021-04-29 01:25:46 +02:00
change_vec.append("%s=%s" % (k, self._sql_val_str(obj._dict[k])))
2021-04-26 06:26:40 +02:00
query += ','.join(change_vec)
query += " WHERE id=%d" % (obj._dict['id'])
self._dbconn.execute(query)
obj._changed = set()
def delete(self, obj):
locker = ScopedLocker(self._lock)
query = "DELETE FROM %s WHERE id=%d" % (self._name, obj['id'])
self._dbconn.execute(query)
2021-04-22 02:50:33 +02:00
### User ###
class User(DbObject):
2021-04-26 06:26:40 +02:00
def __init__(self, row):
DbObject.__init__(self, row)
2021-04-22 02:50:33 +02:00
@staticmethod
def create(name, fullname, password, groups):
2021-04-26 06:26:40 +02:00
return User({'name': name, 'fullname': fullname,
'pwhash': sha256_string(password), 'groups': groups})
2021-04-22 02:50:33 +02:00
def auth_password(self, password):
pwhash = sha256_string(password)
2021-04-26 06:26:40 +02:00
return pwhash == self['pwhash']
2021-04-22 02:50:33 +02:00
def in_group(self, group):
2021-04-26 06:26:40 +02:00
return group in self['groups'].split(':')
2021-04-22 02:50:33 +02:00
def may_access_file(self, pathname):
2021-04-26 06:26:40 +02:00
if not self['localuid']:
2021-04-22 02:50:33 +02:00
return False
sb = os.stat(pathname)
2021-04-26 06:26:40 +02:00
return sb.st_uid == self['localuid']
2021-04-22 02:50:33 +02:00
### Image ###
class Image(DbObject):
2021-04-26 06:26:40 +02:00
def __init__(self, row):
DbObject.__init__(self, row)
2021-04-22 02:50:33 +02:00
self._copy_status = None
2021-04-26 06:26:40 +02:00
(self._physical_size, self._virtual_size, self._fmt) = image_info(self['pathname'])
2021-04-22 02:50:33 +02:00
self._ref = 0
@staticmethod
def create_from_local(name, pathname, user, public):
2021-04-26 06:26:40 +02:00
return Image({'name': name, 'pathname': pathname, 'owner': user['name'], 'public': public})
2021-04-22 02:50:33 +02:00
@staticmethod
def create_from_upload(name, filename, data, user, public):
if filename.endswith('.iso'):
pathname = "%s/%s" % (config['iso.storage.location'], filename)
else:
pathname = "%s/%s" % (config['image.storage.location'], filename)
if os.path.exists(pathname):
raise RuntimeError("Image already exists")
2021-04-22 02:50:33 +02:00
f = open(pathname, 'wb')
f.write(data)
f.close()
return Image.create_from_local(name, pathname, user, public)
@staticmethod
def create_from_url(name, url, user, public):
if url.endswith('.iso'):
pathname = "%s/%s" % (config['iso.storage.location'], os.path.basename(url))
else:
pathname = "%s/%s" % (config['image.storage.location'], os.path.basename(url))
if os.path.exists(pathname):
raise RuntimeError("Image already exists")
2021-04-26 06:26:40 +02:00
img = Image({'name': name, 'pathname': pathname, 'owner': user['name'], 'public': public})
acp_queue(url, pathname)
2021-04-22 02:50:33 +02:00
return img
@staticmethod
def create_from_vmdisk(name, vm, user, public):
2021-04-29 02:10:57 +02:00
filename = os.path.basename(vm['diskpath'])
2021-04-22 02:50:33 +02:00
pathname = "%s/%s" % (config['image.storage.location'], filename)
if os.path.exists(pathname):
raise RuntimeError("Image already exists")
2021-04-26 06:26:40 +02:00
img = Image({'name': name, 'pathname': pathname, 'owner': user['name'], 'public': public})
2021-04-29 02:10:57 +02:00
acp_queue(vm['diskpath'], img['pathname'])
2021-04-22 02:50:33 +02:00
return img
def fmt(self):
return self._fmt
def physical_size(self):
try:
sb = os.stat(self['pathname'])
self._physical_size = (sb.st_blocks * 512) / ONE_MB
except OSError as e:
pass
return self._physical_size
def virtual_size(self):
return self._virtual_size
2021-04-22 02:50:33 +02:00
def type(self):
return 'iso' if self._fmt == 'iso' else 'disk'
2021-04-22 02:50:33 +02:00
def incref(self):
self._ref += 1
def decref(self):
assert self._ref > 0
self._ref -= 1
### VirtualMachine ###
class VirtualMachine(DbObject):
2021-04-26 06:26:40 +02:00
def __init__(self, row):
DbObject.__init__(self, row)
self._lock = threading.Lock()
if self.get('macaddr', None) is None:
2021-04-22 02:50:33 +02:00
found = False
while not found:
b4 = int(random.random() * 256)
b5 = int(random.random() * 256)
b6 = int(random.random() * 256)
2021-04-29 06:53:51 +02:00
macaddr = "52:54:00:%02x:%02x:%02x" % (b4, b5, b6)
cursor = vms_table.select_where("macaddr='%s'" % (macaddr))
row = cursor.fetchone()
if not row:
2021-04-22 02:50:33 +02:00
found = True
2021-04-29 06:53:51 +02:00
self['macaddr'] = macaddr
if self.get('vncpass', None) is None:
2021-04-26 06:26:40 +02:00
self['vncpass'] = pwgen(8)
if self.get('uuid', None) is None:
2021-05-13 20:17:27 +02:00
f1 = "%08x" % (int(random.random() * (1 << 32)))
f2 = "%04x" % (int(random.random() * (1 << 16)))
f3 = "%04x" % (int(random.random() * (1 << 16)))
f4 = "%04x" % (int(random.random() * (1 << 16)))
f5 = "%012x" % (int(random.random() * (1 << 48)))
self['uuid'] = "%s-%s-%s-%s-%s" % (f1, f2, f3, f4, f5)
2021-04-26 06:26:40 +02:00
(self._disk_psize, self._disk_vsize, self._disk_fmt) = image_info(self['diskpath'])
2021-04-22 02:50:33 +02:00
self._copy_status = None
@staticmethod
def pathname_for_disk(username, vmname, ext):
return "%s/%s.%s%s" % (config['disk.storage.location'], username, vmname, ext)
@staticmethod
def create_new(name, owner, arch, cpus, mem, disk_size):
# XXX: deal with LVM
2021-04-26 06:26:40 +02:00
diskpath = VirtualMachine.pathname_for_disk(owner['name'], name, '.qcow2')
if os.path.exists(diskpath):
raise RuntimeError("Disk already exists")
argv = ['qemu-img', 'create',
2021-04-22 02:50:33 +02:00
'-f', 'qcow2',
'-o', 'preallocation=metadata',
diskpath, "%dM" % (disk_size)]
2021-04-22 02:50:33 +02:00
cmd_run(argv)
2021-04-26 06:26:40 +02:00
return VirtualMachine({'name': name, 'owner': owner['name'],
'arch': arch, 'cpus': cpus, 'mem': mem,
'diskpath': diskpath})
2021-04-22 02:50:33 +02:00
@staticmethod
def create_from_image(name, owner, arch, cpus, mem, image_id):
2021-04-22 02:50:33 +02:00
# XXX: deal with LVM
img = disk_images_table.select_by_id(image_id)
2021-04-26 06:26:40 +02:00
(root, ext) = os.path.splitext(img['pathname'])
diskpath = VirtualMachine.pathname_for_disk(owner['name'], name, ext)
if os.path.exists(diskpath):
raise RuntimeError("Disk already exists")
2021-04-26 06:26:40 +02:00
if ext == '.qcow2':
argv = ['qemu-img', 'create', '-f', 'qcow2', '-b', img['pathname'], diskpath]
2021-04-22 02:50:33 +02:00
cmd_run(argv)
else:
acp_queue(img['pathname'], diskpath)
2021-04-26 06:26:40 +02:00
return VirtualMachine({'name': name, 'owner': owner['name'],
'arch': arch, 'cpus': cpus, 'mem': mem,
'diskpath': diskpath})
2021-04-22 02:50:33 +02:00
@staticmethod
def create_from_local(name, owner, arch, cpus, mem, pathname):
2021-04-26 06:26:40 +02:00
return VirtualMachine({'name': name, 'owner': owner['name'],
'arch': arch, 'cpus': cpus, 'mem': mem,
'diskpath': pathname})
2021-04-22 02:50:33 +02:00
@staticmethod
def create_from_upload(name, owner, arch, cpus, mem, filename, data):
diskpath = "%s/%s" % (config['disk.storage.location'], filename)
if os.path.exists(diskpath):
raise RuntimeError("Disk already exists")
f = open(diskpath, 'wb')
2021-04-22 02:50:33 +02:00
f.write(data)
f.close()
return VirtualMachine.create_from_local(name, owner, arch, cpus, mem, diskpath)
2021-04-22 02:50:33 +02:00
@staticmethod
def create_from_url(name, owner, arch, cpus, mem, image_url):
# XXX: deal with LVM
(root, ext) = os.path.splitext(image_url)
diskpath = VirtualMachine.pathname_for_disk(owner.name(), name, ext)
if os.path.exists(diskpath):
raise RuntimeError("Disk already exists")
vm = VirtualMachine.create_from_local(name, owner, arch, cpus, mem, diskpath)
acp_queue(image_url, diskpath)
2021-04-22 02:50:33 +02:00
return vm
def _qemu_pidfile(self):
return "%s/%04x/qemu.pid" % (run_dir, self['id'])
2021-04-22 02:50:33 +02:00
def _get_iso_pathname(self):
pathname = None
if self.running():
lines = self._run_monitor_command('info block ide1-cd0')
for line in lines:
if line.startswith('ide1-cd0'):
fields = line.split(':', 1)
pathname = fields[1].strip().split(' ')[0]
if pathname.startswith('['):
pathname = None
return pathname
# Return list of snapshots as (id, tag). Disk must not be in use.
def _snapshot_list(self):
snapshots = []
2021-04-26 06:26:40 +02:00
if self['diskpath'].endswith('.qcow2'):
argv = ['qemu-img', 'snapshot', '-l', self['diskpath']]
2021-04-22 02:50:33 +02:00
(out, err) = cmd_run(argv)
for line in out.rstrip('\n').split('\n'):
fields = line.split()
if len(fields) < 3:
continue
if fields[0] == 'ID':
continue
snapshots.append((fields[0], fields[1]))
return snapshots
def _has_snapshot_tag(self, tag):
snapshots = self._snapshot_list()
for item in snapshots:
if item[1] == tag:
return True
return False
def disk_exists(self):
return not self._disk_vsize is None
2021-04-24 01:52:30 +02:00
def disk_physical_size(self):
try:
2021-04-26 06:26:40 +02:00
sb = os.stat(self['diskpath'])
2021-04-24 01:52:30 +02:00
self._disk_psize = (sb.st_blocks * 512) / ONE_MB
except OSError as e:
pass
return self._disk_psize
def disk_virtual_size(self):
return self._disk_vsize
2021-04-22 02:50:33 +02:00
def pid(self):
2021-04-29 00:02:09 +02:00
try:
f = open(self._qemu_pidfile(), 'r')
buf = f.read()
f.close()
pid = int(buf.strip())
if os.path.isdir("/proc/%d" % (pid)):
return pid
return None
except BaseException as e:
return None
def running(self):
return not self.pid() is None
2021-04-22 02:50:33 +02:00
def state(self):
if self.running():
return 'running'
return 'stopped'
2021-04-26 06:26:40 +02:00
def ipv4addr(self):
return leases.get(self['macaddr'], None)
def ipv6addr(self, val=None):
return None
2021-04-22 02:50:33 +02:00
def start(self, **kwargs):
if not self.disk_exists():
raise RuntimeError("Cannot start without disk")
if not acp_progress(self['diskpath']) is None:
2021-04-22 02:50:33 +02:00
raise RuntimeError("Cannot start while copying")
2021-04-26 06:26:40 +02:00
force_readonly = self['diskpath'].endswith('.vmdk')
2021-04-22 02:50:33 +02:00
readonly = force_readonly or kwargs.get('readonly', False)
if force_readonly and not readonly:
raise RuntimeError("VMDK disks must be read-only")
resuming = (not readonly) and self._has_snapshot_tag('vmm-suspend')
vm_run_dir = "%s/%04x" % (run_dir, self['id'])
2021-04-22 02:50:33 +02:00
mkdir_p(vm_run_dir)
2021-04-30 01:00:29 +02:00
has_usb = False
has_pci = False
if self['arch'] == 'x86_64':
prog = 'qemu-system-x86_64'
machine_arg = 'pc'
if platform.machine() == 'x86_64':
machine_arg += ',accel=kvm'
cpu_arg = 'host' if config['vm.nesting'] else 'qemu64'
has_usb = True
has_pci = True
elif self['arch'] == 'arm64':
prog = 'qemu-system-aarch64'
machine_arg = 'virt'
cpu_arg = 'cortex-a53'
has_pci = True
elif self['arch'] == 'rpi3':
prog = 'qemu-system-aarch64'
machine_arg = 'raspi3'
cpu_arg = 'cortex-a53'
else:
raise RuntimeError('Unknown arch')
argv = [prog]
2021-04-22 02:50:33 +02:00
argv.extend(['-daemonize', '-pidfile', self._qemu_pidfile()])
argv.extend(['-machine', machine_arg, '-cpu', cpu_arg])
ethdev = 'virtio-net' if self['ostype'] == 'linux' else 'e1000'
2021-04-30 01:00:29 +02:00
blkopt = ''
if self['arch'] == 'x86_64':
blkopt += ',if=virtio' if self['ostype'] == 'linux' else ',if=ide'
if self['diskpath'].endswith('.img'):
blkopt += ',format=raw'
if readonly:
blkopt += ',snapshot=on'
2021-04-26 06:26:40 +02:00
argv.extend(['-smp', str(self['cpus']),
'-m', "%dM" % self['mem'],
2021-04-22 02:50:33 +02:00
'-monitor', "unix:%s/monitor,server,nowait" % (vm_run_dir),
'-serial', "unix:%s/serial,server,nowait" % (vm_run_dir),
'-vnc', ":%d,password=on" % (self['id']),
2021-05-13 20:17:27 +02:00
'-smbios', "type=1,uuid=%s" % (self['uuid']),
2021-04-30 01:00:29 +02:00
'-drive', "file=%s%s" % (self['diskpath'], blkopt)])
if has_pci:
argv.extend(['-netdev', "bridge,br=%s,id=net1" % (config['network.bridge.name']),
'-device', "%s,netdev=net1,mac=%s" % (ethdev, self['macaddr'])])
if has_usb:
argv.extend(['-usb', '-device', 'usb-tablet'])
2021-04-28 23:53:28 +02:00
if self['isopath']:
2021-04-22 02:50:33 +02:00
# XXX? -drive media=cdrom,file=%s
2021-04-28 23:53:28 +02:00
argv.extend(['-cdrom', self['isopath'], '-boot', 'd'])
2021-04-22 02:50:33 +02:00
if resuming:
argv.extend(['-loadvm', 'vmm-suspend'])
2021-04-26 19:35:39 +02:00
try:
os.unlink(self._qemu_pidfile())
except OSError as e:
pass
2021-04-22 02:50:33 +02:00
fork_child(argv)
file_wait_exists(self._qemu_pidfile(), 2.0)
2021-04-22 02:50:33 +02:00
if resuming:
2021-04-30 19:57:08 +02:00
self._run_monitor_command('delvm vmm-suspend', 5.0)
2021-04-26 06:26:40 +02:00
if self['vncpass']:
self._run_monitor_command("change vnc password %s" % (self['vncpass']))
2021-04-22 02:50:33 +02:00
def _drain_monitor_socket(self, sock):
buf = ''
while not buf.endswith('\n(qemu) '):
buf += sock.recv(4096).decode()
lines = buf.rstrip('\n').replace('\r', '').split('\n')
return lines
2021-04-30 19:57:08 +02:00
def _run_monitor_command(self, cmd, timeout=None):
if timeout is None:
timeout = 1.0
2021-04-22 02:50:33 +02:00
locker = ScopedLocker(self._lock)
vm_run_dir = "%s/%04x" % (run_dir, self['id'])
pathname = "%s/monitor" % (vm_run_dir)
file_wait_exists(pathname, 1.0)
2021-04-22 02:50:33 +02:00
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
2021-04-30 19:57:08 +02:00
sock.settimeout(timeout)
res = []
2021-04-22 02:50:33 +02:00
try:
sock.connect(pathname)
self._drain_monitor_socket(sock)
line = cmd
if not line.endswith('\n'):
line += '\n'
sock.send(line.encode())
res = self._drain_monitor_socket(sock)
except BaseException as e:
print("Exception running monitor command: %s" % (e))
return res
2021-04-22 02:50:33 +02:00
def iso_eject(self):
if self.running():
self._run_monitor_command('eject ide1-cd0')
2021-04-28 23:53:28 +02:00
self['isopath'] = None
def iso_insert(self, isopath):
2021-04-22 02:50:33 +02:00
self.iso_eject()
2021-04-28 23:53:28 +02:00
self['isopath'] = isopath
2021-04-22 02:50:33 +02:00
if self.running():
2021-04-28 23:53:28 +02:00
self._run_monitor_command("change ide1-cd0 %s" % (self['isopath']))
2021-04-22 02:50:33 +02:00
def reset(self):
self._run_monitor_command('system_reset')
def suspend(self):
2021-04-30 19:57:08 +02:00
self._run_monitor_command('savevm vmm-suspend', 15.0)
2021-04-22 02:50:33 +02:00
self.kill()
def poweroff(self):
self._run_monitor_command('system_powerdown')
def kill(self):
2021-04-29 00:02:09 +02:00
pid = self.pid()
if pid:
os.kill(pid, signal.SIGTERM)
2021-04-22 02:50:33 +02:00
tries = 0
2021-04-29 00:02:09 +02:00
while tries < 10 and not self.pid() is None:
tries += 1
2021-04-22 02:50:33 +02:00
time.sleep(0.1)
### CliClientConnectionHandler and listener ###
class CliClientConnectionHandler(threading.Thread):
def __init__(self, sock, secure):
# XXX: This should not be instance data
self._dispatch_table = {
'login': self.cmd_login,
'serial': self.cmd_serial,
'monitor': self.cmd_monitor,
'help': self.cmd_help
}
threading.Thread.__init__(self)
self._sock = sock
self._secure = secure
self._user = None
# Override
def run(self):
buf = ''
try:
while True:
self._sock.send('vmm> '.encode())
while buf.find('\n') == -1:
data = self._sock.recv(4096)
if not data:
return
for ch in data.decode():
if ch == '\x7f':
if len(buf) > 0:
buf = buf[:-1]
self._sock.send(b'\x08 \x08')
continue
if ch == '\r' or ch == '\n':
buf += '\n'
self._sock.send(b'\r\n')
continue
self._sock.send(ch.encode())
buf += ch
idx = buf.find('\n')
while idx != -1:
line = buf[:idx]
buf = buf[idx+1:]
idx = buf.find('\n')
fields = line.split()
if not fields:
continue
cmd = fields[0]
args = fields[1:]
if cmd == 'q' or cmd == 'quit':
raise BrokenPipeError('Client quit')
if not self._user and not cmd in ['login', 'help']:
self.send_msg('-Unauthorized')
continue
if not cmd in self._dispatch_table:
self.send_msg('-Unknown command')
continue
msg = self._dispatch_table[cmd](args)
self.send_msg(msg)
except BaseException as e:
loge("Exception handling client: %s" % (e))
def send_msg(self, msg):
buf = "%s\r\n" % (msg)
self._sock.send(buf.encode())
def cmd_login(self, args):
if len(args) != 2:
return '-Invalid usage'
if is_int(args[0]):
row = users_table.select_by_id(int(args[0]))
2021-04-22 02:50:33 +02:00
else:
row = users_table.select_by_name(args[0])
user = User(row)
2021-04-22 02:50:33 +02:00
if len(args[1]) == 256/8*2:
if user.pwhash() != args[1]:
return '-Unauthorized'
else:
if not self._secure or not user.auth_password(args[1]):
return '-Unauthorized'
self._user = user
return '+ok'
def _cmd_passthru(self, name, args):
if len(args) != 1:
return '-Invalid usage'
if is_int(args[0]):
row = vms_table.select_by_id(int(args[0]))
2021-04-22 02:50:33 +02:00
else:
2021-04-30 02:27:29 +02:00
row = vms_table.select_by_name(args[0])
if row['owner'] != self._user['name'] and not self._user.in_group('admin'):
2021-04-22 02:50:33 +02:00
return '-Unauthorized'
2021-04-30 02:27:29 +02:00
self.send_msg("Connecting to %s ...\n" % (row['name']))
2021-04-22 02:50:33 +02:00
vm_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
2021-04-30 02:27:29 +02:00
vm_sock.connect("%s/%04x/%s" % (run_dir, row['id'], name))
2021-04-22 02:50:33 +02:00
self.send_msg("Escape char is '^['\n")
try:
while True:
(rfds, wfds, efds) = select.select([self._sock, vm_sock], [], [])
if vm_sock in rfds:
buf = vm_sock.recv(4096)
self._sock.send(buf)
if self._sock in rfds:
buf = self._sock.recv(4096)
idx = buf.find(b'\x1b')
if idx != -1:
vm_sock.send(buf[:idx])
break
vm_sock.send(buf)
except BrokenPipeError as e:
self.send_msg("Connection lost")
return '+ok'
def cmd_serial(self, args):
return self._cmd_passthru('serial', args)
def cmd_monitor(self, args):
return self._cmd_passthru('monitor', args)
def cmd_help(self, args):
self.send_msg('login <name|id> <password|token>')
self.send_msg('serial <name|id>')
self.send_msg('monitor <name|id>')
self.send_msg('help')
return '+ok'
def raw_cli_listener():
listen_addr = (config['cli.listen.address'], config['cli.listen.port'])
raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
raw_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
raw_sock.bind(listen_addr)
server_sock = raw_sock
server_sock.listen()
while running:
try:
conn, addr = server_sock.accept()
thread = CliClientConnectionHandler(conn, False)
thread.start()
except BaseException as e:
loge("Exception in raw_cli_listener: %s" % (e))
def ssl_cli_listener():
listen_addr = (config['cli.listen.ssladdress'], config['cli.listen.sslport'])
raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
raw_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
raw_sock.bind(listen_addr)
ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
ssl_ctx.options = ssl.OP_NO_TLSv1_3
ssl_ctx.check_hostname = False
ssl_ctx.load_cert_chain(config['ssl.certfile'])
server_sock = ssl_ctx.wrap_socket(raw_sock, server_side=True)
server_sock.listen()
while running:
try:
conn, addr = server_sock.accept()
thread = CliClientConnectionHandler(conn, True)
thread.start()
except BaseException as e:
loge("Exception in ssl_cli_listener: %s" % (e))
### HttpClientRequestHandler and http_listener ###
class HttpClientRequestHandler(http.server.BaseHTTPRequestHandler):
def __init__(self, request, client_address, server):
self._dispatch_table = {
'/api/v1/user': self._api_v1_user,
'/api/v1/user/create': self._api_v1_user_create,
'/api/v1/image': self._api_v1_image,
'/api/v1/image/create': self._api_v1_image_create,
'/api/v1/vm': self._api_v1_vm,
'/api/v1/vm/create': self._api_v1_vm_create,
# XXX: Ensure aligned with API
# XXX: Add image manipulation (create/upload, delete)
'/ui': self.ui_overview,
'/ui/login': self.ui_login,
'/ui/logout': self.ui_logout,
'/ui/image': self.ui_image,
'/ui/image/create': self.ui_image_create,
'/ui/user': self.ui_user,
'/ui/user/create': self.ui_user_create,
'/ui/vm': self.ui_vm,
'/ui/vm/create': self.ui_vm_create
}
http.server.BaseHTTPRequestHandler.__init__(self, request, client_address, server)
# Override
def version_string(self):
return 'VirtualMachineManager/1.0'
def _send_response(self, code, headers, content):
encoded_content = content.encode() if content else None
self.send_response(code)
if headers:
for k,v in headers.items():
self.send_header(k, v)
if content:
if content.startswith('<'):
self.send_header('Content-Type', 'text/html')
else:
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', len(encoded_content))
self.end_headers()
if encoded_content:
self.wfile.write(encoded_content)
def _api_v1_user(self, user, args):
is_admin = user.in_group('admin')
args_id = None
if 'id' in args:
args_id = int(args['id'][0])
if args_id != user['id'] and not is_admin:
2021-04-22 02:50:33 +02:00
self._send_response(403, None, json.dumps({'result': 403}))
if 'action' in args:
changed = False
if args['action'][0] == 'edit':
if 'fullname' in args:
user.fullname(args['fullname'][0])
changed = True
if 'password' in args:
user.password(args['password'][0])
changed = True
if 'email' in args:
user.email(args['email'][0])
if args['action'][0] == 'delete':
if not user.is_admin:
self._send_response(403, None, json.dumps({'result': 403}))
return
user_db.remove(user)
if changed:
user_db.save()
r = json.dumps({
'result': 200,
'id': user.oid(),
'name': user.name(),
'fullname': user.fullname(),
'groups': user.groups(),
'email': user.email(),
'timezone': user.timezone()
})
self._send_response(200, None, r)
def _api_v1_user_create(self, user, args):
self._send_response(201, None, None)
pass
def _api_v1_image(self, user, args):
pass
def _api_v1_image_create(self, user, args):
pass
def _api_v1_vm(self, user, args):
pass
def _api_v1_vm_create(self, user, args):
pass
def _html_head(self, user=None):
r = ''
r += '<html>\n'
r += ' <head>\n'
r += ' <title>Virtual Machine Manager</title>\n'
2021-04-23 20:00:15 +02:00
r += ' <style>\n'
r += ' a { color: blue; text-decoration: none; }\n'
r += ' </style>\n'
2021-04-22 02:50:33 +02:00
r += ' </head>\n'
r += ' <body>\n'
r += ' <table width="100%">\n'
r += ' <tr>\n'
r += ' <td width="33%">&nbsp;\n'
r += " <td width=\"33%%\" align=\"center\"><img src=\"%s/logo.jpg\" alt=\"logo\">\n" % (config['ui.imagepath'])
2021-04-22 02:50:33 +02:00
if user:
2021-04-26 06:26:40 +02:00
r += " <td width=\"33%%\" align=\"right\" valign=\"top\">%s<br><a href=\"/ui/logout\">logout</a>\n" % (user['fullname'])
2021-04-22 02:50:33 +02:00
else:
r += ' <td width="33%">&nbsp;\n'
r += ' <tr>\n'
r += ' <td>&nbsp;\n'
r += ' <td align="center"><p style="font-size:150%">Virtual Machine Manager</p>\n'
r += ' <td>&nbsp;\n'
r += ' </table>\n'
r += ' <hr width="50%">\n'
if user:
r += ' <table width="100%">\n'
r += ' <tr>\n'
r += ' <td width="20%" style="vertical-align:top">\n'
if user.in_group('admin'):
r += ' <p style="color:red">Admin</p>\n'
r += ' <hr>\n'
2021-04-23 20:00:15 +02:00
r += ' <a href="/ui">Overview</a><br>\n'
2021-04-22 02:50:33 +02:00
r += ' <br>\n'
2021-04-23 20:00:15 +02:00
r += ' <a href="/ui/vm">Virtual Machines</a><br>\n'
2021-04-22 02:50:33 +02:00
r += ' <br>\n'
2021-04-23 20:00:15 +02:00
r += ' <a href="/ui/image?type=disk">Disk Images</a><br>\n'
2021-04-22 02:50:33 +02:00
r += ' <br>\n'
2021-04-23 20:00:15 +02:00
r += ' <a href="/ui/image?type=iso">ISO Images</a><br>\n'
2021-04-22 02:50:33 +02:00
r += ' <br>\n'
2021-04-23 20:00:15 +02:00
r += ' <a href="/ui/user">Settings</a><br>\n'
2021-04-22 02:50:33 +02:00
r += ' <br>\n'
r += ' <td align="left" valign="top">\n'
return r
def _html_foot(self, user=None):
r = ''
if user:
r += ' </table>\n'
r += ' </body>\n'
r += '</html>\n'
return r
2021-04-30 01:00:29 +02:00
def _arch_select(self, arch=None):
r = ''
r += '<select name="arch">'
for val in [ 'x86_64', 'arm64', 'rpi3' ]:
sel = ' selected="true"' if val == arch else ''
r += "<option value=\"%s\"%s>%s" % (val, sel, val)
r += '</select>'
return r
def _ostype_select(self, ostype=None):
r = ''
r += '<select name="ostype">'
for val in [ 'Linux', 'Windows', 'MacOS' ]:
sel = ' selected="true"' if val == ostype else ''
r += "<option value=\"%s\"%s>%s" % (val, sel, val)
r += '</select>'
return r
2021-04-22 02:50:33 +02:00
def _iso_image_select(self):
r = ''
r += '<select name="iso_image">'
r += '<option value="">'
2021-04-26 06:26:40 +02:00
for row in iso_images_table.select_all():
r += "<option value=\"%d\">%s" % (row['id'], row['name'])
2021-04-22 02:50:33 +02:00
r += '</select>'
return r
def _disk_image_select(self, img_id=None):
r = ''
r += '<select name="disk_image">'
r += '<option value=""></option>'
2021-04-26 06:26:40 +02:00
for row in disk_images_table.select_all():
sel = ' selected="true"' if row['id'] == img_id else ''
r += "<option value=\"%d\"%s>%s" % (row['id'], sel, row['name'])
r += '</select>'
return r
2021-04-22 02:50:33 +02:00
def ui_login(self, user, args):
msg = None
if 'username' in args and 'password' in args:
username = args['username'][0]
2021-04-26 06:26:40 +02:00
row = users_table.select_by_name(username)
if row:
user = User(row)
2021-04-22 02:50:33 +02:00
password = args['password'][0]
2021-04-26 06:26:40 +02:00
if user.auth_password(args['password'][0]):
2021-04-30 07:05:06 +02:00
now = int(time.time())
hash = sha256_string("%s%s%s" % (user['name'], user['pwhash'], now))
expire = now + config['ui.session.duration']
session = {'hash': hash, 'user_id': user['id'], 'expire': expire}
sessions_table.insert(session)
cookie = "session=%s; path=/ui" % session['hash']
2021-04-22 02:50:33 +02:00
self._send_response(302, {'Set-Cookie': cookie, 'Location': '/ui'}, None)
return
msg = 'Login failed'
r = self._html_head()
r += ' <div align="center">\n'
r += ' <form method="POST" action="/ui/login">\n'
r += ' <table align="center">\n'
r += ' <tr><td>&nbsp;</tr>\n'
r += ' <tr><td style="font-size:150%"><b>Login</b></tr>\n'
r += ' <tr><td>&nbsp;</tr>\n'
r += ' <tr><td><b>Username</b></tr>\n'
r += ' <tr><td><input type="text" name="username"/></tr>\n'
r += ' <tr><td><b>Password</b></br>\n'
r += ' <tr><td><input type="password" name="password"/></tr>\n'
r += ' <tr><td align="center"><input type="submit" value="Login"/></tr>\n'
r += ' </table>\n'
r += ' </form>\n'
if msg:
r += " <h2>%s</h2>\n" % (msg)
r += ' </div>\n'
r += self._html_foot()
self._send_response(200, None, r)
def ui_logout(self, user, args):
cookie = 'session=none; path=/ui'
self._send_response(302, {'Set-Cookie': cookie, 'Location': '/ui/login'}, None)
def ui_overview(self, user, args):
2021-04-24 01:52:30 +02:00
total = { 'cpus': 0, 'mem': 0, 'phys_disk': 0, 'virt_disk': 0 }
active = { 'cpus': 0, 'mem': 0, 'phys_disk': 0, 'virt_disk': 0 }
2021-04-26 06:26:40 +02:00
for row in vms_table.select_all():
if row['owner'] != user['name'] and not user.in_group('admin'):
2021-04-22 02:50:33 +02:00
continue
2021-04-26 06:26:40 +02:00
vm = VirtualMachine(row)
total['cpus'] += vm['cpus']
total['mem'] += vm['mem']
if vm.disk_exists():
total['phys_disk'] += vm.disk_physical_size()
total['virt_disk'] += vm.disk_virtual_size()
2021-04-22 02:50:33 +02:00
if vm.running():
2021-04-26 06:26:40 +02:00
active['cpus'] += vm['cpus']
active['mem'] += vm['mem']
if vm.disk_exists():
active['phys_disk'] += vm.disk_physical_size()
active['virt_disk'] += vm.disk_virtual_size()
2021-04-22 02:50:33 +02:00
r = self._html_head(user)
r += ' <p style="font-size:150%">Overview</p>\n'
r += ' <table width="100%">\n'
r += ' <tr><td>&nbsp;<td style="font-weight:bold">Active<td style="font-weight:bold">Total\n'
r += " <tr><td style=\"font-weight:bold\">CPUs<td>%d<td>%d\n" % (active['cpus'], total['cpus'])
r += " <tr><td style=\"font-weight:bold\">Memory<td>%s<td>%s\n" % (readable_size(active['mem'], ONE_MB), readable_size(total['mem'], ONE_MB))
2021-04-24 01:52:30 +02:00
r += " <tr><td style=\"font-weight:bold\">Physical Disk<td>%s<td>%s\n" % (readable_size(active['phys_disk'], ONE_MB), readable_size(total['phys_disk'], ONE_MB))
r += " <tr><td style=\"font-weight:bold\">Virtual Disk<td>%s<td>%s\n" % (readable_size(active['virt_disk'], ONE_MB), readable_size(total['virt_disk'], ONE_MB))
2021-04-22 02:50:33 +02:00
r += ' </table>\n'
r += self._html_foot(user)
self._send_response(200, None, r)
def ui_image(self, user, args):
# XXX: auth
r = self._html_head(user)
err = None
msg = None
is_admin = user.in_group('admin')
img_type = args['type'][0] if 'type' in args else 'disk'
table = iso_images_table if img_type == 'iso' else disk_images_table
2021-04-22 02:50:33 +02:00
if 'id' in args:
args_id = int(args['id'][0])
row = table.select_by_id(args_id)
2021-04-26 06:26:40 +02:00
img = Image(row)
if img['owner'] != user['name'] and not img['public'] and not is_admin:
r += ' <p>Access denied</p>\n'
r += self._html_foot(user)
self._send_response(403, None, r)
return
else:
args_id = None
img = None
# XXX: handle delete better, like user does
if img:
editable = img['owner'] == user['name'] or is_admin
edit_mode = False
2021-04-22 02:50:33 +02:00
if 'action' in args:
if not editable:
err = 'Permission denied'
args['action'][0] = None
if args['action'][0] == 'Edit':
edit_mode = True
2021-04-22 02:50:33 +02:00
if args['action'][0] == 'Save':
if 'name' in args:
2021-04-27 22:35:11 +02:00
img['name'] = args['name'][0]
img['public'] = 1 if 'public' in args else 0
table.update(img)
2021-04-22 02:50:33 +02:00
msg = 'Settings saved'
if args['action'][0] == 'Delete':
2021-04-27 22:35:11 +02:00
table.delete(img)
path = "/ui/image?type=%s" % (img.type())
self._send_response(302, {'Location': path}, None)
2021-04-22 02:50:33 +02:00
return
2021-04-26 06:26:40 +02:00
r += " <p style=\"font-size:150%%\">%s</p>\n" % (img['name'])
2021-04-22 02:50:33 +02:00
if msg:
r += " <p style=\"font-size:125%%\">%s</p>\n" % (msg)
if err:
r += " <p style=\"font-size:125%%;color:red\">%s</p>\n" % (err)
2021-04-22 02:50:33 +02:00
r += ' <form method="POST" action="/ui/image">\n'
2021-04-26 06:26:40 +02:00
r += " <input type=\"hidden\" name=\"type\" value=\"%s\">\n" % (img_type)
r += " <input type=\"hidden\" name=\"id\" value=\"%d\">\n" % (img['id'])
2021-04-22 02:50:33 +02:00
r += ' <table>\n'
if edit_mode:
2021-04-27 22:35:11 +02:00
r += " <tr><td style=\"font-weight:bold\">Name<td><input type=\"text\" name=\"name\" value=\"%s\">\n" % (img['name'])
val = 'checked' if img['public'] else ''
2021-04-22 02:50:33 +02:00
r += " <tr><td style=\"font-weight:bold\">Visibility<td><input type=\"checkbox\" name=\"public\" %s>Public\n" % (val)
r += ' <tr><td><input type="submit" name="action" value="Save"><td>&nbsp;\n'
else:
2021-04-26 06:26:40 +02:00
val = 'Public' if img['public'] else 'Private'
2021-04-22 02:50:33 +02:00
r += " <tr><td style=\"font-weight:bold\">Visibility<td>%s\n" % (val)
if editable:
r += ' <tr><td><input type="submit" name="action" value="Edit"><td>&nbsp;\n'
r += " <tr><td style=\"font-weight:bold\">Virtual Size<td>%s\n" % (readable_size(img.virtual_size(), ONE_MB))
r += " <tr><td style=\"font-weight:bold\">Physical Size<td>%s\n" % (readable_size(img.physical_size(), ONE_MB))
2021-04-22 02:50:33 +02:00
r += ' <tr><td>&nbsp;<td>&nbsp;\n'
pct = acp_progress(img['pathname'])
if pct is None:
if editable:
r += ' <tr><td><input style="color:red" type="submit" name="action" value="Delete"><td>&nbsp;'
else:
r += " <tr><td style=\"font-weight:bold\">Copying<td>%d%%\n" % (pct)
2021-04-22 02:50:33 +02:00
r += ' </table>\n'
r += ' </form>\n'
else:
2021-04-26 06:26:40 +02:00
r += " <p style=\"font-size:150%%\">%s Images</p>\n" % (img_type)
if msg:
r += " <p style=\"font-size:125%%\">%s</p>\n" % (msg)
if err:
r += " <p style=\"font-size:125%%;color:red\">%s</p>\n" % (err)
2021-04-22 02:50:33 +02:00
r += ' <form method="GET" action="/ui/image/create">\n'
2021-04-26 06:26:40 +02:00
r += " <input type=\"hidden\" name=\"type\" value=\"%s\">\n" % (img_type)
2021-04-22 02:50:33 +02:00
r += ' <table width="100%">\n'
r += ' <tr>\n'
r += ' <td><input type="submit" value="Create">\n'
r += ' </table>\n'
r += ' </form>\n'
r += ' <table width="100%">\n'
r += ' <tr style="font-weight:bold"><td>Name<td>Owner<td>Visibility<td>&nbsp;</tr>\n'
idx = -1
cursor = table.select_all() if is_admin else table.select_where("owner='%s' OR public!=0" % (user['name']))
2021-04-26 06:26:40 +02:00
for row in cursor:
img = Image(row)
2021-04-22 02:50:33 +02:00
bgcolor = '#e0e0e0' if (idx % 2) == 0 else 'initial'
idx += 1
r += " <tr style=\"background-color:%s\">" % (bgcolor)
2021-04-26 06:26:40 +02:00
r += "<td><a href=\"/ui/image?type=%s&id=%d\">%s</a>" % (img_type, img['id'], img['name'])
r += "<td>%s" % (img['owner'])
r += "<td>%s" % ('Public' if img['public'] else 'Private')
pct = acp_progress(img['pathname'])
if pct is None:
if img.type() == 'disk':
2021-04-26 06:26:40 +02:00
r += "<td><a href=\"/ui/vm/create?img_id=%d\">Launch</a>" % (img['id'])
2021-04-22 02:50:33 +02:00
else:
r += "<td>&nbsp;"
else:
r += "<td>Creating %d%%" % (pct)
2021-04-22 02:50:33 +02:00
r += '</tr>\n'
r += ' </table>\n'
r += self._html_foot(user)
self._send_response(200, None, r)
def ui_image_create(self, user, args):
r = self._html_head(user)
msg = None
err = None
2021-04-26 06:26:40 +02:00
img_type = args['type'][0]
2021-04-22 02:50:33 +02:00
if 'action' in args and args['action'][0] == 'Create':
img = None
name = args['name'][0]
public = args['public'][0] if 'public' in args else False
try:
if args['image_source'][0] == 'upload_file':
filename = args['upload_file.filename'][0]
data = args['upload_file'][0]
img = Image.create_from_upload(name, filename, data, user, public)
if args['image_source'][0] == 'server_file':
pathname = args['server_file'][0]
if user.may_access_file(pathname):
img = Image.create_from_local(name, pathname, user, public)
else:
err = 'Permission denied'
if args['image_source'][0] == 'remote_url':
url = args['remote_url'][0]
img = Image.create_from_url(name, url, user, public)
except BaseException as e:
err = str(e)
img = None
2021-04-22 02:50:33 +02:00
if img:
2021-04-26 06:26:40 +02:00
if img_type == 'iso':
iso_images_table.insert(img)
else:
disk_images_table.insert(img)
self._send_response(302, {'Location': "/ui/image?type=%s&id=%d" % (img_type, img['id'])}, None)
2021-04-22 02:50:33 +02:00
r += ' <p style="font-size:150%">Create Image</p>\n'
if msg:
r += " <p style=\"font-size:125%%\">%s</p>\n" % (msg)
if err:
r += " <p style=\"font-size:125%%;color:red\">%s</p>\n" % (err)
r += ' <script>\n'
r += ' function imageSourceSelected(sel) {\n'
r += ' var vec = [\n'
r += ' document.getElementById("image_upload_row"),\n'
r += ' document.getElementById("image_local_row"),\n'
r += ' document.getElementById("image_remote_row")\n'
r += ' ];\n'
r += ' for (i = 0; i < 3; i++) {\n'
r += ' if (sel.selectedIndex == i) {\n'
r += ' vec[i].style.display = "table-row";\n'
r += ' }\n'
r += ' else {\n'
r += ' vec[i].style.display = "none";\n'
r += ' }\n'
r += ' }\n'
r += ' }\n'
r += ' </script>\n'
r += ' <form method="POST" action="/ui/image/create" enctype="multipart/form-data">\n'
2021-04-26 06:26:40 +02:00
r += " <input type=\"hidden\" name=\"type\" value=\"%s\">\n" % (img_type)
2021-04-22 02:50:33 +02:00
r += ' <table>\n'
r += ' <tr><td style="font-weight:bold">Name<td><input type="text" name="name">\n'
r += ' <tr><td style="font-weight:bold">Image Source<td><select name="image_source" onChange="imageSourceSelected(this);">'
r += '<option value="upload_file">Upload File</option>'
r += '<option value="server_file">Server File</option>'
r += '<option value="remote_url">Remote URL</option>'
r += '</select>\n'
r += ' <tr style="display:table-row" id="image_upload_row"><td style="font-weight:bold">Upload File<td><input type="file" name="upload_file">\n'
r += ' <tr style="display:none" id="image_local_row"><td style="font-weight:bold">Server File<td><input type="text" name="server_file" size="32">\n'
r += ' <tr style="display:none" id="image_remote_row"><td style="font-weight:bold">Remote URL<td><input type="text" name="remote_url" size="32">\n'
r += ' <tr><td><input type="checkbox" name="public">Public<td>&nbsp;\n'
r += ' <tr><td colspan="2" align="center"><input type="submit" name="action" value="Create">\n'
r += ' </table>\n'
r += ' </form>\n'
r += self._html_foot(user)
self._send_response(200, None, r)
def ui_user(self, user, args):
is_admin = user.in_group('admin')
args_id = None
r = self._html_head(user)
if 'id' in args:
args_id = int(args['id'][0])
if args_id != user['id'] and not is_admin:
2021-04-22 02:50:33 +02:00
r += ' <p>Access denied</p>\n'
r += self._html_foot(user)
self._send_response(403, None, r)
return
row = users_table.select_by_id(args_id)
2021-04-26 06:26:40 +02:00
user = User(row)
2021-04-22 02:50:33 +02:00
msg = None
err = None
token = None
if 'action' in args:
changed = False
if args['action'][0] == 'Save':
if (not is_admin) and ('localuid' in args):
r += ' <p>Access denied</p>\n'
r += self._html_foot(user)
self._send_response(403, None, r)
return
if 'fullname' in args:
user.fullname(args['fullname'][0])
changed = True
if 'oldpass' in args and 'newpass1' in args and 'newpass2' in args:
if not err and not user.auth_password(args['oldpass'][0]):
err = 'Old password is incorrect'
if not err and args['newpass1'][0] != args['newpass2'][0]:
err = 'New passwords do not match'
if not err:
user.password(args['newpass1'][0])
changed = True
if 'email' in args:
user.email(args['email'][0])
changed = True
if 'timezone' in args:
user.timezone(args['timezone'][0])
changed = True
if 'localuid' in args:
user.localuid(int(args['localuid'][0]))
changed = True
if args['action'][0] == 'Delete':
if not is_admin:
r += ' <p>Access denied</p>\n'
r += self._html_foot(user)
self._send_response(403, None, r)
return
user_db.remove(user)
msg = 'User deleted'
args_id = None
if args['action'][0] == 'Generate':
2021-04-30 07:05:06 +02:00
now = int(time.time())
token = sha256_string("%s%s%s" % (user['name'], user['pwhash'], now))
2021-04-26 06:26:40 +02:00
user['token'] = token
2021-04-22 02:50:33 +02:00
changed = True
if changed:
2021-04-26 06:26:40 +02:00
users_table.update(user)
2021-04-22 02:50:33 +02:00
msg = 'Settings saved'
r += ' <p style="font-size:150%">Settings</p>\n'
if msg:
r += " <p style=\"font-size:125%%\">%s</p>\n" % (msg)
if err:
r += " <p style=\"font-size:125%%;color:red\">%s</p>\n" % (err)
if is_admin and not args_id:
r += ' <p style="font-size:125%">Users</p>\n'
r += ' <form method="GET" action="/ui/user/create">\n'
r += ' <table width="100%">\n'
r += ' <tr>\n'
r += ' <td><input type="submit" value="Create">\n'
r += ' </table>\n'
r += ' </form>\n'
r += ' <table width="100%">\n'
r += ' <tr><td>Name<td>Full name<td>&nbsp;\n'
2021-04-26 06:26:40 +02:00
for row in users_table.select_all():
user = User(row)
r += " <tr><td><a href=\"/ui/user?id=%d\">%s</a><td>%s" % (user['id'], user['name'], user['fullname'])
2021-04-22 02:50:33 +02:00
r += '<td><form method="POST" action="/ui/user">'
r += "<input type=\"hidden\" name=\"id\" value=\"%d\">" % (user['id'])
2021-04-22 02:50:33 +02:00
r += '<input type="submit" name="action" value="Delete">\n'
r += '</form>\n'
r += ' </table>\n'
else:
r += ' <form method="POST" action="/ui/user">\n'
r += " <input type=\"hidden\" name=\"id\" value=%d>\n" % (user['id'])
2021-04-22 02:50:33 +02:00
r += ' <table width="100%">\n'
r += ' <tr><td style="font-size:80%;font-weight:bold">Full name<td>&nbsp;\n'
2021-04-26 06:26:40 +02:00
r += " <tr><td><input type=\"text\" name=\"fullname\" value=\"%s\"><td>&nbsp;\n" % (user['fullname'])
2021-04-22 02:50:33 +02:00
r += ' <tr><td colspan="2">&nbsp;\n'
r += ' <tr><td style="font-size:80%;font-weight:bold">Password<td>&nbsp\n'
r += ' <tr><td><input type="password" name="oldpass">&nbsp;[old]<td>&nbsp;\n'
r += ' <tr><td><input type="password" name="newpass1">&nbsp;[new]<td>&nbsp;\n'
r += ' <tr><td><input type="password" name="newpass2">&nbsp;[confirm]<td>&nbsp;\n'
r += ' <tr><td colspan="2">&nbsp;\n'
r += ' <tr><td style="font-size:80%;font-weight:bold">Email<td>&nbsp\n'
2021-04-26 06:26:40 +02:00
r += " <tr><td><input type=\"text\" name=\"email\" value=\"%s\"><td>&nbsp;\n" % (user['email'])
2021-04-22 02:50:33 +02:00
if is_admin:
r += ' <tr><td style="font-size:80%;font-weight:bold">Local UID<td>&nbsp\n'
2021-04-26 06:26:40 +02:00
r += " <tr><td><input type=\"number\" name=\"localuid\" value=\"%s\"<td>&nbsp;\n" % (user['localuid'])
2021-04-22 02:50:33 +02:00
r += ' <tr><td colspan="2">&nbsp;\n'
# r += ' <tr><td style="font-size:80%;font-weight:bold">Time zone<td>&nbsp;\n'
# r += " <tr><td><input type=\"text\" name=\"timezone\" value=\"%s\"><td>&nbsp;\n" % (user.timezone())
r += ' <tr><td><input type="submit" name="action" value="Save"><td>&nbsp;\n'
r += ' <tr><td colspan="2">&nbsp;\n'
r += " <tr><td>API token: %s<td>&nbsp;\n" % (token if token else "[hidden]")
r += ' <tr><td><input type="submit" name="action" value="Generate"><td>&nbsp;\n'
r += ' </table>\n'
r += ' </form>\n'
r += self._html_foot(user)
self._send_response(200, None, r)
def ui_user_create(self, user, args):
r = self._html_head(user)
if not user.in_group('admin'):
r += ' Access denied'
r += self._html_foot(user)
self._send_response(403, None, r)
return
msg = None
err = None
if 'action' in args and args['action'][0] == 'Add':
name = args['name'][0]
fullname = args['fullname'][0]
password = args['password'][0]
user = User.create(name, fullname, password, [])
2021-04-26 06:26:40 +02:00
users_table.insert(user)
2021-04-22 02:50:33 +02:00
msg = 'User created'
r += ' <p style="font-size:150%">Add user</p>\n'
if msg:
r += " <p style=\"font-size:125%%\">%s</p>\n" % (msg)
if err:
r += " <p style=\"font-size:125%%;color:red\">%s</p>\n" % (err)
r += ' <form method="POST" action="/ui/user/create">\n'
r += ' <p style="font-size:80%;font-weight:bold">User name</p>\n'
r += ' <input type="text" name="name"><br>\n'
r += ' <p style="font-size:80%;font-weight:bold">Full name</p>\n'
r += ' <input type="text" name="fullname"><br>\n'
r += ' <p style="font-size:80%;font-weight:bold">Password</p>\n'
r += ' <input type="password" name="password"><br>\n'
r += ' <br>\n'
r += ' <input type="submit" name="action" value="Add"><br>\n'
r += ' </form>\n'
r += self._html_foot(user)
self._send_response(200, None, r)
# list, delete, start/reset/poweroff/kill on main page
# start/reset/poweroff/kill, insert/eject on detail page
def ui_vm(self, user, args):
# XXX: auth
r = self._html_head(user)
2021-04-30 22:11:22 +02:00
err = None
msg = None
is_admin = user.in_group('admin')
2021-04-22 02:50:33 +02:00
if 'id' in args:
2021-04-30 22:11:22 +02:00
args_id = int(args['id'][0])
row = vms_table.select_by_id(args_id)
vm = VirtualMachine(row)
if vm['owner'] != user['name'] and not is_admin:
r += ' <p>Access denied</p>\n'
r += self._html_foot(user)
self._send_response(403, None, r)
return
else:
args_id = None
vm = None
if vm:
2021-04-22 02:50:33 +02:00
server_host = self.headers['Host']
if ':' in server_host:
server_host = server_host.split(':')[0]
2021-04-29 01:34:38 +02:00
vm_running = vm.running()
edit_mode = (not vm_running) and ('action' in args) and (args['action'][0] == 'Edit')
2021-04-22 02:50:33 +02:00
if 'action' in args:
if args['action'][0] == 'Save':
if 'name' in args:
vm['name'] = args['name'][0]
2021-04-30 01:00:29 +02:00
if 'arch' in args:
vm['arch'] = args['arch'][0]
2021-04-22 02:50:33 +02:00
if 'cpus' in args:
vm['cpus'] = int(args['cpus'][0])
2021-04-22 02:50:33 +02:00
if 'mem' in args:
mem = parse_num(args['mem'][0])
if mem >= ONE_MB:
mem /= ONE_MB
vm['mem'] = mem
if 'ostype' in args:
vm['ostype'] = args['ostype'][0]
if 'vncpass' in args:
vm['vncpass'] = args['vncpass'][0]
vms_table.update(vm)
2021-04-22 02:50:33 +02:00
msg = 'Settings saved'
if args['action'][0] == 'Start':
ro = 'readonly' in args
vm.start(readonly=ro)
if args['action'][0] == 'Suspend':
vm.suspend()
if args['action'][0] == 'Power Off':
vm.poweroff()
if args['action'][0] == 'Kill':
vm.kill()
if args['action'][0] == 'Insert':
id = int(args['iso_image'][0])
row = iso_images_table.select_by_id(id)
vm.iso_insert(row['pathname'])
2021-04-28 23:53:28 +02:00
vms_table.update(vm)
2021-04-22 02:50:33 +02:00
if args['action'][0] == 'Eject':
vm.iso_eject()
2021-04-28 23:53:28 +02:00
vms_table.update(vm)
2021-04-22 02:50:33 +02:00
if args['action'][0] == 'Delete':
2021-04-29 01:34:38 +02:00
if not vm_running:
if vm['diskpath'].startswith(config['disk.storage.location']):
file_delete(vm['diskpath'])
2021-04-26 06:26:40 +02:00
vms_table.delete(vm)
2021-04-22 02:50:33 +02:00
self._send_response(302, {'Location': '/ui/vm'}, None)
else:
err = 'Cannot delete: machine is running'
if args['action'][0] == 'Create Image':
name = args['image_name'][0]
public = args['image_public'][0]
img = Image.create_from_vmdisk(name, vm, user, public)
2021-04-29 02:10:57 +02:00
disk_images_table.insert(img)
2021-04-29 01:34:38 +02:00
vm_running = vm.running()
edit_mode = (not vm_running) and ('action' in args) and (args['action'][0] == 'Edit')
2021-04-26 06:26:40 +02:00
r += " <p style=\"font-size:150%%\">%s</p>\n" % (vm['name'])
2021-04-22 02:50:33 +02:00
if msg:
r += " <p style=\"font-size:125%%\">%s</p>\n" % (msg)
if err:
r += " <p style=\"font-size:125%%;color:red\">%s</p>\n" % (err)
2021-04-22 02:50:33 +02:00
r += ' <form method="POST" action="/ui/vm">\n'
2021-04-30 22:11:22 +02:00
r += " <input type=\"hidden\" name=\"id\" value=\"%d\">\n" % (args_id)
2021-04-22 02:50:33 +02:00
r += ' <table>\n'
if edit_mode:
2021-04-26 06:26:40 +02:00
r += " <tr><td style=\"font-weight:bold\">Name<td><input type=\"text\" name=\"name\" value=\"%s\">\n" % (vm['name'])
2021-04-30 01:00:29 +02:00
r += " <tr><td style=\"font-weight:bold\">Arch<td>%s\n" % (self._arch_select(vm['arch']))
2021-04-26 06:26:40 +02:00
r += " <tr><td style=\"font-weight:bold\">CPUs<td><input type=\"number\" name=\"cpus\" value=\"%d\" size=\"6\">\n" % (vm['cpus'])
r += " <tr><td style=\"font-weight:bold\">Mem<td><input type=\"text\" name=\"mem\" value=\"%s\" size=\"6\">\n" % (readable_size(vm['mem'], ONE_MB))
r += ' <tr><td style="font-weight:bold">OS Type<td>%s\n' % (self._ostype_select(vm['ostype']))
r += " <tr><td style=\"font-weight:bold\">VNC Pass<td><input type=\"text\" name=\"vncpass\" value=\"%s\" size=\"8\">\n" % (vm['vncpass'])
2021-04-22 02:50:33 +02:00
r += ' <tr><td><input type="submit" name="action" value="Save"><td>&nbsp;\n'
else:
2021-04-30 01:00:29 +02:00
r += " <tr><td style=\"font-weight:bold\">Arch<td>%s\n" % (vm['arch'])
2021-04-26 06:26:40 +02:00
r += " <tr><td style=\"font-weight:bold\">CPUs<td>%d\n" % (vm['cpus'])
r += " <tr><td style=\"font-weight:bold\">Mem<td>%s\n" % (readable_size(vm['mem'], ONE_MB))
r += " <tr><td style=\"font-weight:bold\">OS type<td>%s\n" % (vm['ostype'])
2021-04-29 01:34:38 +02:00
if not vm_running:
2021-04-22 02:50:33 +02:00
r += ' <tr><td><input type="submit" name="action" value="Edit"><td>&nbsp;\n'
2021-04-24 01:52:30 +02:00
r += " <tr><td style=\"font-weight:bold\">Disk<td>%s\n" % (readable_size(vm.disk_virtual_size(), ONE_MB))
2021-04-22 02:50:33 +02:00
r += " <tr><td style=\"font-weight:bold\">State<td>%s\n" % (vm.state())
2021-04-26 06:26:40 +02:00
r += " <tr><td style=\"font-weight:bold\">MAC<td>%s\n" % (vm['macaddr'])
2021-04-22 02:50:33 +02:00
r += " <tr><td style=\"font-weight:bold\">Addr<td>%s\n" % (vm.ipv4addr())
r += ' <tr><td>&nbsp;<td>&nbsp;\n'
2021-04-29 01:34:38 +02:00
if vm_running:
2021-04-30 22:11:22 +02:00
r += " <tr><td style=\"font-weight:bold\">VNC Host<td>%s:%d\n" % (server_host, args_id)
2021-04-26 06:26:40 +02:00
r += " <tr><td style=\"font-weight:bold\">VNC Pass<td>%s\n" % (vm['vncpass'])
2021-04-22 02:50:33 +02:00
r += ' <tr><td>&nbsp;<td>&nbsp;\n'
r += ' </table>\n'
r += ' <table>\n'
r += ' <tr><td style="font-weight:bold">ISO'
2021-04-28 23:53:28 +02:00
if vm['isopath']:
row = iso_images_table.select_one_where("pathname='%s'" % (vm['isopath']))
r += "<td>%s<input style=\"float:right\" type=\"submit\" name=\"action\" value=\"Eject\">\n" % (row['name'])
2021-04-22 02:50:33 +02:00
else:
r += "<td>%s<input type=\"submit\" name=\"action\" value=\"Insert\">\n" % (self._iso_image_select())
r += ' <tr><td>&nbsp;<td>&nbsp;\n'
pct = acp_progress(vm['diskpath'])
2021-04-29 01:34:38 +02:00
if vm_running:
2021-04-22 02:50:33 +02:00
r += ' <tr><td colspan="2"><input type="submit" name="action" value="Suspend">\n'
r += ' <tr><td colspan="2"><input type="submit" name="action" value="Power Off">\n'
r += ' <tr><td colspan="2"><input type="submit" name="action" value="Kill">\n'
elif not pct is None:
r += " <tr><td style=\"font-weight:bold\">Copying<td>%d%%\n" % (pct)
2021-04-22 02:50:33 +02:00
else:
r += ' <tr><td colspan="2"><input type="submit" name="action" value="Start">'
2021-04-26 06:26:40 +02:00
# XXX: vm.disktype()
if vm['diskpath'].endswith('.vmdk'):
2021-04-22 02:50:33 +02:00
r += '<input type="checkbox" name="readonly" disabled checked>Read only mode\n'
else:
r += '<input type="checkbox" name="readonly">Read only mode\n'
r += ' <tr><td colspan="2"><input style="color:red" type="submit" name="action" value="Delete">\n'
r += ' <tr><td colspan="2"><hr>\n'
2021-05-13 20:18:28 +02:00
r += ' <tr><td colspan="2" style="font-size:125%">Create image from disk\n'
2021-04-22 02:50:33 +02:00
r += ' <tr><td style="font-weight:bold">Name<td><input type="text" name="image_name">\n'
r += ' <tr><td>&nbsp;<td><input type="checkbox" name="image_public">Public\n'
r += ' <tr><td colspan="2"><input type="submit" name="action" value="Create Image">\n'
r += '\n'
r += ' </table>\n'
r += ' </form>\n'
else:
r += ' <p style="font-size:150%">Virtual Machines</p>\n'
r += ' <form method="GET" action="/ui/vm/create">\n'
r += ' <table width="100%">\n'
r += ' <tr>\n'
r += ' <td><input type="submit" value="Create">\n'
r += ' </table>\n'
r += ' </form>\n'
r += ' <table with="100%">\n'
r += ' <tr style="font-weight:bold"><td>Name<td>State<td>Address</tr>\n'
idx = -1
2021-04-26 06:26:40 +02:00
if user.in_group('admin'):
cursor = vms_table.select_all()
else:
cursor = vms_table.select_where("owner=\"%s\"" % (user['name']))
for row in cursor:
vm = VirtualMachine(row)
2021-04-22 02:50:33 +02:00
idx += 1
bgcolor = '#e0e0e0' if (idx % 2) == 0 else 'initial'
addr = '&nbsp;'
2021-04-26 06:26:40 +02:00
# XXX: use vm_running for this
2021-04-22 02:50:33 +02:00
if vm.running():
addr = vm.ipv4addr()
r += " <tr style=\"background-color:%s\">" % (bgcolor)
r += "<td><a href=\"/ui/vm?id=%d\">%s</a>" % (vm['id'], vm['name'])
2021-04-22 02:50:33 +02:00
r += "<td>%s" % (vm.state())
r += "<td>%s" % (addr)
# XXX: create-time (age), on-time (uptime)
r += '</tr>\n'
r += ' </table>\n'
r += self._html_foot(user)
self._send_response(200, None, r)
def ui_vm_create(self, user, args):
r = self._html_head(user)
msg = None
err = None
if 'action' in args and args['action'][0] == 'Create':
vm = None
name = args['name'][0]
arch = args['arch'][0]
cpus = int(args['cpus'][0])
if cpus < 1 or cpus > 8:
err = 'Invalid CPUs'
mem = parse_num(args['mem'][0])
if mem >= ONE_MB:
mem /= ONE_MB
if mem < 1 or mem > 16*1024:
err = 'Invalid mem'
try:
if args['disk_source'][0] == 'create_new':
disk_size = parse_num(args['disk_size'][0])
if disk_size >= ONE_MB:
disk_size /= ONE_MB
if disk_size < 1 or disk_size > 256*1024:
err = 'Invalid disk size'
vm = VirtualMachine.create_new(name, user, arch, cpus, mem, disk_size)
if args['disk_source'][0] == 'use_image':
image_id = int(args['disk_image'][0])
vm = VirtualMachine.create_from_image(name, user, arch, cpus, mem, image_id)
if args['disk_source'][0] == 'upload_file':
filename = args['upload_file.filename'][0]
data = args['upload_file'][0]
vm = VirtualMachine.create_from_upload(name, user, arch, cpus, mem, filename, data)
if args['disk_source'][0] == 'server_file':
pathname = args['server_file'][0]
if user.may_access_file(pathname):
vm = VirtualMachine.create_from_local(name, user, arch, cpus, mem. pathname)
else:
err = 'Permission denied'
if args['disk_source'][0] == 'remote_url':
image_url = args['remote_url'][0]
vm = VirtualMachine.create_from_url(name, user, arch, cpus, mem, image_url)
except BaseException as e:
err = str(e)
vm = None
if vm:
vms_table.insert(vm)
self._send_response(302, {'Location': "/ui/vm?id=%d" % (vm['id'])}, None)
2021-04-22 02:50:33 +02:00
img_id = int(args['img_id'][0]) if 'img_id' in args else None
r += ' <p style="font-size:150%">Create VM</p>\n'
if msg:
r += " <p style=\"font-size:125%%\">%s</p>\n" % (msg)
if err:
r += " <p style=\"font-size:125%%;color:red\">%s</p>\n" % (err)
r += ' <script>\n'
r += ' function diskSourceSelected(sel) {\n'
r += ' var vec = [\n'
r += ' document.getElementById("disk_size_row"),\n'
r += ' document.getElementById("disk_image_row"),\n'
r += ' document.getElementById("disk_upload_row"),\n'
r += ' document.getElementById("disk_local_row"),\n'
r += ' document.getElementById("disk_remote_row")\n'
r += ' ];\n'
r += ' for (i = 0; i < 5; i++) {\n'
r += ' if (sel.selectedIndex == i) {\n'
r += ' vec[i].style.display = "table-row";\n'
r += ' }\n'
r += ' else {\n'
r += ' vec[i].style.display = "none";\n'
r += ' }\n'
r += ' }\n'
r += ' }\n'
r += ' </script>\n'
r += ' <form method="POST" action="/ui/vm/create">\n'
r += ' <table>\n'
r += ' <input type="hidden" name="arch" value="x86_64">\n'
r += ' <tr><td style="font-weight:bold">Name<td><input type="text" name="name">\n'
2021-04-30 01:00:29 +02:00
r += " <tr><td style=\"font-weight:bold\">Arch<td>%s\n" % (self._arch_select())
2021-04-22 02:50:33 +02:00
r += ' <tr><td style="font-weight:bold">CPUs<td><input type="number" name="cpus" size="4">\n'
r += ' <tr><td style="font-weight:bold">Memory<td><input type="text" name="mem" size="8">\n'
r += " <tr><td style=\"font-weight:bold\">OS Type<td>%s\n" % (self._ostype_select())
2021-04-22 02:50:33 +02:00
r += ' <tr><td style="font-weight:bold">Disk Source<td><select name="disk_source" onChange="diskSourceSelected(this);">'
r += '<option value="create_new">Create New</option>'
if img_id:
r += '<option value="use_image" selected="true">Use Image</option>'
else:
r += '<option value="use_image">Use Image</option>'
r += '<option value="upload_file">Upload File</option>'
r += '<option value="server_file">Server File</option>'
r += '<option value="remote_url">Remote URL</option>'
r += '</select>\n'
if img_id:
r += ' <tr style="display:none" id="disk_size_row"><td style="font-weight:bold">Disk Size<td><input type="text" name="disk_size" size="8">\n'
r += " <tr style=\"display:table-row\" id=\"disk_image_row\"><td style=\"font-weight:bold\">Disk Image<td>%s\n" % (self._disk_image_select(img_id))
else:
r += ' <tr style="display:table-row" id="disk_size_row"><td style="font-weight:bold">Disk Size<td><input type="text" name="disk_size" size="8">\n'
r += " <tr style=\"display:none\" id=\"disk_image_row\"><td style=\"font-weight:bold\">Disk Image<td>%s\n" % (self._disk_image_select(img_id))
r += ' <tr style="display:none" id="disk_upload_row"><td style="font-weight:bold">Upload File<td><input type="file" name="upload_file">\n'
r += ' <tr style="display:none" id="disk_local_row"><td style="font-weight:bold">Server File<td><input type="text" name="server_file" size="32">\n'
r += ' <tr style="display:none" id="disk_remote_row"><td style="font-weight:bold">Remote URL<td><input type="text" name="remote_url" size="32">\n'
r += ' <tr><td colspan="2" align="center"><input type="submit" name="action" value="Create">\n'
r += ' </table>\n'
r += ' </form>\n'
r += self._html_foot(user)
self._send_response(200, None, r)
def _handle_request(self):
try:
path = self.path
args = {}
if self.command == 'GET':
if '?' in self.path:
(path, query_string) = self.path.split('?', 1)
args = urllib.parse.parse_qs(query_string)
if self.command == 'POST':
content_type = self.headers['Content-Type']
content_len = int(self.headers['Content-Length'])
content = self.rfile.read(content_len)
if content_type == 'application/x-www-form-urlencoded':
args = urllib.parse.parse_qs(content.decode())
if content_type == 'application/json':
args = json.loads(content.decode())
if content_type.startswith('multipart/form-data'):
hdr_str = "Mime-Version: 1.0\r\nContent-Type: %s\r\n\r\n" % (content_type)
parser = email.parser.BytesParser(policy=email.policy.default)
msg = parser.parsebytes(hdr_str.encode() + content)
args = {}
for part in msg.walk():
disp = part.get('Content-Disposition', '')
if not disp.startswith('form-data'):
continue
formdata = {}
for field in disp.split(';'):
if not '=' in field:
continue
(k,v) = field.strip().split('=', 1)
v = v.strip('"')
formdata[k] = v
input_name = formdata['name']
if not input_name in args:
args[input_name] = []
args[input_name].append(part.get_content())
if 'filename' in formdata:
args["%s.filename" % (input_name)] = [formdata['filename']]
user = None
if 'Authorization' in self.headers:
fields = self.headers['Authorization'].split()
if len(fields) == 2 and fields[0] == 'Bearer':
token = fields[1]
2021-04-26 06:26:40 +02:00
row = user_table.select_one("token='%s'" % (token))
user = User(row)
2021-04-22 02:50:33 +02:00
if 'Cookie' in self.headers:
for kvp in self.headers['Cookie'].split(';'):
(k, v) = kvp.strip().split('=', 1)
if k == 'session':
2021-04-30 07:05:06 +02:00
try:
row = sessions_table.select_one_where("hash='%s'" % (v))
session = DbObject(row)
row = users_table.select_by_id(session['user_id'])
2021-04-30 07:05:06 +02:00
user = User(row)
now = int(time.time())
session['expire'] = now + config['ui.session.duration']
sessions_table.update(session)
except RuntimeError as e:
pass
2021-04-22 02:50:33 +02:00
if not user and path != '/ui/login':
self._send_response(302, {'Location': '/ui/login'}, None)
return
if not path in self._dispatch_table:
r = self._html_head(user)
r += ' Page not found\n'
r += self._html_foot(user)
self._send_response(404, None, r)
return
self._dispatch_table[path](user, args)
except BaseException as e:
# XXX: send json for api requests
r = self._html_head()
r += ' <p>Exception handling request</p>\n'
r += " <pre>%s</pre>\n" % (e)
if log_debug: # XXX: opts['debug']
r += ' <p>Backtrace</p>\n'
r += ' <pre>\n'
for line in traceback.format_tb(e.__traceback__):
r += "%s\n" % (line)
r += ' </pre>\n'
r += self._html_foot()
self._send_response(500, None, r)
def do_GET(self):
self._handle_request()
def do_POST(self):
self._handle_request()
def raw_http_listener():
listen_addr = (config['http.listen.address'], config['http.listen.port'])
if sys.hexversion >= 0x030700f0:
server = http.server.ThreadingHTTPServer(listen_addr, HttpClientRequestHandler)
else:
logw("HTTP server threading disabled because Python version is less than 3.7")
server = http.server.HTTPServer(listen_addr, HttpClientRequestHandler)
server.cookies = dict()
server.serve_forever()
def ssl_http_listener():
listen_addr = (config['http.listen.ssladdress'], config['http.listen.sslport'])
if sys.hexversion >= 0x030700f0:
server = http.server.ThreadingHTTPServer(listen_addr, HttpClientRequestHandler)
else:
logw("HTTPS server threading disabled because Python version is less than 3.7")
server = http.server.HTTPServer(listen_addr, HttpClientRequestHandler)
server.cookies = dict()
server.socket = ssl.wrap_socket(server.socket, certfile=config['ssl.certfile'], server_side=True)
server.serve_forever()
### File copier ###
_acp_lock = threading.Lock()
_acp_queue = []
_acp_map = {}
_acp_src = None
_acp_dst = None
_acp_pct = None
def acp_queue(src, dst):
_acp_lock.acquire()
_acp_queue.append(dst)
_acp_map[dst] = src
_acp_lock.release()
def acp_progress(dst):
pct = None
_acp_lock.acquire()
if dst in _acp_map:
pct = 0
if _acp_dst == dst:
pct = _acp_pct
_acp_lock.release()
return pct
def acp_simple(srcfile, dstfile, file_size):
global _acp_pct
2021-04-22 02:50:33 +02:00
off = 0
pct = 0
2021-04-22 02:50:33 +02:00
while off < file_size:
chunk = min(1024*1024, file_size - off)
buf = srcfile.read(chunk)
if len(buf) == 0:
raise RuntimeError("Failed to read")
dstfile.write(buf)
off += len(buf)
curpct = int(100 * off / file_size)
if curpct != pct:
pct = curpct
_acp_lock.acquire()
_acp_pct = pct
_acp_lock.release()
2021-04-22 02:50:33 +02:00
# XXX: could provide better status with disk blocks and copied size
def acp_sparse(srcfile, dstfile, file_size):
global _acp_pct
2021-04-22 02:50:33 +02:00
off = 0
pct = 0
2021-04-22 02:50:33 +02:00
while off < file_size:
try:
off = srcfile.seek(off, os.SEEK_DATA)
except OSError as e:
if e.errno == errno.ENXIO:
dstfile.truncate(file_size)
return
raise
2021-04-22 02:50:33 +02:00
end = srcfile.seek(off, os.SEEK_HOLE)
srcfile.seek(off)
dstfile.seek(off)
while off < end:
chunk = min(1024*1024, end - off)
buf = srcfile.read(chunk)
if len(buf) == 0:
raise RuntimeError("Failed to read")
dstfile.write(buf)
off += len(buf)
curpct = int(100 * off / file_size)
if curpct != pct:
pct = curpct
_acp_lock.acquire()
_acp_pct = pct
_acp_lock.release()
2021-04-22 02:50:33 +02:00
def file_copier():
global _acp_src
global _acp_dst
global _acp_pct
2021-04-30 07:05:06 +02:00
while running:
2021-04-22 02:50:33 +02:00
try:
src = None
_acp_lock.acquire()
if _acp_queue:
dst = _acp_queue.pop(0)
src = _acp_map[dst]
_acp_lock.release()
if src:
_acp_src = src
_acp_dst = dst
_acp_pct = 0
2021-04-26 06:26:40 +02:00
if src.startswith('/'):
srcfile = open(src, 'rb')
dstfile = open(dst, 'wb')
size = os.stat(src).st_size
acp_sparse(srcfile, dstfile, size)
2021-04-22 02:50:33 +02:00
else:
2021-04-26 06:26:40 +02:00
srcfile = urllib.request.urlopen(src)
dstfile = open(dst, 'wb')
2021-04-22 02:50:33 +02:00
size = int(srcfile.headers.get('Content-Length', '0'))
acp_simple(srcfile, dstfile, size)
_acp_src = None
_acp_dst = None
_acp_lock.acquire()
del _acp_map[dst]
_acp_lock.release()
2021-04-22 02:50:33 +02:00
except BaseException as e:
loge("Exception in file_copy thread: %s" % (e))
time.sleep(1)
### Lease updater ###
def lease_updater():
2021-04-26 06:26:40 +02:00
global leases
2021-04-22 02:50:33 +02:00
leases_pathname = "%s/dnsmasq.leases" % (state_dir)
last_mtime = 0
2021-04-30 07:05:06 +02:00
while running:
2021-04-22 02:50:33 +02:00
try:
cur_mtime = os.stat(leases_pathname)
if cur_mtime != last_mtime:
2021-04-26 06:26:40 +02:00
new_leases = {}
2021-04-22 02:50:33 +02:00
last_mtime = cur_mtime
f = open(leases_pathname, 'r')
for line in f:
fields = line.split()
2021-04-26 06:26:40 +02:00
macaddr = fields[1]
ipv4addr = fields[2]
name = fields[3]
new_leases[macaddr] = ipv4addr
2021-04-22 02:50:33 +02:00
f.close()
2021-04-26 06:26:40 +02:00
leases = new_leases
2021-04-22 02:50:33 +02:00
except OSError as e:
last_mtime = 0
except BaseException as e:
loge("Exception in lease_updater thread: %s" % (e))
time.sleep(1)
2021-04-30 07:05:06 +02:00
### Session expirer ###
def session_expirer():
while running:
now = int(time.time())
sessions_table.delete_where("expire < %d" % (now))
time.sleep(60)
2021-04-22 02:50:33 +02:00
### Main ###
config_defaults = {
'http.listen.address': '127.0.0.1',
'http.listen.port': 8080,
'ui.imagepath': 'https://www.nwwn.com/vmm/images',
2021-04-30 07:05:06 +02:00
'ui.session.duration': 3600,
2021-04-22 02:50:33 +02:00
'iso.storage.location': '/var/lib/vmm/isos',
'image.storage.location': '/var/lib/vmm/images',
'disk.storage.type': 'dir',
2021-04-26 06:26:40 +02:00
'disk.storage.location': '/var/lib/vmm/disks',
2021-04-22 02:50:33 +02:00
'disk.storage.name': '%u.%n',
'network.mode': 'manual',
'network.bridge.name': 'br0',
'network.bridge.mode': 'subnet',
'network.bridge.addr': '172.16.1.1/24',
'network.dhcp.start': '172.16.1.2',
'network.dhcp.end': '172.16.1.254',
2021-04-22 18:23:56 +02:00
'network.dns.server': '1.1.1.1',
2021-04-22 02:50:33 +02:00
'network.proxy_arp_dev': 'eth0',
'vm.nesting': True,
'vm.max_cpu': '',
'vm.max_mem': '',
'vm.max_disk': '',
'user.max_cpu': '',
'user.max_mem': '',
'user.max_disk': ''
}
config = Config('/etc/vmm/config', config_defaults)
global_opts = 'dv'
global_longopts = ['debug', 'verbose']
optargs, argv = getopt.getopt(sys.argv[1:], global_opts, global_longopts)
for k,v in optargs:
if k in ('-d', '--debug'):
log_debug = True
if k in ('-v', '--verbose'):
log_verbose += 1
mkdir_p(state_dir)
mkdir_p(run_dir)
mkdir_p(config['iso.storage.location'])
mkdir_p(config['image.storage.location'])
mkdir_p(config['disk.storage.location'])
2021-04-26 06:26:40 +02:00
# Initialize database
dbfile = "%s/vmm.sqlite" % (state_dir)
dbconn = sqlite3.connect(dbfile, check_same_thread=False)
os.chmod(dbfile, 0o0600)
dbconn.isolation_level = None
dbconn.row_factory = sqlite3.Row
users_table = DbTable(dbconn, 'users',
"""id INTEGER PRIMARY KEY,
name VARCHAR(64),
fullname VARCHAR(64),
pwhash CHAR(64),
groups VARCHAR(256),
localuid INTEGER,
email VARCHAR(256),
timezone INTEGER,
token CHAR(64)""")
iso_images_table = DbTable(dbconn, 'iso_images',
"""id INTEGER PRIMARY KEY,
owner VARCHAR(64),
name VARCHAR(256),
pathname VARCHAR(256),
public INTEGER""")
disk_images_table = DbTable(dbconn, 'disk_images',
"""id INTEGER PRIMARY KEY,
owner VARCHAR(64),
name VARCHAR(256),
pathname VARCHAR(256),
public INTEGER""")
vms_table = DbTable(dbconn, 'vms',
"""id INTEGER PRIMARY KEY,
owner VARCHAR(64),
name VARCHAR(256),
arch VARCHAR(64),
cpus INTEGER,
mem INTEGER,
ostype VARCHAR(64),
2021-04-26 06:26:40 +02:00
vncpass CHAR(8),
2021-05-13 20:17:27 +02:00
uuid CHAR(36),
2021-04-26 06:26:40 +02:00
macaddr CHAR(17),
disksize INTEGER,
2021-04-28 23:53:28 +02:00
diskpath VARCHAR(256),
isopath VARCHAR(256)""")
2021-04-26 06:26:40 +02:00
2021-04-30 07:05:06 +02:00
sessions_table = DbTable(dbconn, 'sessions',
"""id INTEGER PRIMARY KEY,
hash CHAR(64),
user_id INTEGER,
expire INTEGER""")
2021-04-26 06:26:40 +02:00
if users_table.empty():
root = User.create('root', 'Root User', 'root', 'admin')
users_table.insert(root)
2021-04-22 02:50:33 +02:00
else:
2021-04-26 06:26:40 +02:00
root = users_table.select_by_name('root')
if iso_images_table.empty():
2021-04-22 02:50:33 +02:00
for filename in os.listdir(config['iso.storage.location']):
pathname = "%s/%s" % (config['iso.storage.location'], filename)
(name, ext) = os.path.splitext(filename)
if not ext in ['.iso']:
continue
2021-04-26 06:26:40 +02:00
img = Image.create_from_local(name, pathname, root, 1)
iso_images_table.insert(img)
# XXX: else check removed
if disk_images_table.empty():
2021-04-22 02:50:33 +02:00
for filename in os.listdir(config['image.storage.location']):
pathname = "%s/%s" % (config['image.storage.location'], filename)
(name, ext) = os.path.splitext(filename)
if not ext in ['.vmdk', '.qcow2']:
continue
2021-04-26 06:26:40 +02:00
img = Image.create_from_local(name, pathname, root, 1)
disk_images_table.insert(img)
# XXX: else check removed
if vms_table.empty():
2021-04-22 02:50:33 +02:00
# XXX: handle LVM
for filename in os.listdir(config['disk.storage.location']):
pathname = "%s/%s" % (config['disk.storage.location'], filename)
fields = filename.split('.') # XXX: parse better: username.vmname.ext
if len(fields) != 3:
continue
if not fields[2] in ['qcow2', 'vmdk']:
continue
2021-04-26 06:26:40 +02:00
vm = VirtualMachine.create_from_local(fields[1], fields[0], 'x86_64', 2, 4096, pathname)
vms_table.insert(vm)
# XXX: else check removed
2021-04-22 02:50:33 +02:00
# Setup networking
if config['network.mode'] == 'bridge':
argv = ['brctl', 'addbr', config['network.bridge.name']]
2021-04-22 02:50:33 +02:00
try:
cmd_run(argv)
except:
# XXX: handle errors other than already exists
pass
argv = ['ip', 'addr', 'add', config['network.bridge.addr'],
'dev', config['network.bridge.name']]
2021-04-22 02:50:33 +02:00
try:
cmd_run(argv)
except:
# XXX: handle errors other than already exists
pass
argv = ['ip', 'link', 'set', config['network.bridge.name'], 'up']
try:
cmd_run(argv)
except:
loge("Cannot activate bridge device")
sys.exit(1)
2021-04-22 02:50:33 +02:00
if 'network.proxy_arp_dev' in config:
pathname = "/proc/sys/net/ipv4/conf/%s/proxy_arp" % (config['network.proxy_arp_dev'])
file_write(pathname, "1")
dnsmasq_pid = pidfile_read('dnsmasq')
if not pid_exists(dnsmasq_pid):
logi("Starting dnsmasq...")
cfg_filename = "%s/dnsmasq-vmm.conf" % (run_dir)
f = open(cfg_filename, 'w')
f.write("pid-file=%s/dnsmasq.pid\n" % (run_dir))
f.write("interface=%s\n" % (config['network.bridge.name']))
f.write("except-interface=lo\n")
f.write("no-hosts\n")
f.write("no-resolv\n")
# DNS
f.write("local=%s\n" % (config['network.dns.server']))
# DHCP
f.write("dhcp-leasefile=%s/dnsmasq.leases\n" % (state_dir))
f.write("dhcp-range=%s,%s\n" % (config['network.dhcp.start'], config['network.dhcp.end']))
f.write("dhcp-authoritative\n")
f.close()
args = ['dnsmasq']
2021-04-22 02:50:33 +02:00
args.append("--conf-file=%s" % (cfg_filename))
fork_child(args)
else:
sys.stderr.write("FIXME: implement non-bridge networking\n")
sys.exit(1)
2021-04-26 06:26:40 +02:00
for entry in os.listdir(run_dir):
path = "%s/%s" % (run_dir, entry)
if not os.path.isdir(entry):
continue
id = int(entry, 16)
row = vms_table.select_by_id(id)
vm = VirtualMachine(row)
vms_running.append(vm)
2021-04-22 02:50:33 +02:00
# XXX: If not debug/foreground, daemonize
signal.signal(signal.SIGTERM, sig_term)
signal.signal(signal.SIGINT, sig_term)
signal.signal(signal.SIGCHLD, sig_chld)
2021-04-22 02:50:33 +02:00
file_copier_thread = threading.Thread(target=file_copier)
file_copier_thread.daemon = True
file_copier_thread.start()
lease_updater_thread = threading.Thread(target=lease_updater)
lease_updater_thread.daemon = True
lease_updater_thread.start()
2021-04-30 07:05:06 +02:00
session_expirer_thread = threading.Thread(target=session_expirer)
session_expirer_thread.daemon = True
session_expirer_thread.start()
2021-04-22 02:50:33 +02:00
if 'cli.listen.address' in config:
raw_cli_listener_thread = threading.Thread(target=raw_cli_listener)
raw_cli_listener_thread.daemon = True
raw_cli_listener_thread.start()
if 'cli.listen.ssladdress' in config:
ssl_cli_listener_thread = threading.Thread(target=ssl_cli_listener)
ssl_cli_listener_thread.daemon = True
ssl_cli_listener_thread.start()
if 'http.listen.address' in config:
raw_http_listener_thread = threading.Thread(target=raw_http_listener)
raw_http_listener_thread.daemon = True
raw_http_listener_thread.start()
if 'http.listen.ssladdress' in config:
ssl_http_listener_thread = threading.Thread(target=ssl_http_listener)
ssl_http_listener_thread.daemon = True
ssl_http_listener_thread.start()
while running:
2021-04-22 02:50:33 +02:00
time.sleep(1)
for table in [users_table, iso_images_table, disk_images_table, vms_table]:
table._lock.acquire()
dbconn.close()