Compare commits

...

10 Commits

Author SHA1 Message Date
Tom Marshall a1a503d289 Improve qemu monitor communication at start
Wait for the monitor to be alive via a no-op command before attempting
to use the monitor for anything functional.

Initial wait time is 30 seconds.
2021-06-16 13:39:26 -07:00
Tom Marshall 50e4397145 Add name argument to allow the VNC/SDL window title to be set 2021-05-13 14:25:09 -07:00
Tom Marshall 19a25fb269 Do not require macaddr, vncpass, uuid to be in VM dict
Also cleanup and fix a few things.
2021-05-13 13:16:43 -07:00
Tom Marshall f3a2fdf8d7 Tweak ui/vm create image label 2021-05-13 11:21:39 -07:00
Tom Marshall db160a8b6a Add uuid to vm 2021-05-13 11:17:27 -07:00
Tom Marshall e04f192194 Add __contains__ to DbObject 2021-05-13 11:10:17 -07:00
Tom Marshall 4c1b515007 Add SIGCHLD handler to cleanup qemu children spawned by fork 2021-05-09 09:12:27 -07:00
Tom Marshall 2c2a26b447 Invoke find_in_path in cmd_run and fork_child, not at every call site 2021-05-03 11:52:42 -07:00
Tom Marshall f02ab9a516 Remove some leftover debug prints 2021-04-30 13:47:59 -07:00
Tom Marshall 3fffc2dc92 Add permission check for ui/vm 2021-04-30 13:13:29 -07:00
1 changed files with 61 additions and 37 deletions

98
vmmd
View File

