[pve-devel] [PATCH qemu-server 16/31] introduce RunState module

Fiona Ebner f.ebner at proxmox.com
Wed Jun 25 17:56:39 CEST 2025


For now, move only the vm_resume() and vm_suspend() functions. Others
like vm_stop() and friends, vm_reboot() and vm_start() would require
more preparation.

Apart from slightly improving modularity, this is in preparation to
add a BlockJob module, where vm_resume() and vm_suspend() need to be
called after drive-mirror, to avoid a cyclic dependency with the main
QemuServer module.

Signed-off-by: Fiona Ebner <f.ebner at proxmox.com>
---
 src/PVE/API2/Qemu.pm           |   7 +-
 src/PVE/CLI/qm.pm              |   3 +-
 src/PVE/QemuServer.pm          | 175 +------------------------------
 src/PVE/QemuServer/Makefile    |   1 +
 src/PVE/QemuServer/RunState.pm | 185 +++++++++++++++++++++++++++++++++
 5 files changed, 196 insertions(+), 175 deletions(-)
 create mode 100644 src/PVE/QemuServer/RunState.pm

diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
index 27426eab..de762cca 100644
--- a/src/PVE/API2/Qemu.pm
+++ b/src/PVE/API2/Qemu.pm
@@ -42,6 +42,7 @@ use PVE::QemuServer::OVMF;
 use PVE::QemuServer::PCI;
 use PVE::QemuServer::QMPHelpers;
 use PVE::QemuServer::RNG;
+use PVE::QemuServer::RunState;
 use PVE::QemuServer::USB;
 use PVE::QemuServer::Virtiofs qw(max_virtiofs);
 use PVE::QemuMigrate;
