diff --git a/flake.lock b/flake.lock index 118664f..20ff226 100644 --- a/flake.lock +++ b/flake.lock @@ -180,11 +180,11 @@ }, "nixpkgs-mine": { "locked": { - "lastModified": 1651928992, - "narHash": "sha256-U9IgIFKj1W6YBsoFy3AG7YaxBw9qQTYeGhvlLcKfUYQ=", + "lastModified": 1651945192, + "narHash": "sha256-3pnzK9RpuWzqnqd1U7zt/z3gvn/UNY41CJuS6Ow/Vwo=", "owner": "devplayer0", "repo": "nixpkgs", - "rev": "e0844396c59d27f26f18b8ee4bff4fdda0e39cfa", + "rev": "95bce541ae41d144b8edb6c47b3b7987295b006f", "type": "github" }, "original": { diff --git a/nixos/modules/vms.nix b/nixos/modules/vms.nix index 50609ad..71c6c08 100644 --- a/nixos/modules/vms.nix +++ b/nixos/modules/vms.nix @@ -12,6 +12,32 @@ let qemuOpts = with lib.types; coercedTo (attrsOf unspecified) flattenQEMUOpts str; extraQEMUOpts = o: optionalString (o != "") ",${o}"; + doCleanShutdown = + let + pyEnv = pkgs.python310.withPackages (ps: with ps; [ qemu ]); + in + pkgs.writeScript "qemu-clean-shutdown" '' + #!${pyEnv}/bin/python + import sys + import os + + import qemu.qmp + + if len(sys.argv) != 2: + print(f'usage: {sys.argv[0]} ', file=sys.stderr) + sys.exit(1) + + if not os.path.exists(sys.argv[1]) and 'MAINPID' not in os.environ: + # Special case: systemd is calling us after QEMU exited on its own + sys.exit(0) + + with qemu.qmp.QEMUMonitorProtocol(sys.argv[1]) as mon: + mon.connect() + mon.command('system_powerdown') + while mon.pull_event(wait=True)['event'] != 'SHUTDOWN': + pass + ''; + cfg = config.my.vms; netOpts = with lib.types; { name, ... }: { @@ -39,6 +65,10 @@ let qemuBin = mkOpt' path "${pkgs.qemu_kvm}/bin/qemu-kvm" "Path to QEMU executable."; qemuFlags = mkOpt' (listOf str) [ ] "Additional flags to pass to QEMU."; autoStart = mkBoolOpt' true "Whether to start the VM automatically at boot."; + cleanShutdown = { + enabled = mkBoolOpt' true "Whether to attempt to cleanly shut down the guest."; + timeout = mkOpt' ints.unsigned 30 "Clean shutdown timeout (in seconds)."; + }; machine = mkOpt' str "q35" "QEMU machine type."; enableKVM = mkBoolOpt' true "Whether to enable KVM."; @@ -68,8 +98,10 @@ let "m ${toString i.memory}" "nographic" "vga ${i.vga}" + "chardev socket,id=monitor-qmp,path=/run/vms/${n}/monitor-qmp.sock,server=on,wait=off" + "mon chardev=monitor-qmp,mode=control" "chardev socket,id=monitor,path=/run/vms/${n}/monitor.sock,server=on,wait=off" - "mon chardev=monitor" + "mon chardev=monitor,mode=readline" "chardev socket,id=tty,path=/run/vms/${n}/tty.sock,server=on,wait=off" "device isa-serial,chardev=tty" ] ++ @@ -108,6 +140,9 @@ in description = "Virtual machine '${n}'"; serviceConfig = { ExecStart = mkQemuCommand n i; + ExecStop = mkIf i.cleanShutdown.enabled "${doCleanShutdown} /run/vms/${n}/monitor-qmp.sock"; + TimeoutStopSec = mkIf i.cleanShutdown.enabled i.cleanShutdown.timeout; + RuntimeDirectory = "vms/${n}"; StateDirectory = "vms/${n}"; }; @@ -120,7 +155,7 @@ in ''; postStart = '' - socks=(monitor tty spice) + socks=(monitor-qmp monitor tty spice) for s in ''${socks[@]}; do path="$RUNTIME_DIRECTORY"/''${s}.sock until [ -e "$path" ]; do sleep 0.1; done