diff --git a/init.sh b/init.sh index f8d7e4423400c87498a607430e748a00e8bfc6a0..67c4351d8c7dd023ab0c90eea6253126af24b576 100755 --- a/init.sh +++ b/init.sh @@ -1,3 +1,4 @@ #!/bin/bash ln -sr vm.py /usr/bin/vm cp vm.service /lib/systemd/system/ +cp vm-qemu@.service /lib/systemd/system/ diff --git a/vm-qemu@.service b/vm-qemu@.service new file mode 100644 index 0000000000000000000000000000000000000000..3d26511b8304e1bf55ab240be144cf69e9b92d9a --- /dev/null +++ b/vm-qemu@.service @@ -0,0 +1,16 @@ +[Unit] +Description=qemu vm %i +After=network.target + +[Service] + +Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/sbin:/usr/bin:/bin: +Environment=VM_BACKEND=qemu +WorkingDirectory=/mnt/virtual/%i.vm +ExecStart=/bin/sh run-qemu + +Restart=no + + +[Install] +WantedBy=default.target diff --git a/vm.py b/vm.py index 3100bd94b01a9f6fe9d08e6a786ae357512b92fa..7028904d6c4a20a886826e9c21060bcaf2a56e7d 100755 --- a/vm.py +++ b/vm.py @@ -6,17 +6,42 @@ import time import json from dataclasses import dataclass import functools -from typing import Optional +from typing import Optional, Any import traceback +######################## +# Global configuration # +######################## socket_path = '.socket' + is_daemon = False if __name__ == "__main__": if len(sys.argv)>=2 and sys.argv[1]=="server": is_daemon = True +root_folder="/mnt/virtual/" + +backend_qemu = "qemu" +backend_vbox = "vbox" +backend = os.environ.get("VM_BACKEND", backend_qemu) +assert backend in [backend_qemu, backend_vbox] + +net_prefix = "10.37" if backend == backend_vbox else "10.38" + +boot_id = open("/proc/sys/kernel/random/boot_id").read().strip()+"-"+backend + +force=False +no_daemon=False +verbose=1 # daemon is verbose + + + +######################## +# Utils # +######################## + class S(): ''' Class for nice formated long text area. @@ -46,10 +71,8 @@ class S(): return "\n".join("" if len(l) < to_remove else l[to_remove:] for l in lines) S=S() -force=False -no_daemon=False -verbose=1 # daemon is verbose - +def escape_sh(*arg): + return " ".join("'" + s.replace("'", "'\"'\"'") + "'" for s in arg) def r(*arg, check=None, stdin=None): if check is None: @@ -65,15 +88,6 @@ def nft(rules): subprocess.run(["nft", rules], check=not force) -parser = argparse.ArgumentParser() - - -subparsers = parser.add_subparsers(help="commands", dest="subcommand") -subcommands = {} - -parser.add_argument("-f", "--force", action='store_true') -parser.add_argument("-r", "--root_folder", type=str) -parser.add_argument("-v", "--verbose", action='count') def get_spec(f): import inspect @@ -81,12 +95,14 @@ def get_spec(f): f.spec = inspect.getfullargspec(f) return f.spec -def cmd(f): +def internal_cmd(f): + return cmd(f, internal=True) +def cmd(f, internal=False): if f is None: return f import inspect spec = get_spec(f) - subcommands[f.__name__] = f - f.parser = subparsers.add_parser(f.__name__) + (subcommands_internal if internal else subcommands)[f.__name__] = f + f.parser = (subparsers_internal if internal else subparsers).add_parser(f.__name__) # print() # print(f) #fprint(spec) @@ -116,6 +132,11 @@ def cmd(f): "--"+arg, action="store_true", ) + if annotation in [Identification]: + f.parser.add_argument( + ("--" if has_default else "")+arg, + type=str, + ) for i, arg in enumerate(spec.args): has_default = spec.defaults is not None and i >= len(spec.args) - len(spec.defaults) @@ -139,7 +160,8 @@ def cmd(f): return f def random_passwd(): - return "".join(chr(ord('0') + i%10) for i in os.urandom(50)) + passwd_chars = "0123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM,.;?!/\\<>(){}[]'\"`|~@#$%^&*-_+-=" + return "".join(passwd_chars[i%len(passwd_chars)] for i in os.urandom(50)) @dataclass class Ucred: @@ -183,10 +205,10 @@ def daemon(root_only=False): spec = spec._replace(args=spec.args[1:]) if root_only: + @functools.wraps(f) def l(ucred, *arg, **kvarg): assert ucred.uid == 0 f(ucred, *arg, **kvarg) - l.__name__ = f.__name__ l.spec = get_spec(f) daemon_funcs[f.__name__] = l else: @@ -198,34 +220,84 @@ def daemon(root_only=False): # TODO validate types if root_only and my_ucred().uid != 0: return None + @functools.wraps(f) def l(*arg, **kvarg): if no_daemon: f(my_ucred(), *arg, **kvarg) - r = ask_server({"fname":f.__name__, "arg": arg, "kvarg": kvarg}) + r = ask_server({"fname":f.__name__, "arg": arg, "kvarg": kvarg, "backend": backend}) + if "exception" in r: + print(r["exception"], file=sys.stderr) + exit(1) return r["return"] - l.__name__ = f.__name__ l.spec = spec return l return ll +class Identification: + vm: str + user: Optional[str] + display: Optional[str] + wayland_display: Optional[str] + vnc_port: Optional[int] + + def __init__(self, vm: str, user: Optional[str] = None, display: Optional[str] = None, wayland_display: Optional[str] = None, vnc_port: Optional[int] = None): + self.vm = vm + self.user = user + self.display = display + self.wayland_display = wayland_display + self.vnc_port = vnc_port + + def __getitem__(self, key): + return [self.vm, self.user][key] + + def __str__(self): + out = self.vm + if self.user is not None: + out = f"{self.user}@{out}" + return out + + def set_default_user(self, user: str = "u"): + if self.user is None: + self.user = user + return self + +######################## +# Argparser # +######################## -########################################################## +parser = argparse.ArgumentParser() -root_folder="/mnt/virtual/" -boot_id = open("/proc/sys/kernel/random/boot_id").read().strip() +subparsers = parser.add_subparsers(help="commands", dest="subcommand") +subcommands = {} + +parser_internal = subparsers.add_parser("internal") +subparsers_internal = parser_internal.add_subparsers(help="internal_commands", dest="subcommand_internal") +subcommands_internal = {} + +parser.add_argument("-f", "--force", action='store_true') +parser.add_argument("-r", "--root_folder", type=str) +parser.add_argument("-v", "--verbose", action='count') + state_not_registered = "not registered" state_powered_off = "powered off" state_aborted = "aborted" state_saved = "saved" state_running = "running" +state_paused = "paused" + + + +######################### +# Name, id, dir … tools # +######################### def vm_dir(vm: str): return f"{root_folder}/{vm}.vm/" -@cmd +@internal_cmd def name_to_id(name: str) -> str: assert is_valid_id(name) or is_valid_name(name) if is_valid_name(name): @@ -236,7 +308,7 @@ def name_to_id(name: str) -> str: assert not os.path.islink(name+".vm") return name -@cmd +@internal_cmd def name(vm: str) -> str: vm = name_to_id(vm) return open(vm_dir(vm)+"name").read().strip() @@ -247,6 +319,18 @@ def is_valid_name(name): def is_valid_id(id): return all(i.isnumeric() for i in id) +def all_virtuals(): + for f in os.listdir(root_folder): + if f.endswith(".vm"): + vm = f[:-3] + if is_valid_id(vm): + yield vm + + +@internal_cmd +def is_android(vm: str): + return os.path.exists(vm_dir(vm)+"is_android") + @daemon() def has_read_acces(ucred, vm: str): # TODO! @@ -257,25 +341,31 @@ def has_write_acces(ucred, vm: str): # TODO! return True -@cmd +######################## +# Low-level API # +######################## + +@internal_cmd def get_ip(vm: str) -> str: network_dir = vm_dir(vm)+"network/" net_id = open(network_dir+"net_id").read().strip() - return f'10.37.{net_id}.150' + return f'{net_prefix}.{net_id}.150' -@cmd +@internal_cmd def get_permanency(vm: str): try: return open(vm_dir(vm)+"permanency").read().strip() except FileNotFoundError: return "undef" -@cmd +@internal_cmd @daemon() def set_permanency(ucred, vm: str, permanency: str): vm = name_to_id(vm) assert has_write_acces(ucred, vm) - assert permanency in ["tmp", "stable"] + assert permanency in ["tmp", "stable", "prepared"] + if ucred.uid != 0: + assert permanency in ["tmp", "stable"] with open(vm_dir(vm)+"permanency", "w") as f: f.write(permanency) @@ -285,24 +375,178 @@ def give_to_user(vm: str, uid: int, gid: Optional[int] = None): import pwd gid = pwd.getpwuid(uid).pw_gid vm = name_to_id(vm) - os.chown(vm_dir(vm)+"id_ed25519", uid, gid) + if not is_android(vm): + os.chown(vm_dir(vm)+"id_ed25519", uid, gid) -@cmd + +@internal_cmd +@daemon() +def start(ucred, vm: str, display: bool = None): + vm = name_to_id(vm) + assert has_write_acces(ucred, vm) + + if backend == backend_vbox: + if state(ucred, vm) == state_not_registered: + register_vm(ucred, vm) + + if not os.path.exists(vm_dir(vm)+"network/boot_id") or open(vm_dir(vm)+"network/boot_id", "r").read().strip() != boot_id: + create_net(ucred, vm) + + if backend == backend_vbox: + r("VBoxManage", "startvm", vm, "--type=headless") + elif backend == backend_qemu: + if not os.path.exists(vm_dir(vm)+"OVMF.fd"): + r("cp", "/usr/share/edk2-ovmf/x64/OVMF.fd", vm_dir(vm)+"OVMF.fd") + with open(vm_dir(vm)+"run-qemu", "w") as f: + cmd = S-f""" + qemu-system-x86_64 -enable-kvm \\ + -device usb-ehci \\ + -m 5000M -cpu host -smp 4 -cpu host -smp 4 \\ + -monitor unix:qemu-monitor,server,nowait \\ + -drive if=pflash,format=raw,file=OVMF.fd \\ + -drive file=img,index=0,media=disk,format=raw \\ + -nographic \\ + """+'\n' + cmd += f'-device virtio-net,netdev=network0 -netdev tap,id=network0,ifname={open(vm_dir(vm)+"network/interface").read().strip()},script=network/up.sh,downscript=network/down.sh \\\n' + if display: + cmd += "-vnc unix:qemu-vnc,power-control=on \\\n" + if is_android(vm): + cmd += f'-vga vmware\\\n' + f.write(cmd+"\n") + r("systemctl", "start", f"vm-qemu@{vm}") + else: + raise NotImplementedError() + + if get_permanency(vm).startswith("init "): + set_permanency(ucred, vm, "tmp") + + +######################## +# Backend control # +######################## + +@internal_cmd @daemon() def state(ucred, vm: str): vm = name_to_id(vm) assert has_read_acces(ucred, vm) - p = subprocess.run(["VBoxManage", "showvminfo", vm], capture_output=True, encoding='utf8') - if "Could not find a registered machine named " in p.stderr: - return state_not_registered - for l in p.stdout.split('\n'): - if l.startswith("State:"): - return l[10:].split("(")[0].strip() - raise RuntimeError(p.stderr) + if backend == backend_vbox: + p = subprocess.run(["VBoxManage", "showvminfo", vm], capture_output=True, encoding='utf8') + if "Could not find a registered machine named " in p.stderr: + return state_not_registered + for l in p.stdout.split('\n'): + if l.startswith("State:"): + return l[10:].split("(")[0].strip() + raise RuntimeError(p.stderr) + elif backend == backend_qemu: + p = subprocess.run(["systemctl", "show", f"vm-qemu@{vm}.service", "--property=ActiveState"], capture_output=True, encoding='utf8', check=True) + return p.stdout.split("=")[1].strip() + + + else: + raise NotImplementedError() + + +@internal_cmd +@daemon() +def register_vm(ucred: Ucred, vm: str): + vm = name_to_id(vm) + assert has_read_acces(ucred, vm) + if backend == backend_vbox: + r('VBoxManage', 'internalcommands', 'sethduuid', target_dir+"disk.vmdk") + + r('VBoxManage', 'createvm', f'--name={target}', f"--basefolder={vm_dir(target)}", "--register") + r('VBoxManage', 'modifyvm', target, '--firmware=efi') + r('VBoxManage', 'modifyvm', target, '--memory=4096') + r("VBoxManage", "storagectl", target, '--name', "SATA Controller", '--add', 'sata', '--bootable', 'on') + r("VBoxManage", "storageattach", target, "--storagectl", "SATA Controller", "--port", "0", "--device", "0", "--type", "hdd", "--medium", f"{vm_dir(target)}/disk.vmdk") + +@internal_cmd +@daemon() +def unregister_vm(ucred: Ucred, vm: str): + if backend == backend_vbox: + r("VBoxManage", "unregistervm", vm, "--delete-all") + +@cmd +@daemon() +def poweroff(ucred, vm: str): + vm = name_to_id(vm) + assert has_write_acces(ucred, vm) + + if backend == backend_vbox: + r("VBoxManage", "controlvm", vm, "acpipowerbutton") + elif backend == backend_qemu: + qemu_cmd(vm, "system_powerdown") + else: + raise NotImplementedError() + +@cmd +@daemon() +def kill(ucred, vm: str): + vm = name_to_id(vm) + assert has_write_acces(ucred, vm) + + if backend == backend_qemu: + r("systemctl", "stop", f"vm-qemu@{vm}") + else: + raise NotImplementedError() + +@cmd +@daemon() +def pause(ucred, vm: str): + vm = name_to_id(vm) + assert has_write_acces(ucred, vm) + if backend == backend_vbox: + r("VBoxManage", "controlvm", vm, "pause") + elif backend == backend_qemu: + qemu_cmd(vm, "stop") + else: + raise NotImplementedError() +@cmd +@daemon() +def resume(ucred, vm: str): + vm = name_to_id(vm) + assert has_write_acces(ucred, vm) + if backend == backend_vbox: + r("VBoxManage", "controlvm", vm, "resume") + elif backend == backend_qemu: + qemu_cmd(vm, "cont") + else: + raise NotImplementedError() @cmd +def poweroff_and_wait(vm: str): + vm = name_to_id(vm) + if state(vm) in [state_powered_off, state_not_registered, state_aborted, state_saved, 'inactive', 'failed']: return + poweroff(vm) + while True: + time.sleep(0.1) + s = state(vm) + print("state", s) + if s in [state_powered_off, 'inactive']: + time.sleep(1) + return + +def poweroff_and_wait_daemon(ucred, vm: str): + vm = name_to_id(vm) + if state(ucred, vm) in [state_powered_off, state_not_registered, state_aborted, state_saved, 'inactive']: return + poweroff(ucred, vm) + while True: + time.sleep(0.1) + s = state(ucred, vm) + print("state", s) + if s == state_powered_off: + time.sleep(1) + return + + +######################## +# Init vm # +######################## + +@internal_cmd @daemon(root_only=True) def create_from_img(ucred: Ucred, target: str, new_ssh: bool = True, target_name = None): target_name = target_name or target @@ -315,27 +559,28 @@ def create_from_img(ucred: Ucred, target: str, new_ssh: bool = True, target_name with open(target_dir+"vnc_passwd", "w") as f: f.write(vnc_passwd) - mount_dir = target_dir+"mount/" - os.mkdir(mount_dir) - r('mount', '-o', 'nosymfollow,loop,offset=210763776', '--type', 'ext4', target_dir+'img', mount_dir) - try: - with open(mount_dir+"/etc/hostname", "w") as f: f.write(target_name+"\n") - if new_ssh: - r("ssh-keygen", "-t", "ed25519", "-C", f"virtual for root,u@vm_{target}", "-f", target_dir+"id_ed25519", "-P", "") - for place in ["/root/.ssh/", "/home/u/.ssh/"]: - if os.path.isfile(mount_dir+place+"/authorized_keys"): - with open(mount_dir+place+"/authorized_keys", "w") as f: - f.write(open(target_dir+"id_ed25519.pub", "r").read()) - r("ssh-keygen", "-t", "ed25519", "-C", f"hostkey of root,u@vm_{target}", "-f", target_dir+"ssh_host_ed25519_key", "-P", "") - for suffix in ["", ".pub"]: - with open(mount_dir+"/etc/ssh/ssh_host_ed25519_key"+suffix, "w") as f: - f.write(open(target_dir+"ssh_host_ed25519_key"+suffix, "r").read()) - with open(target_dir+"known_hosts", "w") as f: - f.write(f"vm_{target} "+" ".join(open(target_dir+"ssh_host_ed25519_key"+suffix, "r").read().split()[:2])) - with open(mount_dir+"vnc_passwd", "w") as f: f.write(vnc_passwd) - finally: - r('umount', mount_dir) - os.rmdir(mount_dir) + if not is_android(target): + mount_dir = target_dir+"mount/" + os.mkdir(mount_dir) + r('mount', '-o', 'nosymfollow,loop,offset=210763776', '--type', 'ext4', target_dir+'img', mount_dir) + try: + with open(mount_dir+"/etc/hostname", "w") as f: f.write(target_name+"\n") + if new_ssh: + r("ssh-keygen", "-t", "ed25519", "-C", f"virtual for root,u@vm_{target}", "-f", target_dir+"id_ed25519", "-P", "") + for place in ["/root/.ssh/", "/home/u/.ssh/"]: + if os.path.isfile(mount_dir+place+"/authorized_keys"): + with open(mount_dir+place+"/authorized_keys", "w") as f: + f.write(open(target_dir+"id_ed25519.pub", "r").read()) + r("ssh-keygen", "-t", "ed25519", "-C", f"hostkey of root,u@vm_{target}", "-f", target_dir+"ssh_host_ed25519_key", "-P", "") + for suffix in ["", ".pub"]: + with open(mount_dir+"/etc/ssh/ssh_host_ed25519_key"+suffix, "w") as f: + f.write(open(target_dir+"ssh_host_ed25519_key"+suffix, "r").read()) + with open(target_dir+"known_hosts", "w") as f: + f.write(f"vm_{target} "+" ".join(open(target_dir+"ssh_host_ed25519_key"+suffix, "r").read().split()[:2])) + with open(mount_dir+"vnc_passwd", "w") as f: f.write(vnc_passwd) + finally: + r('umount', mount_dir) + os.rmdir(mount_dir) with open(target_dir+"disk.vmdk", "w") as f: f.write(S-f""" @@ -366,26 +611,20 @@ def create_from_img(ucred: Ucred, target: str, new_ssh: bool = True, target_name """) - - r('VBoxManage', 'internalcommands', 'sethduuid', target_dir+"disk.vmdk") - - r('VBoxManage', 'createvm', f'--name={target}', f"--basefolder={vm_dir(target)}", "--register") - r('VBoxManage', 'modifyvm', target, '--firmware=efi') - r('VBoxManage', 'modifyvm', target, '--memory=4096') - r("VBoxManage", "storagectl", target, '--name', "SATA Controller", '--add', 'sata', '--bootable', 'on') - r("VBoxManage", "storageattach", target, "--storagectl", "SATA Controller", "--port", "0", "--device", "0", "--type", "hdd", "--medium", f"{vm_dir(target)}/disk.vmdk") - + + register_vm(my_ucred(), target) create_net(my_ucred(), target) -@cmd +@internal_cmd @daemon() def clone(ucred, target: str, base: str = "base") -> str: base = name_to_id(base) assert has_read_acces(ucred, base) - target = clone_copy(ucred, target, vm_dir(base)+"img") + andr = is_android(base) + target = clone_copy(ucred, target, vm_dir(base)+"img", vm_dir(base)+"OVMF.fd" if andr else None, andr) give_to_user(target, ucred.uid, ucred.gid) return target - + def create_vm_dir(target: str) -> str: import random @@ -400,18 +639,183 @@ def create_vm_dir(target: str) -> str: with open(vm_dir(target)+"name", "w") as f: f.write(target) return target_id -@cmd +@internal_cmd @daemon(root_only=True) -def clone_copy(ucred, target: str, img_path: str) -> str: +def clone_copy(ucred, target: str, img_path: str, ovmf_path: Optional[str] = None, is_android: bool = False) -> str: assert is_valid_name(target) target_id = create_vm_dir(target) r('cp', '--reflink', "-n", img_path, vm_dir(target_id)+"img") + if ovmf_path: + r('cp', '--reflink', "-n", ovmf_path, vm_dir(target_id)+"OVMF.fd") + if is_android: + r('touch', vm_dir(target_id)+"is_android") create_from_img(ucred, target_id, target_name=target) return target_id -def ssh_args(vm: str, *arg: tuple[str, ...], user: str = "u"): - vm, user = extended_name(vm, user) + +######################## +# Networking # +######################## + +@internal_cmd +@daemon() +def remove_net(ucred, vm: str): + vm = name_to_id(vm) + assert has_write_acces(ucred, vm) + network_dir = vm_dir(vm)+"network/" + if os.path.isdir(network_dir): + if backend == backend_vbox: + if os.path.isfile(network_dir+"interface"): + interface = open(network_dir+"interface").read().strip() + if open(network_dir+"boot_id", "r").read().strip() == boot_id: + if os.path.isfile(network_dir+"dhcp.pid"): + r("kill", open(network_dir+"dhcp.pid").read().strip(), check=False) + net_id = int(open(network_dir+"net_id", "r").read()) + r("ip", "addr", "del", f"{net_prefix}.{net_id}.1/24", "dev", interface) + + + r("VBoxManage", "hostonlyif", "remove", interface) + elif backend == backend_qemu: + ... + else: + raise NotImplementedError() + r('rm', '-r', network_dir) + +@internal_cmd +@daemon() +def create_net(ucred, vm: str): + vm = name_to_id(vm) + assert has_write_acces(ucred, vm) + + remove_net(ucred, vm) + + network_dir = vm_dir(vm)+"network/" + os.mkdir(network_dir) + + if backend == backend_vbox: + p = subprocess.run(["VBoxManage", "hostonlyif", "create"], capture_output=True, encoding='utf8') + if p.returncode: + print(p.stderr, file=sys.stderr) + raise RuntimeError() + interface = p.stdout.split("'")[1] + + net_id = int(interface[7:]) + + if verbose: print("interface", interface) + + r("VBoxManage", "hostonlyif", "ipconfig", interface, f"--ip={net_prefix}.{net_id}.1", "--netmask=255.255.255.0") + + if backend == backend_qemu: + import random + net_id = random.randint(0, 255) # TODO allocation + interface = f"qemu{net_id}" + + with open(network_dir+"interface", "w") as f: + f.write(interface) + + r("sysctl", "net.ipv4.ip_forward=1") + + + with open(network_dir+"boot_id", "w") as f: f.write(boot_id) + with open(network_dir+"net_id", "w") as f: f.write(str(net_id)) + + #r("ip", "link", "add", f"v{net_id}h", "type", "veth", "peer", "name", f"v{net_id}g") + #r("ip", "link", "add", f"v{net_id}b", "type", "bridge") + #r("ifconfig", f"v{net_id}h", "up") + #r("ip", "link", "set", f"v{net_id}g", "master", f"v{net_id}b") + nft(f"") + nft(S-f""" + add chain inet filter input_from_{interface} + add chain inet filter forward_from_{interface} + add chain inet filter forward_to_{interface} + + insert rule inet filter input iifname {interface} jump input_from_{interface} + insert rule inet filter forward iifname {interface} jump forward_from_{interface} + insert rule inet filter forward oifname {interface} jump forward_to_{interface} + """) + modify_net(ucred, vm) + #nft("add rule inet filter forward iifname wlp1s0 accept") + #r("VBoxManage", "modifyvm", vm, "--nic1=bridged", f"--bridgeadapter1=v{net_id}b") + + with open(network_dir+"dhcp.lp", "w") as f: f.write("authoring-byte-order little-endian;") + with open(network_dir+"dhcp.pid", "w") as f: f.write("") + with open(network_dir+"dhcp.config", "w") as f: + f.write(S-f""" + option domain-name-servers 8.8.8.8, 8.8.4.4; + option subnet-mask 255.255.255.0; + option routers {net_prefix}.{net_id}.1; + subnet {net_prefix}.{net_id}.0 netmask 255.255.255.0 {{ + range {net_prefix}.{net_id}.150 {net_prefix}.{net_id}.250; + }} + """) + + if backend == backend_vbox: + r("VBoxManage", "modifyvm", vm, "--nic1=hostonly", f"--host-only-adapter1={interface}") + r("ifconfig", interface, f"{net_prefix}.{net_id}.1", "netmask", "255.255.255.0", "up"); + r("dhcpd", "-4", "-cf", network_dir+"dhcp.config", "-pf", network_dir+"dhcp.pid", "-lf", network_dir+"dhcp.lp", interface) + if backend == backend_qemu: + with open(network_dir+"up.sh", "w") as f: + f.write(S-f""" + #!/bin/sh + ifconfig {interface} {net_prefix}.{net_id}.1 netmask 255.255.255.0 up + dhcpd -4 -cf network/dhcp.config -pf network/dhcp.pid -lf network/dhcp.lp {interface} + """) + with open(network_dir+"down.sh", "w") as f: + f.write(S-f""" + #!/bin/sh + """) + r("chmod", "+x", network_dir+"up.sh", network_dir+"down.sh") + +@internal_cmd +@daemon() +def modify_net(ucred, vm: str, wan: bool = False, lan: bool = False, pc: bool = False, pc_all: bool = False): + vm = name_to_id(vm) + assert has_write_acces(ucred, vm) + assert not (pc_all and not pc) + network_dir = vm_dir(vm)+"network/" + interface = open(network_dir+"interface").read().strip() + net_id = open(network_dir+"net_id").read().strip() + assert open(network_dir+"boot_id", "r").read().strip() == boot_id + + local_ips = "{10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16}" + if wan: + pass + todo = [f"flush chain inet filter input_from_{interface}", + f"flush chain inet filter forward_from_{interface}", + f"flush chain inet filter forward_to_{interface}"] + todo.append(f"add rule inet filter input_from_{interface} ct state {{ established, related }} accept") + if not pc: + todo.append(f"add rule inet filter input_from_{interface} drop") + if pc_all: + todo.append(f"add rule inet filter input_from_{interface} accept") + + if lan: + todo.append(f"add rule inet filter forward_from_{interface} ip daddr {local_ips} accept") + todo.append(f"add rule inet filter forward_to_{interface} ip saddr {local_ips} ct state {{ established, related }} accept") + todo.append(f"add rule inet filter forward_to_{interface} ip saddr {local_ips} drop") + else: + todo.append(f"add rule inet filter forward_from_{interface} ip daddr {local_ips} drop") + todo.append(f"add rule inet filter forward_to_{interface} ip saddr {local_ips} drop") + + if wan: + todo.append(f"add rule inet filter forward_from_{interface} accept") + + todo.append(f"add rule inet filter forward_to_{interface} ct state {{ established, related }} accept") + else: + todo.append(f"add rule inet filter forward_from_{interface} drop") + + todo.append(f"add rule inet filter forward_to_{interface} drop") + nft("\n".join(todo)) + + +######################## +# Using vm # +######################## + +def ssh_args(ident: Identification, *arg: tuple[str, ...]): + ident.set_default_user() + vm, user = ident vm = name_to_id(vm) arg = ['ssh', "-i", vm_dir(vm)+"id_ed25519", "-o", f"UserKnownHostsFile={vm_dir(vm)}/known_hosts", "-o", "HostKeyAlgorithms=ssh-ed25519", "-o", f"HostKeyAlias=vm_{vm}", @@ -421,21 +825,21 @@ def ssh_args(vm: str, *arg: tuple[str, ...], user: str = "u"): @cmd -def ssh(vm: str, *arg: tuple[str, ...], user: str = "u"): - subprocess.run(ssh_args(vm, *arg, user=user)) +def ssh(ident: Identification, *arg: tuple[str, ...]): + subprocess.run(ssh_args(ident, *arg)) sshfs_root = lambda: os.environ["HOME"]+f"/m/vm/" -@cmd -def sshfs_mountdir(vm: str, user: str = "u"): - return sshfs_root()+f"/{user}@{name(vm)}" +@internal_cmd +def sshfs_mountdir(ident: Identification): + return sshfs_root()+f"/{ident.user}@{name(ident.vm)}" @cmd -def sshfs(vm: str, user: str = None): - vm, user = extended_name(vm, user) +def sshfs(vm: str): + vm, user = ident if user is None: - sshfs(vm, "root") - sshfs(vm, "u") + sshfs(Identification(vm, "root")) + sshfs(Identification(vm, "u")) return mount_dir = sshfs_mountdir(vm, user) if os.path.isdir(mount_dir) and len(os.listdir(mount_dir)) != 0: @@ -457,27 +861,48 @@ def sshfs_clean(): r("rm", root+f) -def escape_sh(*arg): - return " ".join("'" + s.replace("'", "'\"'\"'") + "'" for s in arg) - -def get_vnc_client_env(vm): +def get_vnc_client_env(ident): vnc_client_env = os.environ.copy() - vnc_client_env["VNC_PASSWORD"] = open(vm_dir(vm)+"vnc_passwd", "r").read().strip() + vnc_client_env["VNC_PASSWORD"] = open(vm_dir(ident.vm)+"vnc_passwd", "r").read().strip() + vnc_client_env["VNC_USERNAME"] = ident.user + vnc_client_env["VM_IDENT"] = str(ident) return vnc_client_env vncviewer_args = ["-FullscreenSystemKeys=0", "-AcceptClipboard=0", "-SendClipboard=0"] +def start_vnc_server(ident: Identification, unit_name: str, wayland: bool = False) -> (Any, Identification): + import random + import psutil + ident.set_default_user() + if wayland: + # echo enable_auth=true; echo \"password=$(cat /vnc_passwd)\"; echo username=u + vnc_server = subprocess.Popen(ssh_args(ident, f"systemd-run --unit {unit_name} --user -P bash -c '(echo enable_auth=true; echo \"password=$(cat /vnc_passwd)\"; echo username=u) > ~/.config/wayvnc/config ; WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 sway -c ~/.config/sway/vnc.config'"), stdout=subprocess.PIPE) + ident.wayland_display = vnc_server.stdout.readline().decode("utf-8").strip() + ident.sway_socket = vnc_server.stdout.readline().decode("utf-8").strip() + display_id = int(ident.wayland_display[8:]) + ident.vnc_port = 5800+display_id + print(f"WAYLAND_DISPLAY={ident.wayland_display}") + print(f"SWAYSOCK={ident.sway_socket}") + else: + display_id = int(ident.display[:1]) if ident.display else random.randint(10, 50) + vnc_server = subprocess.Popen(ssh_args(ident, f"systemd-run --unit {unit_name} --user -P bash -c '(cat /vnc_passwd;echo; cat /vnc_passwd; echo;echo n) | vncpasswd; vncserver :{display_id}'")) + ident.display = f":{display_id}" + ident.vnc_port = 5900 + display_id + return vnc_server, ident + +def start_vnc_client(ident: Identification, variant="vncviewer"): + #vnc_client = subprocess.Popen(["remmina", "vnc://"+get_ip(vm)+f"::{port}"], env=get_vnc_client_env(vm)) + return subprocess.Popen(["vncviewer", get_ip(ident.vm)+f"::{ident.vnc_port}", *vncviewer_args], env=get_vnc_client_env(ident)) + @cmd -def vncapp(vm: str, cmd: str, user: str = "u"): +def vncapp(ident: Identification, cmd: str, wayland: bool = False): import random import psutil unit_id = random.randint(100000, 999999) - vm, user = extended_name(vm, user=user) - display_id=random.randint(10, 50) - vnc_server = subprocess.Popen(ssh_args(vm, f"systemd-run --unit vncapp-vnc-{display_id}-{unit_id} --user -P bash -c '(cat /vnc_passwd;echo; cat /vnc_passwd; echo;echo n) | vncpasswd; vncserver :{display_id}'", user=user)) + vnc_server, ident = start_vnc_server(ident, f"vncapp-vnc-{unit_id}", wayland=wayland) time.sleep(1) - app = subprocess.Popen(ssh_args(vm, f"systemd-run --unit vncapp-app-{display_id}-{unit_id} --user -P -E DISPLAY=:{display_id} bash -c {escape_sh(cmd)}", user=user)); - vnc_client = subprocess.Popen(["vncviewer", get_ip(vm)+f":{display_id}", *vncviewer_args], env=get_vnc_client_env(vm)) + app = subprocess.Popen(ssh_args(ident, f"systemd-run --unit vncapp-app-{unit_id} --user -P -E DISPLAY={ident.display} -E WAYLAND_DISPLAY={ident.wayland_display} bash -c {escape_sh(cmd)}")); + vnc_client = start_vnc_client(ident) def on_terminate(proc): if verbose: print(f"KILLING ALL APPS because {proc} terminated") @@ -486,17 +911,26 @@ def vncapp(vm: str, cmd: str, user: str = "u"): app.send_signal(15) psutil.wait_procs([vnc_client, app, vnc_server], callback=on_terminate) - ssh(vm, f"systemctl --user stop vncapp-vnc-{display_id}-{unit_id} vncapp-app-{display_id}-{unit_id}") + ssh(ident, f"systemctl --user stop vncapp-vnc-{unit_id} vncapp-app-{unit_id}") + + +# WAYLAND_DISPLAY=wayland-1 SWAYSOCK=/run/user/1000/sway-ipc.1000.6544.sock swaymsg output HEADLESS-1 pos 0 0 res 1920x1080 + +@cmd +def waydroid(vm: str, *apps: tuple[str, ...]): + if len(apps): + return vncapp(vm, f"(sleep 14; waydroid app launch {apps[0]}) & waydroid show-full-ui", wayland=True) + else: + return vncapp(vm, f"waydroid show-full-ui", wayland=True) @cmd -def vncsession(vm: str, display_id: int =0, user: str = "u"): +def vncsession(ident: Identification, wayland: bool = False): import random import psutil unit_id = random.randint(100000, 999999) - vm, user = extended_name(vm, user=user) - vnc_server = subprocess.Popen(ssh_args(vm, f"systemd-run --unit vncsession-{display_id}-{unit_id} --user -P bash -c '(cat /vnc_passwd;echo; cat /vnc_passwd; echo;echo n) | vncpasswd; vncserver :{display_id}'", user=user)) + vnc_server, ident = start_vnc_server(ident, f"vncsession-{unit_id}", wayland=wayland) time.sleep(1) - vnc_client = subprocess.Popen(["vncviewer", get_ip(vm)+f":{display_id}", *vncviewer_args], env=get_vnc_client_env(vm)) + vnc_client = start_vnc_client(ident) def on_terminate(proc): if verbose: print("KILLING ALL APPS") @@ -504,18 +938,46 @@ def vncsession(vm: str, display_id: int =0, user: str = "u"): vnc_client.send_signal(15) psutil.wait_procs([vnc_client, vnc_server], callback=on_terminate) - ssh(vm, f"systemctl --user stop vncsession-{display_id}-{unit_id}") + ssh(ident, f"systemctl --user stop vncsession-{unit_id}") +@daemon() +def chown_qemu_vnc_sock(ucred, vm): + vm = name_to_id(vm) + assert has_write_acces(ucred, vm) + r("chown", f"{ucred.uid}", vm_dir(vm)+"qemu-vnc") +@cmd +def vnc(ident: Identification): + vm, _ = ident + chown_qemu_vnc_sock(vm) + r("vncviewer", f"{vm_dir(vm)}/qemu-vnc", *vncviewer_args) + +def str_remove_prefix(s, prefix): + assert s.startswith(prefix) + return s[len(prefix):] + +@internal_cmd +def get_vm_by_window(win_id: int = None): + import psutil + if win_id is None: + win_id = int(subprocess.check_output(["xdotool", "getactivewindow"]).decode("utf-8")) + win_class = str_remove_prefix(subprocess.check_output(["xprop", "-id", str(win_id), "WM_CLASS"]).decode("utf-8"), "WM_CLASS(STRING) = ") + pid = int(str_remove_prefix(subprocess.check_output(["xprop", "-id", str(win_id), "_NET_WM_PID"]).decode("utf-8"), "_NET_WM_PID(CARDINAL) = ")) + if '"Alacritty"' in win_class: + pass + else: + process = psutil.Process(pid=os.getpid()) + process_env: Dict = process.environ() + print(process_env) + return win_class -def all_virtuals(): - for f in os.listdir(root_folder): - if f.endswith(".vm"): - vm = f[:-3] - if is_valid_id(vm): - yield vm + +######################## +# High-level api # +######################## def terminal_len(val: str) -> int: + # TODO return len(val) def format_table(table): @@ -534,7 +996,7 @@ def index(color: bool = True): out = [] for vm in all_virtuals(): out_state = state(vm) - if out_state in [state_running]: + if out_state in [state_running, "active"]: if try_ping(vm): out_state += " (pinging)" else: @@ -551,155 +1013,15 @@ def clean(ucred): for vm in all_virtuals(): if has_write_acces(ucred, vm): permanency = get_permanency(vm) - if permanency == "tmp": - if state(ucred, vm) in [state_powered_off, state_not_registered, state_aborted, state_saved]: + if permanency in ["tmp", "prepared"]: + if state(ucred, vm) in [state_powered_off, state_not_registered, state_aborted, state_saved, "inactive"]: remove_force(ucred, vm) if permanency.startswith("init "): init_time = int(permanency[5:]) if time.time() - init_time > 100: remove_force(ucred, vm) - -@cmd -@daemon() -def remove_net(ucred, vm: str): - vm = name_to_id(vm) - assert has_write_acces(ucred, vm) - network_dir = vm_dir(vm)+"network/" - if os.path.isfile(network_dir+"interface"): - interface = open(network_dir+"interface").read().strip() - if os.path.isfile(network_dir+"interface"): - interface = open(network_dir+"interface").read().strip() - if open(network_dir+"boot_id", "r").read().strip() == boot_id: - if os.path.isfile(network_dir+"dhcp.pid"): - r("kill", open(network_dir+"dhcp.pid").read().strip(), check=False) - net_id = int(open(network_dir+"net_id", "r").read()) - r("ip", "addr", "del", f"10.37.{net_id}.1/24", "dev", interface) - - - r("VBoxManage", "hostonlyif", "remove", interface) - r('rm', '-r', network_dir) - -@cmd -@daemon() -def create_net(ucred, vm: str): - vm = name_to_id(vm) - assert has_write_acces(ucred, vm) - - remove_net(ucred, vm) - - network_dir = vm_dir(vm)+"network/" - os.mkdir(network_dir) - - if os.path.isfile(network_dir+"interface"): - interface = open(network_dir+"interface").read().strip() - else: - p = subprocess.run(["VBoxManage", "hostonlyif", "create"], capture_output=True, encoding='utf8') - if p.returncode: - print(p.stderr, file=sys.stderr) - raise RuntimeError() - interface = p.stdout.split("'")[1] - with open(network_dir+"interface", "w") as f: - f.write(interface) - - net_id = int(interface[7:]) - - if verbose: print("interface", interface) - - r("VBoxManage", "hostonlyif", "ipconfig", interface, f"--ip=10.37.{net_id}.1", "--netmask=255.255.255.0") - - r("sysctl", "net.ipv4.ip_forward=1") - - - with open(network_dir+"boot_id", "w") as f: f.write(boot_id) - with open(network_dir+"net_id", "w") as f: f.write(str(net_id)) - - #r("ip", "link", "add", f"v{net_id}h", "type", "veth", "peer", "name", f"v{net_id}g") - #r("ip", "link", "add", f"v{net_id}b", "type", "bridge") - #r("ifconfig", f"v{net_id}h", "up") - #r("ip", "link", "set", f"v{net_id}g", "master", f"v{net_id}b") - r("ifconfig", interface, f"10.37.{net_id}.1", "netmask", "255.255.255.0", "up"); - nft(f"") - nft(S-f""" - add chain inet filter input_from_{interface} - add chain inet filter forward_from_{interface} - add chain inet filter forward_to_{interface} - - insert rule inet filter input iifname {interface} jump input_from_{interface} - insert rule inet filter forward iifname {interface} jump forward_from_{interface} - insert rule inet filter forward oifname {interface} jump forward_to_{interface} - """) - modify_net(ucred, vm) - #nft("add rule inet filter forward iifname wlp1s0 accept") - r("VBoxManage", "modifyvm", vm, "--nic1=hostonly", f"--host-only-adapter1={interface}") - #r("VBoxManage", "modifyvm", vm, "--nic1=bridged", f"--bridgeadapter1=v{net_id}b") - - with open(network_dir+"dhcp.lp", "w") as f: f.write("authoring-byte-order little-endian;") - with open(network_dir+"dhcp.pid", "w") as f: f.write("") - with open(network_dir+"dhcp.config", "w") as f: - f.write(S-f""" - option domain-name-servers 8.8.8.8, 8.8.4.4; - option subnet-mask 255.255.255.0; - option routers 10.37.{net_id}.1; - subnet 10.37.{net_id}.0 netmask 255.255.255.0 {{ - range 10.37.{net_id}.150 10.37.{net_id}.250; - }} - """) - r("dhcpd", "-4", "-cf", network_dir+"dhcp.config", "-pf", network_dir+"dhcp.pid", "-lf", network_dir+"dhcp.lp", interface) - -@cmd -@daemon() -def modify_net(ucred, vm: str, wan: bool = False, lan: bool = False, pc: bool = False, pc_all: bool = False): - vm = name_to_id(vm) - assert has_write_acces(ucred, vm) - assert not (pc_all and not pc) - network_dir = vm_dir(vm)+"network/" - interface = open(network_dir+"interface").read().strip() - net_id = open(network_dir+"net_id").read().strip() - assert open(network_dir+"boot_id", "r").read().strip() == boot_id - - local_ips = "{10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16}" - if wan: - pass - todo = [f"flush chain inet filter input_from_{interface}", - f"flush chain inet filter forward_from_{interface}", - f"flush chain inet filter forward_to_{interface}"] - todo.append(f"add rule inet filter input_from_{interface} ct state {{ established, related }} accept") - if not pc: - todo.append(f"add rule inet filter input_from_{interface} drop") - if pc_all: - todo.append(f"add rule inet filter input_from_{interface} accept") - - if lan: - todo.append(f"add rule inet filter forward_from_{interface} ip daddr {local_ips} accept") - todo.append(f"add rule inet filter forward_to_{interface} ip saddr {local_ips} ct state {{ established, related }} accept") - todo.append(f"add rule inet filter forward_to_{interface} ip saddr {local_ips} drop") - else: - todo.append(f"add rule inet filter forward_from_{interface} ip daddr {local_ips} drop") - todo.append(f"add rule inet filter forward_to_{interface} ip saddr {local_ips} drop") - - if wan: - todo.append(f"add rule inet filter forward_from_{interface} accept") - - todo.append(f"add rule inet filter forward_to_{interface} ct state {{ established, related }} accept") - else: - todo.append(f"add rule inet filter forward_from_{interface} drop") - - todo.append(f"add rule inet filter forward_to_{interface} drop") - nft("\n".join(todo)) - -@cmd -@daemon() -def start(ucred, vm: str): - vm = name_to_id(vm) - assert has_write_acces(ucred, vm) - if not os.path.exists(vm_dir(vm)+"network/boot_id") or open(vm_dir(vm)+"network/boot_id", "r").read().strip() != boot_id: - create_net(ucred, vm) - r("VBoxManage", "startvm", vm, "--type=headless") - if get_permanency(vm).startswith("init "): - set_permanency(ucred, vm, "tmp") - -@cmd +@internal_cmd @daemon() def try_ping(ucred, vm: str) -> bool: vm = name_to_id(vm) @@ -709,73 +1031,50 @@ def try_ping(ucred, vm: str) -> bool: r = ping(ip, verbose=True, count=1, timeout=0.1) return r.packets_lost == 0 -@cmd +@internal_cmd def wait_started(vm: str): while True: if try_ping(vm): break + time.sleep(0.5) -@cmd -def start_and_wait(vm: str): +@internal_cmd +def start_and_wait(vm: str, display: bool = None): vm = name_to_id(vm) - start(vm) + start(vm, display=display) wait_started(vm) -@cmd -@daemon() -def poweroff(ucred, vm: str): - vm = name_to_id(vm) - assert has_write_acces(ucred, vm) - r("VBoxManage", "controlvm", vm, "acpipowerbutton") - -@cmd -def poweroff_and_wait(vm: str): - vm = name_to_id(vm) - if state(vm) in [state_powered_off, state_not_registered, state_aborted, state_saved]: return - poweroff(vm) - while True: - time.sleep(0.1) - s = state(vm) - print("state", s) - if s == state_powered_off: - time.sleep(1) - return - -def poweroff_and_wait_daemon(ucred, vm: str): - vm = name_to_id(vm) - if state(ucred, vm) in [state_powered_off, state_not_registered, state_aborted, state_saved]: return - poweroff(ucred, vm) - while True: - time.sleep(0.1) - s = state(ucred, vm) - print("state", s) - if s == state_powered_off: - time.sleep(1) - return - -@cmd +@internal_cmd @daemon() def get_tmp_name(ucred) -> str: last_id = int(open("last_tmp_id").read().strip()) - last_id = (last_id+1) % 1000 - while os.path.exists(f"tmp-{last_id}.vm"): + last_id = (last_id+1) % 100 + while os.path.exists(f"tmp{last_id}.vm"): last_id = (last_id+1) % 1000 with open("last_tmp_id", "w") as f: f.write(str(last_id)) - return f"tmp-{last_id}" + return f"tmp{last_id}" if is_daemon: + class PreparedFork: + def __init__(self, vm): + self.vm = vm + self.start_monotonic_time = time.monotonic() + self.is_paused = False prepared_forks = {} -@cmd +@internal_cmd @daemon() def get_prepared_fork(ucred, base: str = "base") -> Optional[str]: name_to_id(base) assert has_read_acces(ucred, base) if base in prepared_forks and len(prepared_forks[base]): - vm = prepared_forks[base][0] + pf = prepared_forks[base][0] prepared_forks[base] = prepared_forks[base][1:] - give_to_user(vm, ucred.uid, ucred.gid) - return vm + if pf.is_paused: + resume(ucred, pf.vm) + give_to_user(pf.vm, ucred.uid, ucred.gid) + set_permanency(ucred, pf.vm, "tmp") + return pf.vm @cmd @daemon(root_only=True) @@ -786,11 +1085,20 @@ def prepare_forks(ucred, base: str = "base", count: int =1) -> Optional[str]: target = get_tmp_name(ucred) target = clone(ucred, target, base=base) start(ucred, target) - prepared_forks[base].append(target) + pf = PreparedFork(target) + prepared_forks[base].append(pf) + set_permanency(ucred, target, "prepared") return target - @cmd +@daemon(root_only=True) +def pause_prepared_forks(ucred, base: str = "base", runtime: int = 30): + for pf in prepared_forks[base]: + if not pf.is_paused and pf.start_monotonic_time + runtime <= time.monotonic(): + pause(ucred, pf.vm) + pf.is_paused = True + +@internal_cmd def get_tmp_vm(base: str = "base"): target = get_prepared_fork(base) if not target: @@ -800,17 +1108,34 @@ def get_tmp_vm(base: str = "base"): return target @cmd -def extended_name(vm: str, user: str = "u") -> tuple[str, str]: +def extended_name(name: str) -> tuple[str, str]: assert not is_daemon + vm = name + user = None if len(vm.split("@"))==2: user, vm = vm.split("@") do_power_on = False + do_power_on_display= False + net_options = None + permanency = None + if len(vm.split("$"))==2: + vm, tmp = vm.split("$") + do_power_on = True + do_power_on_display = True + assert tmp == "" + if len(vm.split("!"))==2: vm, tmp = vm.split("!") do_power_on = True assert tmp == "" + if len(vm.split("~"))==2: + vm, net_options = vm.split("~") + + if len(vm.split("^"))==2: + vm, permanency = vm.split("^") + if len(vm.split("+"))==2: base, vm = vm.split("+") if not vm : @@ -824,8 +1149,20 @@ def extended_name(vm: str, user: str = "u") -> tuple[str, str]: if do_power_on: if state(vm) != state_running: - start_and_wait(vm) - return vm, user + if state(vm) != state_paused: + start_and_wait(vm, display=do_power_on_display) + else: + resume(vm) + if net_options is not None: + modify_net(vm, wan="w" in net_options, lan="l" in net_options, pc="p" in net_options or "P" in net_options, pc_all="P" in net_options) + if permanency is not None: + set_permanency(vm, permanency or "stable") + return Identification(vm, user) + +@cmd +def eval(ident: Identification): + return str(ident) + @daemon() def remove_force(ucred, vm: str, keep_image: bool = False): @@ -834,7 +1171,7 @@ def remove_force(ucred, vm: str, keep_image: bool = False): raise RuntimeError("Delete foribidden") assert has_write_acces(ucred, vm) if state(ucred, vm) != state_not_registered: - r("VBoxManage", "unregistervm", vm, "--delete-all") + unregister_vm(ucred, vm) remove_net(ucred, vm) if keep_image: for f in os.listdir(vm+".vm"): @@ -858,6 +1195,7 @@ def exit_server(ucred): for vm in all_virtuals(): poweroff_and_wait_daemon(ucred, vm) remove_net(ucred, vm) + unregister_vm(ucred, vm) exit(0) @cmd @@ -898,53 +1236,83 @@ def run(vm: str, prog: str, *arg: tuple[str, ...], gui: bool = False, out_file: host_file = it shutil.copy(tmp_dir+"/"+vm_file, host_file) +@internal_cmd +def qemu_monitor(vm: str): + vm, _ = extended_name(vm) + r("socat", "-,echo=0,icanon=0", f"unix-connect:{vm_dir(vm)}qemu-monitor") + +@internal_cmd +def qemu_cmd(vm: str, cmd: str): + import socket + client = socket.socket( socket.AF_UNIX) + print(f"{vm_dir(vm)}qemu-monitor") + client.connect(f"{vm_dir(vm)}qemu-monitor") + client.send((cmd+'\n').encode('utf-8')) + time.sleep(1) + r = client.recv(1024).decode('utf-8') + client.close() + print(r) + return "\n".join(r.split("\n")[2:-1]) + ########################################################## def run_args(args): + import inspect if verbose: print(args) if not args.subcommand: parser.print_help() + return + if args.subcommand == "internal": + if not args.subcommand_internal: + parser_internal.print_help() + return + f = subcommands_internal[args.subcommand_internal] else: - import inspect f = subcommands[args.subcommand] - spec = get_spec(f) - f_kvarg = {} - f_arg = [] + spec = get_spec(f) + f_kvarg = {} + f_arg = [] - def process_arg(name, has_default, default): - if has_default: - if args.__dict__[name] is not None: - f_kvarg[name] = args.__dict__[name] - else: - f_arg.append(args.__dict__[name]) - - for i, arg in enumerate(spec.args): - has_default = spec.defaults is not None and i >= len(spec.args) - len(spec.defaults) - default = None - if has_default: - default = spec.defaults[i - len(spec.args) + len(spec.defaults)] - process_arg(arg, has_default, default) - - for i, arg in enumerate(spec.kwonlyargs): - default = spec.kwonlydefaults[arg] - process_arg(arg, True, default) - - if spec.varargs is not None: - arg = spec.varargs - annotation = spec.annotations.get(arg, None) - if annotation == tuple[str, ...]: - f_arg += args.__dict__[arg] - - if verbose: print(f_arg, f_kvarg) - r = f(*f_arg, **f_kvarg) - if r is not None: - if isinstance(r, tuple) or isinstance(r, list): - for i in r: - print(i) - else: - print(r) + def process_arg(name, has_default, default): + if has_default and args.__dict__[name] is None: + return + val = args.__dict__[name] + annotation = spec.annotations.get(name, None) + if annotation in [Identification]: + val = extended_name(val) + if has_default: + if args.__dict__[name] is not None: + f_kvarg[name] = val + else: + f_arg.append(val) + + for i, arg in enumerate(spec.args): + has_default = spec.defaults is not None and i >= len(spec.args) - len(spec.defaults) + default = None + if has_default: + default = spec.defaults[i - len(spec.args) + len(spec.defaults)] + process_arg(arg, has_default, default) + + for i, arg in enumerate(spec.kwonlyargs): + default = spec.kwonlydefaults[arg] + process_arg(arg, True, default) + + if spec.varargs is not None: + arg = spec.varargs + annotation = spec.annotations.get(arg, None) + if annotation == tuple[str, ...]: + f_arg += args.__dict__[arg] + + if verbose: print(f_arg, f_kvarg) + r = f(*f_arg, **f_kvarg) + if r is not None: + if isinstance(r, tuple) or isinstance(r, list): + for i in r: + print(i) + else: + print(r) @cmd def run_periodically(delay: int, *argv: tuple[str, ...]): @@ -985,20 +1353,19 @@ def main_daemon(): ucred = Ucred(pid, uid, gid) in_data = recvall(connection) - print("IN", in_data) in_struct = json.loads(in_data) print("IN", in_struct) + assert in_struct["backend"] == backend f = daemon_funcs[in_struct["fname"]] try: res = f(ucred, *in_struct["arg"], **in_struct["kvarg"]) except Exception as e: traceback.print_exception(e) - out_struct = {'excepttion': str(type(e))} + out_struct = {'exception': str(type(e))} else: out_struct = {'return': res} print("OUT", out_struct) out_data = json.dumps(out_struct).encode('utf-8') - print("OUT", out_data) sys.stdout.flush() sys.stderr.flush() try: diff --git a/vm.service b/vm.service index 2a5a2679342bc91be161ade92228854457e90f8b..7197e30a92e33aabaf3027f33ee502cb41c5da24 100644 --- a/vm.service +++ b/vm.service @@ -4,9 +4,10 @@ After=network.target [Service] -Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/sbin:/usr/bin:/bin:/home/jiri/bin +Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/sbin:/usr/bin:/bin +Environment=VM_BACKEND=qemu WorkingDirectory=/mnt/virtual -ExecStart=/mnt/virtual/prog/vm.py server 'run_periodically 10 -- prepare_forks --base base --count 1' 'run_periodically 30 -- clean' +ExecStart=/mnt/virtual/prog/vm.py server 'run_periodically 10 -- prepare_forks --base base --count 1' 'run_periodically 30 -- clean' 'run_periodically 30 -- pause_prepared_forks --base base --runtime 40' Restart=always RestartSec=10