@@ -3934,7 +3935,7 @@ __PACKAGE__->register_method({
 
             syslog('info', "suspend VM $vmid: $upid\n");
 
-            PVE::QemuServer::vm_suspend($vmid, $skiplock, $todisk, $statestorage);
+            PVE::QemuServer::RunState::vm_suspend($vmid, $skiplock, $todisk, $statestorage);
 
             return;
         };
@@ -4011,7 +4012,7 @@ __PACKAGE__->register_method({
             syslog('info', "resume VM $vmid: $upid\n");
 
             if (!$to_disk_suspended) {
-                PVE::QemuServer::vm_resume($vmid, $skiplock, $nocheck);
+                PVE::QemuServer::RunState::vm_resume($vmid, $skiplock, $nocheck);
             } else {
                 my $storecfg = PVE::Storage::config();
                 PVE::QemuServer::vm_start($storecfg, $vmid, { skiplock => $skiplock });
@@ -6642,7 +6643,7 @@ __PACKAGE__->register_method({
                 },
                 'resume' => sub {
                     if (PVE::QemuServer::Helpers::vm_running_locally($state->{vmid})) {
-                        PVE::QemuServer::vm_resume($state->{vmid}, 1, 1);
+                        PVE::QemuServer::RunState::vm_resume($state->{vmid}, 1, 1);
                     } else {
                         die "VM $state->{vmid} not running\n";
                     }
diff --git a/src/PVE/CLI/qm.pm b/src/PVE/CLI/qm.pm
index 23f71ab0..f3e9a702 100755
--- a/src/PVE/CLI/qm.pm
+++ b/src/PVE/CLI/qm.pm
@@ -36,6 +36,7 @@ use PVE::QemuServer::Agent;
 use PVE::QemuServer::ImportDisk;
 use PVE::QemuServer::Monitor qw(mon_cmd);
 use PVE::QemuServer::QMPHelpers;
+use PVE::QemuServer::RunState;
 use PVE::QemuServer;
 
 use PVE::CLIHandler;
@@ -465,7 +466,7 @@ __PACKAGE__->register_method({
                 # check_running and vm_resume with nocheck, since local node
                 # might not have processed config move/rename yet
                 if (PVE::QemuServer::check_running($vmid, 1)) {
-                    eval { PVE::QemuServer::vm_resume($vmid, 1, 1); };
+                    eval { PVE::QemuServer::RunState::vm_resume($vmid, 1, 1); };
                     if ($@) {
                         $tunnel_write->("ERR: resume failed - $@");
                     } else {
diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
index 3c612b44..942a1363 100644
--- a/src/PVE/QemuServer.pm
+++ b/src/PVE/QemuServer.pm
@@ -81,6 +81,7 @@ use PVE::QemuServer::PCI qw(print_pci_addr print_pcie_addr print_pcie_root_port
 use PVE::QemuServer::QemuImage;
 use PVE::QemuServer::QMPHelpers qw(qemu_deviceadd qemu_devicedel qemu_objectadd qemu_objectdel);
 use PVE::QemuServer::RNG qw(parse_rng print_rng_device_commandline print_rng_object_commandline);
+use PVE::QemuServer::RunState;
 use PVE::QemuServer::StateFile;
 use PVE::QemuServer::USB;
 use PVE::QemuServer::Virtiofs qw(max_virtiofs start_all_virtiofsd);
@@ -5359,7 +5360,7 @@ sub vm_start {
             if ($has_backup_lock && $running) {
                 # a backup is currently running, attempt to start the guest in the
                 # existing QEMU instance
-                return vm_resume($vmid);
+                return PVE::QemuServer::RunState::vm_resume($vmid);
             }
 
             PVE::QemuConfig->check_lock($conf)
@@ -6165,174 +6166,6 @@ sub vm_reboot {
     );
 }
 
-# note: if using the statestorage parameter, the caller has to check privileges
-sub vm_suspend {
-    my ($vmid, $skiplock, $includestate, $statestorage) = @_;
-
-    my $conf;
-    my $path;
-    my $storecfg;
-    my $vmstate;
-
-    PVE::QemuConfig->lock_config(
-        $vmid,
-        sub {
-
-            $conf = PVE::QemuConfig->load_config($vmid);
-
-            my $is_backing_up = PVE::QemuConfig->has_lock($conf, 'backup');
-            PVE::QemuConfig->check_lock($conf)
-                if !($skiplock || $is_backing_up);
-
-            die "cannot suspend to disk during backup\n"
-                if $is_backing_up && $includestate;
-
-            PVE::QemuMigrate::Helpers::check_non_migratable_resources($conf, $includestate, 0);
-
-            if ($includestate) {
-                $conf->{lock} = 'suspending';
-                my $date = strftime("%Y-%m-%d", localtime(time()));
-                $storecfg = PVE::Storage::config();
-                if (!$statestorage) {
-                    $statestorage = PVE::QemuConfig::find_vmstate_storage($conf, $storecfg);
-                    # check permissions for the storage
-                    my $rpcenv = PVE::RPCEnvironment::get();
-                    if ($rpcenv->{type} ne 'cli') {
-                        my $authuser = $rpcenv->get_user();
-                        $rpcenv->check(
-                            $authuser,
-                            "/storage/$statestorage",
-                            ['Datastore.AllocateSpace'],
-                        );
-                    }
-                }
-
-                $vmstate = PVE::QemuConfig->__snapshot_save_vmstate(
-                    $vmid, $conf, "suspend-$date", $storecfg, $statestorage, 1,
-                );
-                $path = PVE::Storage::path($storecfg, $vmstate);
-                PVE::QemuConfig->write_config($vmid, $conf);
-            } else {
-                mon_cmd($vmid, "stop");
-            }
-        },
-    );
-
-    if ($includestate) {
-        # save vm state
-        PVE::Storage::activate_volumes($storecfg, [$vmstate]);
-
-        eval {
-            PVE::QemuMigrate::Helpers::set_migration_caps($vmid, 1);
-            mon_cmd($vmid, "savevm-start", statefile => $path);
-            for (;;) {
-                my $state = mon_cmd($vmid, "query-savevm");
-                if (!$state->{status}) {
-                    die "savevm not active\n";
-                } elsif ($state->{status} eq 'active') {
-                    sleep(1);
-                    next;
-                } elsif ($state->{status} eq 'completed') {
-                    print "State saved, quitting\n";
-                    last;
-                } elsif ($state->{status} eq 'failed' && $state->{error}) {
-                    die "query-savevm failed with error '$state->{error}'\n";
-                } else {
-                    die "query-savevm returned status '$state->{status}'\n";
-                }
-            }
-        };
-        my $err = $@;
-
-        PVE::QemuConfig->lock_config(
-            $vmid,
-            sub {
-                $conf = PVE::QemuConfig->load_config($vmid);
-                if ($err) {
-                    # cleanup, but leave suspending lock, to indicate something went wrong
-                    eval {
-                        eval { mon_cmd($vmid, "savevm-end"); };
-                        warn $@ if $@;
-                        PVE::Storage::deactivate_volumes($storecfg, [$vmstate]);
-                        PVE::Storage::vdisk_free($storecfg, $vmstate);
-                        delete $conf->@{qw(vmstate runningmachine runningcpu)};
-                        PVE::QemuConfig->write_config($vmid, $conf);
-                    };
-                    warn $@ if $@;
-                    die $err;
-                }
-
-                die "lock changed unexpectedly\n"
-                    if !PVE::QemuConfig->has_lock($conf, 'suspending');
-
-                mon_cmd($vmid, "quit");
-                $conf->{lock} = 'suspended';
-                PVE::QemuConfig->write_config($vmid, $conf);
-            },
-        );
-    }
-}
-
-# $nocheck is set when called as part of a migration - in this context the
-# location of the config file (source or target node) is not deterministic,
-# since migration cannot wait for pmxcfs to process the rename
-sub vm_resume {
-    my ($vmid, $skiplock, $nocheck) = @_;
-
-    PVE::QemuConfig->lock_config(
-        $vmid,
-        sub {
-            # After migration, the VM might not immediately be able to respond to QMP commands, because
-            # activating the block devices might take a bit of time.
-            my $res = mon_cmd($vmid, 'query-status', timeout => 60);
-            my $resume_cmd = 'cont';
-            my $reset = 0;
-            my $conf;
-            if ($nocheck) {
-                $conf = eval { PVE::QemuConfig->load_config($vmid) }; # try on target node
-                if ($@) {
-                    my $vmlist = PVE::Cluster::get_vmlist();
-                    if (exists($vmlist->{ids}->{$vmid})) {
-                        my $node = $vmlist->{ids}->{$vmid}->{node};
-                        $conf = eval { PVE::QemuConfig->load_config($vmid, $node) }; # try on source node
-                    }
-                    if (!$conf) {
-                        PVE::Cluster::cfs_update(); # vmlist was wrong, invalidate cache
-                        $conf = PVE::QemuConfig->load_config($vmid); # last try on target node again
-                    }
-                }
-            } else {
-                $conf = PVE::QemuConfig->load_config($vmid);
-            }
-
-            die "VM $vmid is a template and cannot be resumed!\n"
-                if PVE::QemuConfig->is_template($conf);
-
-            if ($res->{status}) {
-                return if $res->{status} eq 'running'; # job done, go home
-                $resume_cmd = 'system_wakeup' if $res->{status} eq 'suspended';
-                $reset = 1 if $res->{status} eq 'shutdown';
-            }
-
-            if (!$nocheck) {
-                PVE::QemuConfig->check_lock($conf)
-                    if !($skiplock || PVE::QemuConfig->has_lock($conf, 'backup'));
-            }
-
-            if ($reset) {
-                # required if a VM shuts down during a backup and we get a resume
-                # request before the backup finishes for example
-                mon_cmd($vmid, "system_reset");
-            }
-
-            PVE::QemuServer::Network::add_nets_bridge_fdb($conf, $vmid)
-                if $resume_cmd eq 'cont';
-
-            mon_cmd($vmid, $resume_cmd);
-        },
-    );
-}
-
 sub vm_sendkey {
     my ($vmid, $skiplock, $key) = @_;
 
@@ -7967,7 +7800,7 @@ sub qemu_drive_mirror_monitor {
                         warn $@ if $@;
                     } else {
                         print "suspend vm\n";
-                        eval { PVE::QemuServer::vm_suspend($vmid, 1); };
+                        eval { PVE::QemuServer::RunState::vm_suspend($vmid, 1); };
                         warn $@ if $@;
                     }
 
@@ -7980,7 +7813,7 @@ sub qemu_drive_mirror_monitor {
                         warn $@ if $@;
                     } else {
                         print "resume vm\n";
-                        eval { PVE::QemuServer::vm_resume($vmid, 1, 1); };
+                        eval { PVE::QemuServer::RunState::vm_resume($vmid, 1, 1); };
                         warn $@ if $@;
                     }
 
diff --git a/src/PVE/QemuServer/Makefile b/src/PVE/QemuServer/Makefile
index e30c571c..5f475c73 100644
--- a/src/PVE/QemuServer/Makefile
+++ b/src/PVE/QemuServer/Makefile
@@ -20,6 +20,7 @@ SOURCES=Agent.pm	\
 	QemuImage.pm	\
 	QMPHelpers.pm	\
 	RNG.pm		\
+	RunState.pm	\
 	StateFile.pm	\
 	USB.pm		\
 	Virtiofs.pm
diff --git a/src/PVE/QemuServer/RunState.pm b/src/PVE/QemuServer/RunState.pm
new file mode 100644
index 00000000..05e7fb47
--- /dev/null
+++ b/src/PVE/QemuServer/RunState.pm
@@ -0,0 +1,185 @@
+package PVE::QemuServer::RunState;
+
+use strict;
+use warnings;
+
+use POSIX qw(strftime);
+
+use PVE::Cluster;
+use PVE::RPCEnvironment;
+use PVE::Storage;
+
+use PVE::QemuConfig;
+use PVE::QemuMigrate::Helpers;
+use PVE::QemuServer::Monitor qw(mon_cmd);
+use PVE::QemuServer::Network;
+
+# note: if using the statestorage parameter, the caller has to check privileges
+sub vm_suspend {
+    my ($vmid, $skiplock, $includestate, $statestorage) = @_;
+
+    my $conf;
+    my $path;
+    my $storecfg;
+    my $vmstate;
+
+    PVE::QemuConfig->lock_config(
+        $vmid,
+        sub {
+
+            $conf = PVE::QemuConfig->load_config($vmid);
+
+            my $is_backing_up = PVE::QemuConfig->has_lock($conf, 'backup');
+            PVE::QemuConfig->check_lock($conf)
+                if !($skiplock || $is_backing_up);
+
+            die "cannot suspend to disk during backup\n"
+                if $is_backing_up && $includestate;
+
+            PVE::QemuMigrate::Helpers::check_non_migratable_resources($conf, $includestate, 0);
+
+            if ($includestate) {
+                $conf->{lock} = 'suspending';
+                my $date = strftime("%Y-%m-%d", localtime(time()));
+                $storecfg = PVE::Storage::config();
+                if (!$statestorage) {
+                    $statestorage = PVE::QemuConfig::find_vmstate_storage($conf, $storecfg);
+                    # check permissions for the storage
+                    my $rpcenv = PVE::RPCEnvironment::get();
+                    if ($rpcenv->{type} ne 'cli') {
+                        my $authuser = $rpcenv->get_user();
+                        $rpcenv->check(
+                            $authuser,
+                            "/storage/$statestorage",
+                            ['Datastore.AllocateSpace'],
+                        );
+                    }
+                }
+
+                $vmstate = PVE::QemuConfig->__snapshot_save_vmstate(
+                    $vmid, $conf, "suspend-$date", $storecfg, $statestorage, 1,
+                );
+                $path = PVE::Storage::path($storecfg, $vmstate);
+                PVE::QemuConfig->write_config($vmid, $conf);
+            } else {
+                mon_cmd($vmid, "stop");
+            }
+        },
+    );
+
+    if ($includestate) {
+        # save vm state
+        PVE::Storage::activate_volumes($storecfg, [$vmstate]);
+
+        eval {
+            PVE::QemuMigrate::Helpers::set_migration_caps($vmid, 1);
+            mon_cmd($vmid, "savevm-start", statefile => $path);
+            for (;;) {
+                my $state = mon_cmd($vmid, "query-savevm");
+                if (!$state->{status}) {
+                    die "savevm not active\n";
+                } elsif ($state->{status} eq 'active') {
+                    sleep(1);
+                    next;
+                } elsif ($state->{status} eq 'completed') {
+                    print "State saved, quitting\n";
+                    last;
+                } elsif ($state->{status} eq 'failed' && $state->{error}) {
+                    die "query-savevm failed with error '$state->{error}'\n";
+                } else {
+                    die "query-savevm returned status '$state->{status}'\n";
+                }
+            }
+        };
+        my $err = $@;
+
+        PVE::QemuConfig->lock_config(
+            $vmid,
+            sub {
+                $conf = PVE::QemuConfig->load_config($vmid);
+                if ($err) {
+                    # cleanup, but leave suspending lock, to indicate something went wrong
+                    eval {
+                        eval { mon_cmd($vmid, "savevm-end"); };
+                        warn $@ if $@;
+                        PVE::Storage::deactivate_volumes($storecfg, [$vmstate]);
+                        PVE::Storage::vdisk_free($storecfg, $vmstate);
+                        delete $conf->@{qw(vmstate runningmachine runningcpu)};
+                        PVE::QemuConfig->write_config($vmid, $conf);
+                    };
+                    warn $@ if $@;
+                    die $err;
+                }
+
+                die "lock changed unexpectedly\n"
+                    if !PVE::QemuConfig->has_lock($conf, 'suspending');
+
+                mon_cmd($vmid, "quit");
+                $conf->{lock} = 'suspended';
+                PVE::QemuConfig->write_config($vmid, $conf);
+            },
+        );
+    }
+}
+
+# $nocheck is set when called as part of a migration - in this context the
+# location of the config file (source or target node) is not deterministic,
+# since migration cannot wait for pmxcfs to process the rename
+sub vm_resume {
+    my ($vmid, $skiplock, $nocheck) = @_;
+
+    PVE::QemuConfig->lock_config(
+        $vmid,
+        sub {
+            # After migration, the VM might not immediately be able to respond to QMP commands, because
+            # activating the block devices might take a bit of time.
+            my $res = mon_cmd($vmid, 'query-status', timeout => 60);
+            my $resume_cmd = 'cont';
+            my $reset = 0;
+            my $conf;
+            if ($nocheck) {
+                $conf = eval { PVE::QemuConfig->load_config($vmid) }; # try on target node
+                if ($@) {
+                    my $vmlist = PVE::Cluster::get_vmlist();
+                    if (exists($vmlist->{ids}->{$vmid})) {
+                        my $node = $vmlist->{ids}->{$vmid}->{node};
+                        $conf = eval { PVE::QemuConfig->load_config($vmid, $node) }; # try on source node
+                    }
+                    if (!$conf) {
+                        PVE::Cluster::cfs_update(); # vmlist was wrong, invalidate cache
+                        $conf = PVE::QemuConfig->load_config($vmid); # last try on target node again
+                    }
+                }
+            } else {
+                $conf = PVE::QemuConfig->load_config($vmid);
+            }
+
+            die "VM $vmid is a template and cannot be resumed!\n"
+                if PVE::QemuConfig->is_template($conf);
+
+            if ($res->{status}) {
+                return if $res->{status} eq 'running'; # job done, go home
+                $resume_cmd = 'system_wakeup' if $res->{status} eq 'suspended';
+                $reset = 1 if $res->{status} eq 'shutdown';
+            }
+
+            if (!$nocheck) {
+                PVE::QemuConfig->check_lock($conf)
+                    if !($skiplock || PVE::QemuConfig->has_lock($conf, 'backup'));
+            }
+
+            if ($reset) {
+                # required if a VM shuts down during a backup and we get a resume
+                # request before the backup finishes for example
+                mon_cmd($vmid, "system_reset");
+            }
+
+            PVE::QemuServer::Network::add_nets_bridge_fdb($conf, $vmid)
+                if $resume_cmd eq 'cont';
+
+            mon_cmd($vmid, $resume_cmd);
+        },
+    );
+}
+
+1;
-- 
2.47.2





More information about the pve-devel mailing list