@ -332,7 +332,7 @@ def image_info(pathname):
pass
out = ''
try:
argv = [find_in_path('qemu-img'), 'info', '-U', pathname]
argv = ['qemu-img', 'info', '-U', pathname]
(out, err) = cmd_run(argv)
except RuntimeError as e:
pass
@ -453,6 +453,8 @@ def pidfile_remove(name, path=None):
def cmd_run(args, stdin=None):
logi("cmd_run: %s\n" % (args))
if not args[0].startswith('/'):
args[0] = find_in_path(args[0])
child = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if not stdin is None:
child.stdin.write(stdin)
@ -464,10 +466,11 @@ def cmd_run(args, stdin=None):
def fork_child(args):
logi("fork_child: %s" % (args))
cmd = args[0] if args[0].startswith('/') else find_in_path(args[0])
pid = os.fork()
if pid == 0:
try:
os.execv(args[0], args)
os.execv(cmd, args)
except BaseException as e:
sys.stderr.write("os.execv raised %s\n" % (e))
sys.stderr.write("os.execv returned unexpectedly\n")
@ -506,6 +509,13 @@ def sig_term(signum, frame):
global running
running = False
def sig_chld(signum, frame):
try:
while True:
(pid, status) = os.waitpid(0, os.WNOHANG)
except:
pass
### Config ###
class Config:
@ -590,6 +600,9 @@ class DbObject:
def values(self):
return self._dict.values()
def __contains__(self, name):
return name in self._dict
def __getitem__(self, name):
return self._dict[name]
@ -667,10 +680,8 @@ class DbTable:
keystr = ','.join(obj.keys())
valstr = ','.join([self._sql_val_str(v) for v in obj.values()])
query = "INSERT INTO %s (%s) values (%s)" % (self._name, keystr, valstr)
print("query=%s" % (query))
cursor = dbconn.execute(query)
obj['id'] = cursor.lastrowid
print("id=%d" % (cursor.lastrowid))
def update(self, obj):
if not obj._changed:
@ -747,7 +758,6 @@ class Image(DbObject):
if os.path.exists(pathname):
raise RuntimeError("Image already exists")
img = Image({'name': name, 'pathname': pathname, 'owner': user['name'], 'public': public})
print("Image: add %s to fetch queue" % (url))
acp_queue(url, pathname)
return img
@ -785,7 +795,7 @@ class VirtualMachine(DbObject):
def __init__(self, row):
DbObject.__init__(self, row)
self._lock = threading.Lock()
if self['macaddr'] is None:
if self.get('macaddr', None) is None:
found = False
while not found:
b4 = int(random.random() * 256)
@ -797,8 +807,15 @@ class VirtualMachine(DbObject):
if not row:
found = True
self['macaddr'] = macaddr
if self['vncpass'] is None:
if self.get('vncpass', None) is None:
self['vncpass'] = pwgen(8)
if self.get('uuid', None) is None:
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)
(self._disk_psize, self._disk_vsize, self._disk_fmt) = image_info(self['diskpath'])
self._copy_status = None
@ -812,14 +829,14 @@ class VirtualMachine(DbObject):
diskpath = VirtualMachine.pathname_for_disk(owner['name'], name, '.qcow2')
if os.path.exists(diskpath):
raise RuntimeError("Disk already exists")
argv = [find_in_path('qemu-img'), 'create',
argv = ['qemu-img', 'create',
'-f', 'qcow2',
'-o', 'preallocation=metadata',
diskpath, "%dM" % (disk_size)]
cmd_run(argv)
return VirtualMachine({'name': name, 'owner': owner['name'],
'arch': arch, 'cpus': cpus, 'mem': mem,
'diskpath': diskpath, 'macaddr': None, 'vncpass': None})
'diskpath': diskpath})
@staticmethod
def create_from_image(name, owner, arch, cpus, mem, image_id):
@ -830,20 +847,18 @@ class VirtualMachine(DbObject):
if os.path.exists(diskpath):
raise RuntimeError("Disk already exists")
if ext == '.qcow2':
argv = [find_in_path('qemu-img'), 'create', '-f', 'qcow2', '-b', img['pathname'], diskpath]
argv = ['qemu-img', 'create', '-f', 'qcow2', '-b', img['pathname'], diskpath]
cmd_run(argv)
else:
acp_queue(img['pathname'], diskpath)
return VirtualMachine({'name': name, 'owner': owner['name'],
'arch': arch, 'cpus': cpus, 'mem': mem,
'vncpass': None, 'macaddr': None,
'diskpath': diskpath})
@staticmethod
def create_from_local(name, owner, arch, cpus, mem, pathname):
return VirtualMachine({'name': name, 'owner': owner['name'],
'arch': arch, 'cpus': cpus, 'mem': mem,
'vncpass': None, 'macaddr': None,
'diskpath': pathname})
@staticmethod
@ -863,7 +878,7 @@ class VirtualMachine(DbObject):
diskpath = VirtualMachine.pathname_for_disk(owner.name(), name, ext)
if os.path.exists(diskpath):
raise RuntimeError("Disk already exists")
vm = VirtualMachine(None, name, owner, arch, cpus, mem, None, None, diskpath)
vm = VirtualMachine.create_from_local(name, owner, arch, cpus, mem, diskpath)
acp_queue(image_url, diskpath)
return vm
@ -885,7 +900,7 @@ class VirtualMachine(DbObject):
def _snapshot_list(self):
snapshots = []
if self['diskpath'].endswith('.qcow2'):
argv = [find_in_path('qemu-img'), 'snapshot', '-l', self['diskpath']]
argv = ['qemu-img', 'snapshot', '-l', self['diskpath']]
(out, err) = cmd_run(argv)
for line in out.rstrip('\n').split('\n'):
fields = line.split()
@ -933,7 +948,6 @@ class VirtualMachine(DbObject):
return 'stopped'
def ipv4addr(self):
print("mac=%s leases=%s" % (self['macaddr'], leases))
return leases.get(self['macaddr'], None)
def ipv6addr(self, val=None):
@ -972,8 +986,9 @@ class VirtualMachine(DbObject):
cpu_arg = 'cortex-a53'
else:
raise RuntimeError('Unknown arch')
argv = [find_in_path(prog)]
argv = [prog]
argv.extend(['-daemonize', '-pidfile', self._qemu_pidfile()])
argv.extend(['-name', self['name']])
argv.extend(['-machine', machine_arg, '-cpu', cpu_arg])
ethdev = 'virtio-net' if self['ostype'] == 'linux' else 'e1000'
blkopt = ''
@ -988,6 +1003,7 @@ class VirtualMachine(DbObject):
'-monitor', "unix:%s/monitor,server,nowait" % (vm_run_dir),
'-serial', "unix:%s/serial,server,nowait" % (vm_run_dir),
'-vnc', ":%d,password=on" % (self['id']),
'-smbios', "type=1,uuid=%s" % (self['uuid']),
'-drive', "file=%s%s" % (self['diskpath'], blkopt)])
if has_pci:
argv.extend(['-netdev', "bridge,br=%s,id=net1" % (config['network.bridge.name']),
@ -1005,8 +1021,11 @@ class VirtualMachine(DbObject):
pass
fork_child(argv)
file_wait_exists(self._qemu_pidfile(), 2.0)
res = self._run_monitor_command('info status', 30.0)
if not res:
loge('Failed to communicate with monitor')
if resuming:
self._run_monitor_command('delvm vmm-suspend', 5.0)
self._run_monitor_command('delvm vmm-suspend')
if self['vncpass']:
self._run_monitor_command("change vnc password %s" % (self['vncpass']))
@ -1825,15 +1844,25 @@ class HttpClientRequestHandler(http.server.BaseHTTPRequestHandler):
def ui_vm(self, user, args):
# XXX: auth
r = self._html_head(user)
err = None
msg = None
is_admin = user.in_group('admin')
if 'id' in args:
err = None
msg = None
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:
server_host = self.headers['Host']
if ':' in server_host:
server_host = server_host.split(':')[0]
vm_id = int(args['id'][0])
row = vms_table.select_by_id(vm_id)
vm = VirtualMachine(row)
vm_running = vm.running()
edit_mode = (not vm_running) and ('action' in args) and (args['action'][0] == 'Edit')
if 'action' in args:
@ -1893,7 +1922,7 @@ class HttpClientRequestHandler(http.server.BaseHTTPRequestHandler):
if err:
r += " <p style=\"font-size:125%%;color:red\">%s</p>\n" % (err)
r += ' <form method="POST" action="/ui/vm">\n'
r += " <input type=\"hidden\" name=\"id\" value=\"%d\">\n" % (vm_id)
r += " <input type=\"hidden\" name=\"id\" value=\"%d\">\n" % (args_id)
r += ' <table>\n'
if edit_mode:
r += " <tr><td style=\"font-weight:bold\">Name<td><input type=\"text\" name=\"name\" value=\"%s\">\n" % (vm['name'])
@ -1916,7 +1945,7 @@ class HttpClientRequestHandler(http.server.BaseHTTPRequestHandler):
r += " <tr><td style=\"font-weight:bold\">Addr<td>%s\n" % (vm.ipv4addr())
r += ' <tr><td>&nbsp;<td>&nbsp;\n'
if vm_running:
r += " <tr><td style=\"font-weight:bold\">VNC Host<td>%s:%d\n" % (server_host, vm_id)
r += " <tr><td style=\"font-weight:bold\">VNC Host<td>%s:%d\n" % (server_host, args_id)
r += " <tr><td style=\"font-weight:bold\">VNC Pass<td>%s\n" % (vm['vncpass'])
r += ' <tr><td>&nbsp;<td>&nbsp;\n'
r += ' </table>\n'
@ -1944,7 +1973,7 @@ class HttpClientRequestHandler(http.server.BaseHTTPRequestHandler):
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'
r += ' <tr><td style="font-size:125%">Create image from disk<td>&nbsp;\n'
r += ' <tr><td colspan="2" style="font-size:125%">Create image from disk\n'
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'
@ -1989,7 +2018,6 @@ class HttpClientRequestHandler(http.server.BaseHTTPRequestHandler):
msg = None
err = None
if 'action' in args and args['action'][0] == 'Create':
print("Creating VM")
vm = None
name = args['name'][0]
arch = args['arch'][0]
@ -2239,7 +2267,6 @@ def acp_simple(srcfile, dstfile, file_size):
off += len(buf)
curpct = int(100 * off / file_size)
if curpct != pct:
print("acp_simple: dstfile=%s curpct=%d" % (dstfile, curpct))
pct = curpct
_acp_lock.acquire()
_acp_pct = pct
@ -2331,7 +2358,6 @@ def lease_updater():
new_leases[macaddr] = ipv4addr
f.close()
leases = new_leases
print("leases=%s" % (leases))
except OSError as e:
last_mtime = 0
except BaseException as e:
@ -2440,6 +2466,7 @@ vms_table = DbTable(dbconn, 'vms',
mem INTEGER,
ostype VARCHAR(64),
vncpass CHAR(8),
uuid CHAR(36),
macaddr CHAR(17),
disksize INTEGER,
diskpath VARCHAR(256),
@ -2490,23 +2517,20 @@ if vms_table.empty():
# Setup networking
if config['network.mode'] == 'bridge':
argv = [find_in_path('brctl'),
'addbr', config['network.bridge.name']]
argv = ['brctl', 'addbr', config['network.bridge.name']]
try:
cmd_run(argv)
except:
# XXX: handle errors other than already exists
pass
argv = [find_in_path('ip'),
'addr', 'add', config['network.bridge.addr'],
'dev', config['network.bridge.name']]
argv = ['ip', 'addr', 'add', config['network.bridge.addr'],
'dev', config['network.bridge.name']]
try:
cmd_run(argv)
except:
# XXX: handle errors other than already exists
pass
argv = [find_in_path('ip'),
'link', 'set', config['network.bridge.name'], 'up']
argv = ['ip', 'link', 'set', config['network.bridge.name'], 'up']
try:
cmd_run(argv)
except:
@ -2533,8 +2557,7 @@ if config['network.mode'] == 'bridge':
f.write("dhcp-range=%s,%s\n" % (config['network.dhcp.start'], config['network.dhcp.end']))
f.write("dhcp-authoritative\n")
f.close()
args = []
args.append(find_in_path('dnsmasq'))
args = ['dnsmasq']
args.append("--conf-file=%s" % (cfg_filename))
fork_child(args)
@ -2555,6 +2578,7 @@ for entry in os.listdir(run_dir):
signal.signal(signal.SIGTERM, sig_term)
signal.signal(signal.SIGINT, sig_term)
signal.signal(signal.SIGCHLD, sig_chld)
file_copier_thread = threading.Thread(target=file_copier)
file_copier_thread.daemon = True