* [PATCH guest-common/manager/qemu-server v4 0/3] fix #5032: add guest time sync via QGA
@ 2026-07-03 14:12 Jakob Klocker
2026-07-03 14:12 ` [PATCH pve-guest-common v4 1/3] AbstractConfig: allow passing options to snapshot_rollback Jakob Klocker
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: Jakob Klocker @ 2026-07-03 14:12 UTC (permalink / raw)
To: pve-devel; +Cc: Jakob Klocker
This series adds a new agent option 'sync-time-on-resume' to
automatically synchronize the guest clock via the QEMU Guest Agent
after operations that can leave the guest time stale. The option is
disabled by default and only takes effect when the QEMU Guest Agent is
enabled.
When a VM resumes with a restored RAM state (waking from hibernation or
rolling back to a snapshot that includes RAM), the guest clock
continues from the point the state was saved and no longer matches
wall-clock time. Skew also appears whenever the guest is briefly
frozen and resumed - after creating a snapshot, an
ordinary pause/resume, or the resume step of a live migration.
When enabled, the option triggers a guest-set-time call:
* after resuming from hibernation (suspend-to-disk)
* after rolling back to a snapshot that includes RAM
* after a live migration
* after an ordinary pause/resume
* after taking a snapshot
The start, resume and rollback API endpoints also accept a
'sync-time-on-resume' parameter that overrides the configured value for
a single operation.
The option is exposed in the VM configuration GUI
so it can be toggled per guest.
A version check was added for migration to display a clear error
message in case 'sync-time-on-resume' is enabled and the target doesn't
support 'sync-time-on-resume'.
Link: https://bugzilla.proxmox.com/show_bug.cgi?id=5032
changes from v3 to v4 (thanks @Maximiliano & @Fiona)
- qemu-server: add version check for migration
- qemu-server: add `verbose_description`
- qemu-server: display clearer logging & error messages
- pve-manager: reword GUI checkbox label
changes from v2 to v3 (thanks @Fiona)
- add sync on pause/resume
- add override options
- add print task
- adapt formating and naming
changes from v1 to v2 (thanks @Arthur)
- qemu: adapt warning message in the resume-from-saved-state path
- ui: reword the agent option label for clarity
Tested scenarios:
* test script covering: guest agent enabled with QGA running,
guest agent enabled with QGA not running (not installed in guest)
and guest agent disabled in the VM config
* live migration from a node with the feature to a node too old to
support it (verified it aborts in prepare with a clear error, before
any disk/config changes)
* snapshot rollback with RAM on a Windows guest (verified
guest-set-time works via the explicit-host-time path, since the
RTC-less form is unsupported on Windows)
test script:
`
#!/usr/bin/env bash
vmid=100
echo "=== in-memory suspend, resume with sync=1 (should sync) ==="
qm suspend "$vmid"
qm resume "$vmid" --sync-time-on-resume 1
echo "=== in-memory suspend, resume with sync=0 (should NOT sync) ==="
qm suspend "$vmid"
qm resume "$vmid" --sync-time-on-resume 0
echo "=== suspend to disk, resume with sync=1 (should sync) ==="
qm suspend "$vmid" --todisk 1
qm resume "$vmid" --sync-time-on-resume 1
echo "=== suspend to disk, resume with sync=0 (should NOT sync) ==="
qm suspend "$vmid" --todisk 1
qm resume "$vmid" --sync-time-on-resume 0
echo "=== suspend to disk, start with sync=1 (should sync) ==="
qm suspend "$vmid" --todisk 1
qm start "$vmid" --sync-time-on-resume 1
echo "=== suspend to disk, start with sync=0 (should NOT sync) ==="
qm suspend "$vmid" --todisk 1
qm start "$vmid" --sync-time-on-resume 0
echo "=== create snapshot (should sync)==="
qm snapshot "$vmid" testing --vmstate true
echo "=== rollback with sync=0 (should NOT sync) ==="
qm rollback "$vmid" testing --sync-time-on-resume 0
echo "=== rollback with sync=1 (should sync) ==="
qm rollback "$vmid" testing --sync-time-on-resume 1
`
pve-guest-common:
Jakob Klocker (1):
AbstractConfig: allow passing options to snapshot_rollback
src/PVE/AbstractConfig.pm | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
pve-manager:
Jakob Klocker (1):
fix #5032: ui: qemu agent: add sync-time-on-resume option
www/manager6/form/AgentFeatureSelector.js | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
qemu-server:
Jakob Klocker (1):
fix #5032: agent: sync guest time via QGA when the clock falls behind
src/PVE/API2/Qemu.pm | 75 ++++++++++++++++++++++++++++++++--
src/PVE/CLI/qm.pm | 2 +-
src/PVE/QemuConfig.pm | 12 +++++-
src/PVE/QemuMigrate.pm | 13 ++++++
src/PVE/QemuServer.pm | 18 +++++++-
src/PVE/QemuServer/Agent.pm | 61 +++++++++++++++++++++++++++
src/PVE/QemuServer/RunState.pm | 11 ++++-
7 files changed, 183 insertions(+), 9 deletions(-)
Summary over all repositories:
9 files changed, 204 insertions(+), 11 deletions(-)
--
Generated by murpp 0.12.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH pve-guest-common v4 1/3] AbstractConfig: allow passing options to snapshot_rollback
2026-07-03 14:12 [PATCH guest-common/manager/qemu-server v4 0/3] fix #5032: add guest time sync via QGA Jakob Klocker
@ 2026-07-03 14:12 ` Jakob Klocker
2026-07-03 14:12 ` [PATCH pve-manager v4 2/3] fix #5032: ui: qemu agent: add sync-time-on-resume option Jakob Klocker
2026-07-03 14:12 ` [PATCH qemu-server v4 3/3] fix #5032: agent: sync guest time via QGA when the clock falls behind Jakob Klocker
2 siblings, 0 replies; 4+ messages in thread
From: Jakob Klocker @ 2026-07-03 14:12 UTC (permalink / raw)
To: pve-devel; +Cc: Jakob Klocker
Thread an optional $opts hashref through snapshot_rollback to
__snapshot_rollback_vm_start, so callers can pass per-rollback options
down to the VM start. Used by qemu-server to let the API override the
guest time sync for a single rollback; other options can be added
without further signature changes.
Signed-off-by: Jakob Klocker <j.klocker@proxmox.com>
---
src/PVE/AbstractConfig.pm | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/PVE/AbstractConfig.pm b/src/PVE/AbstractConfig.pm
index 7bcae19..76ea97a 100644
--- a/src/PVE/AbstractConfig.pm
+++ b/src/PVE/AbstractConfig.pm
@@ -1125,7 +1125,8 @@ my $rollback_remove_replication_snapshots = sub {
# Rolls back to a given snapshot.
sub snapshot_rollback {
- my ($class, $vmid, $snapname) = @_;
+ my ($class, $vmid, $snapname, $opts) = @_;
+ $opts //= {};
my $prepare = 1;
@@ -1197,7 +1198,7 @@ sub snapshot_rollback {
$class->write_config($vmid, $conf);
if (!$prepare && $snap->{vmstate}) {
- $class->__snapshot_rollback_vm_start($vmid, $snap->{vmstate}, $data);
+ $class->__snapshot_rollback_vm_start($vmid, $snap->{vmstate}, $data, $opts);
}
};
--
2.47.3
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH pve-manager v4 2/3] fix #5032: ui: qemu agent: add sync-time-on-resume option
2026-07-03 14:12 [PATCH guest-common/manager/qemu-server v4 0/3] fix #5032: add guest time sync via QGA Jakob Klocker
2026-07-03 14:12 ` [PATCH pve-guest-common v4 1/3] AbstractConfig: allow passing options to snapshot_rollback Jakob Klocker
@ 2026-07-03 14:12 ` Jakob Klocker
2026-07-03 14:12 ` [PATCH qemu-server v4 3/3] fix #5032: agent: sync guest time via QGA when the clock falls behind Jakob Klocker
2 siblings, 0 replies; 4+ messages in thread
From: Jakob Klocker @ 2026-07-03 14:12 UTC (permalink / raw)
To: pve-devel; +Cc: Jakob Klocker
Expose the new agent configuration option `sync-time-on-resume`
in the VM configuration GUI.
This allows enabling/disabling automatic guest clock
synchronization after clock-stalling operations.
Signed-off-by: Jakob Klocker <j.klocker@proxmox.com>
---
www/manager6/form/AgentFeatureSelector.js | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/www/manager6/form/AgentFeatureSelector.js b/www/manager6/form/AgentFeatureSelector.js
index 3cae4194..cd668583 100644
--- a/www/manager6/form/AgentFeatureSelector.js
+++ b/www/manager6/form/AgentFeatureSelector.js
@@ -45,6 +45,20 @@ Ext.define('PVE.form.AgentFeatureSelector', {
hidden: '{freeze_fs.checked}',
},
},
+ {
+ xtype: 'proxmoxcheckbox',
+ boxLabel: gettext(
+ "Synchronize guest clock with the host's after resuming from a previous state",
+ ),
+ name: 'sync-time-on-resume',
+ reference: 'sync_time_on_resume',
+ bind: {
+ disabled: '{!enabled.checked}',
+ },
+ disabled: true,
+ uncheckedValue: '0',
+ defaultValue: '0',
+ },
{
xtype: 'displayfield',
userCls: 'pmx-hint',
@@ -75,6 +89,10 @@ Ext.define('PVE.form.AgentFeatureSelector', {
delete values['freeze-fs'];
}
+ if (!PVE.Parser.parseBoolean(values['sync-time-on-resume'])) {
+ delete values['sync-time-on-resume'];
+ }
+
const agentstr = PVE.Parser.printPropertyString(values, 'enabled');
return { agent: agentstr };
},
--
2.47.3
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH qemu-server v4 3/3] fix #5032: agent: sync guest time via QGA when the clock falls behind
2026-07-03 14:12 [PATCH guest-common/manager/qemu-server v4 0/3] fix #5032: add guest time sync via QGA Jakob Klocker
2026-07-03 14:12 ` [PATCH pve-guest-common v4 1/3] AbstractConfig: allow passing options to snapshot_rollback Jakob Klocker
2026-07-03 14:12 ` [PATCH pve-manager v4 2/3] fix #5032: ui: qemu agent: add sync-time-on-resume option Jakob Klocker
@ 2026-07-03 14:12 ` Jakob Klocker
2 siblings, 0 replies; 4+ messages in thread
From: Jakob Klocker @ 2026-07-03 14:12 UTC (permalink / raw)
To: pve-devel; +Cc: Jakob Klocker
Add a 'sync-time-on-resume' agent config property (default off) that,
when enabled, issues the guest-set-time QGA command to set the guest
clock to the host's current time after operations that leave the clock
behind:
- resuming from hibernation (suspend-to-disk)
- resuming from pause/suspend
- rolling back to a snapshot that includes RAM
- live migration
- taking a snapshot, during which the guest is briefly frozen
The start, resume and rollback API endpoints gain a
'sync-time-on-resume' parameter (e.g. 'qm start <vmid>
--sync-time-on-resume 0') that overrides the configured agent value for
a single operation, forcing the sync on or off regardless of config.
guest_set_time prints a confirmation line to stdout on success. During
live migration the resume step runs over the migration tunnel, which
also uses stdout, so an unconditional print corrupts the tunnel
protocol. vm_resume therefore takes a 'quiet' parameter to suppress the
print on the migration path; the confirmation is logged by the migration
code instead.
A version check was added for migration to display a clear error
message in case 'sync-time-on-resume' is enabled and the target doesn't
support 'sync-time-on-resume'.
Signed-off-by: Jakob Klocker <j.klocker@proxmox.com>
---
changes from v3 to v4 (thanks @Maximiliano & @Fiona)
- qemu-server: add version check for migration
- qemu-server: add `verbose_description`
- qemu-server: display clearer logging & error messages
changes from v2 to v3 (thanks @Fiona)
- add sync on pause/resume
- add override options
- add print task
- adapt formating and naming
changes from v1 to v2 (thanks @Arthur)
- qemu: adapt warning message in the resume-from-saved-state path
- ui: reword the agent option label for clarity
Tested scenarios:
* test script covering: guest agent enabled with QGA running,
guest agent enabled with QGA not running (not installed in guest)
and guest agent disabled in the VM config
* live migration from a node with the feature to a node too old to
support it (verified it aborts in prepare with a clear error, before
any disk/config changes)
* snapshot rollback with RAM on a Windows guest (verified
guest-set-time works via the explicit-host-time path, since the
RTC-less form is unsupported on Windows)
test script:
`
#!/usr/bin/env bash
vmid=100
echo "=== in-memory suspend, resume with sync=1 (should sync) ==="
qm suspend "$vmid"
qm resume "$vmid" --sync-time-on-resume 1
echo "=== in-memory suspend, resume with sync=0 (should NOT sync) ==="
qm suspend "$vmid"
qm resume "$vmid" --sync-time-on-resume 0
echo "=== suspend to disk, resume with sync=1 (should sync) ==="
qm suspend "$vmid" --todisk 1
qm resume "$vmid" --sync-time-on-resume 1
echo "=== suspend to disk, resume with sync=0 (should NOT sync) ==="
qm suspend "$vmid" --todisk 1
qm resume "$vmid" --sync-time-on-resume 0
echo "=== suspend to disk, start with sync=1 (should sync) ==="
qm suspend "$vmid" --todisk 1
qm start "$vmid" --sync-time-on-resume 1
echo "=== suspend to disk, start with sync=0 (should NOT sync) ==="
qm suspend "$vmid" --todisk 1
qm start "$vmid" --sync-time-on-resume 0
echo "=== create snapshot (should sync)==="
qm snapshot "$vmid" testing --vmstate true
echo "=== rollback with sync=0 (should NOT sync) ==="
qm rollback "$vmid" testing --sync-time-on-resume 0
echo "=== rollback with sync=1 (should sync) ==="
qm rollback "$vmid" testing --sync-time-on-resume 1
`
src/PVE/API2/Qemu.pm | 75 ++++++++++++++++++++++++++++++++--
src/PVE/CLI/qm.pm | 2 +-
src/PVE/QemuConfig.pm | 12 +++++-
src/PVE/QemuMigrate.pm | 13 ++++++
src/PVE/QemuServer.pm | 18 +++++++-
src/PVE/QemuServer/Agent.pm | 61 +++++++++++++++++++++++++++
src/PVE/QemuServer/RunState.pm | 11 ++++-
7 files changed, 183 insertions(+), 9 deletions(-)
diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
index 9023c344..6bb0f358 100644
--- a/src/PVE/API2/Qemu.pm
+++ b/src/PVE/API2/Qemu.pm
@@ -3510,6 +3510,24 @@ __PACKAGE__->register_method({
. ' the migration. A value of 0 means that the host_mtu parameter is to be'
. ' avoided for the corresponding device.',
},
+ 'sync-time-on-resume' => {
+ type => 'boolean',
+ description =>
+ "Override the 'sync-time-on-resume' setting from the VM's guest agent (QGA)"
+ . " config for this start only. Controls whether the guest's clock is"
+ . " synchronized after the RAM state is restored. The guest agent must be"
+ . " enabled.",
+ verbose_description =>
+ "Override the 'sync-time-on-resume' setting from the VM's guest agent (QGA)"
+ . " config for this start only. When enabled, the 'guest-set-time' QEMU guest"
+ . " agent command is issued to synchronize the guest's clock to the host's current"
+ . " time after the RAM state is restored, which happens here when starting a VM"
+ . " that was hibernated (suspended to disk). Without synchronization the guest's"
+ . " clock may still reflect the time from when the VM was hibernated. The time is"
+ . " only synchronized when the QEMU Guest Agent option is enabled in the guest's"
+ . " configuration and the agent is running inside of the guest.",
+ optional => 1,
+ },
},
},
returns => {
@@ -3525,6 +3543,7 @@ __PACKAGE__->register_method({
my $vmid = extract_param($param, 'vmid');
my $timeout = extract_param($param, 'timeout');
my $machine = extract_param($param, 'machine');
+ my $sync_time = extract_param($param, 'sync-time-on-resume');
my $get_root_param = sub {
my $value = extract_param($param, $_[0]);
@@ -3631,6 +3650,7 @@ __PACKAGE__->register_method({
timeout => $timeout,
forcecpu => $force_cpu,
'nets-host-mtu' => $nets_host_mtu,
+ 'sync-time-on-resume' => $sync_time,
};
PVE::QemuServer::vm_start($storecfg, $vmid, $params, $migrate_opts);
@@ -4115,6 +4135,23 @@ __PACKAGE__->register_method({
description => "Do not check whether the VM is running, used"
. " internally during migration. Only root may use this option.",
},
+ 'sync-time-on-resume' => {
+ type => 'boolean',
+ description =>
+ "Override the 'sync-time-on-resume' setting from the VM's guest agent (QGA)"
+ . " config for this resume only. Controls whether the guest's clock is"
+ . " synchronized after the RAM state is restored. The guest agent must be"
+ . " enabled.",
+ verbose_description =>
+ "Override the 'sync-time-on-resume' setting from the VM's guest agent (QGA)"
+ . " config for this resume only. When enabled, the 'guest-set-time' QEMU guest"
+ . " agent command is issued to synchronize the guest's clock to the host's current"
+ . " time after resuming the VM from a paused state. Without synchronization the"
+ . " guest's clock may still reflect the time from when the VM was paused. The"
+ . " time is only synchronized when the QEMU Guest Agent option is enabled in the"
+ . " guest's configuration and the agent is running inside of the guest.",
+ optional => 1,
+ },
},
},
returns => {
@@ -4131,6 +4168,8 @@ __PACKAGE__->register_method({
my $vmid = extract_param($param, 'vmid');
+ my $sync_time = extract_param($param, 'sync-time-on-resume');
+
my $skiplock = extract_param($param, 'skiplock');
raise_param_exc({ skiplock => "Only root may use this option." })
if $skiplock && $authuser ne 'root@pam';
@@ -4161,10 +4200,14 @@ __PACKAGE__->register_method({
syslog('info', "resume VM $vmid: $upid\n");
if (!$to_disk_suspended) {
- PVE::QemuServer::RunState::vm_resume($vmid, $skiplock, $nocheck);
+ PVE::QemuServer::RunState::vm_resume($vmid, $skiplock, $nocheck, $sync_time);
} else {
my $storecfg = PVE::Storage::config();
- PVE::QemuServer::vm_start($storecfg, $vmid, { skiplock => $skiplock });
+ PVE::QemuServer::vm_start(
+ $storecfg,
+ $vmid,
+ { skiplock => $skiplock, 'sync-time-on-resume' => $sync_time },
+ );
}
return;
@@ -6323,6 +6366,24 @@ __PACKAGE__->register_method({
optional => 1,
default => 0,
},
+ 'sync-time-on-resume' => {
+ type => 'boolean',
+ description =>
+ "Override the 'sync-time-on-resume' setting from the VM's guest agent (QGA)"
+ . " config for this rollback only. Controls whether the guest's clock is"
+ . " synchronized after the RAM state is restored. The guest agent must be"
+ . " enabled.",
+ verbose_description =>
+ "Override the 'sync-time-on-resume' setting from the VM's guest agent (QGA)"
+ . " config for this rollback only. When enabled, the 'guest-set-time' QEMU guest"
+ . " agent command is issued to synchronize the guest's clock to the host's current"
+ . " time after rolling back to a snapshot that includes RAM. Without"
+ . " synchronization the guest's clock may still reflect the time from when the"
+ . " snapshot was taken. The time is only synchronized when the QEMU Guest Agent"
+ . " option is enabled in the guest's configuration and the agent is running"
+ . " inside of the guest.",
+ optional => 1,
+ },
},
},
returns => {
@@ -6342,6 +6403,8 @@ __PACKAGE__->register_method({
my $snapname = extract_param($param, 'snapname');
+ my $sync_time = extract_param($param, 'sync-time-on-resume');
+
# vm_start is invoked directly from the worker, so its own permissions
# predicate doesn't fire here - check VM.PowerMgmt up front so a user
# with only VM.Snapshot.Rollback can't power the VM on as a side effect.
@@ -6350,7 +6413,11 @@ __PACKAGE__->register_method({
my $realcmd = sub {
PVE::Cluster::log_msg('info', $authuser, "rollback snapshot VM $vmid: $snapname");
- PVE::QemuConfig->snapshot_rollback($vmid, $snapname);
+ PVE::QemuConfig->snapshot_rollback(
+ $vmid,
+ $snapname,
+ { 'sync-time-on-resume' => $sync_time },
+ );
if ($param->{start} && !PVE::QemuServer::Helpers::vm_running_locally($vmid)) {
PVE::API2::Qemu->vm_start({ vmid => $vmid, node => $node });
@@ -6915,7 +6982,7 @@ __PACKAGE__->register_method({
},
'resume' => sub {
if (PVE::QemuServer::Helpers::vm_running_locally($state->{vmid})) {
- PVE::QemuServer::RunState::vm_resume($state->{vmid}, 1, 1);
+ PVE::QemuServer::RunState::vm_resume($state->{vmid}, 1, 1, undef, 1);
} else {
die "VM $state->{vmid} not running\n";
}
diff --git a/src/PVE/CLI/qm.pm b/src/PVE/CLI/qm.pm
index b903c1f1..25351f64 100755
--- a/src/PVE/CLI/qm.pm
+++ b/src/PVE/CLI/qm.pm
@@ -473,7 +473,7 @@ __PACKAGE__->register_method({
if (PVE::QemuServer::Helpers::vm_running_locally($vmid)) {
# vm_resume with nocheck, since local node might not have processed config
# move/rename yet
- eval { PVE::QemuServer::RunState::vm_resume($vmid, 1, 1); };
+ eval { PVE::QemuServer::RunState::vm_resume($vmid, 1, 1, undef, 1); };
if ($@) {
$tunnel_write->("ERR: resume failed - $@");
} else {
diff --git a/src/PVE/QemuConfig.pm b/src/PVE/QemuConfig.pm
index 26f0fda2..e95a8c61 100644
--- a/src/PVE/QemuConfig.pm
+++ b/src/PVE/QemuConfig.pm
@@ -383,6 +383,11 @@ sub __snapshot_create_vol_snapshots_hook {
next;
}
}
+ my $conf = $class->load_config($vmid);
+ if (PVE::QemuServer::Agent::should_sync_time_on_resume($conf->{agent})) {
+ eval { PVE::QemuServer::Agent::guest_set_time($vmid); };
+ warn "could not sync guest time after snapshot - $@" if $@;
+ }
}
}
}
@@ -444,8 +449,9 @@ sub __snapshot_rollback_hook {
my ($class, $vmid, $conf, $snap, $prepare, $data) = @_;
if ($prepare) {
- # we save the machine of the current config
+ # we save the machine and agent of the current config
$data->{oldmachine} = $conf->{machine};
+ $data->{agent} = $conf->{agent};
} else {
# if we have a 'runningmachine' entry in the snapshot we use that
# for the forcemachine parameter, else we use the old logic
@@ -509,7 +515,7 @@ sub __snapshot_rollback_vm_stop {
}
sub __snapshot_rollback_vm_start {
- my ($class, $vmid, $vmstate, $data) = @_;
+ my ($class, $vmid, $vmstate, $data, $opts) = @_;
my $storecfg = PVE::Storage::config();
my $params = {
@@ -517,6 +523,8 @@ sub __snapshot_rollback_vm_start {
forcemachine => $data->{forcemachine},
forcecpu => $data->{forcecpu},
'nets-host-mtu' => $data->{'nets-host-mtu'},
+ agent => $data->{agent},
+ 'sync-time-on-resume' => $opts->{'sync-time-on-resume'},
};
PVE::QemuServer::vm_start($storecfg, $vmid, $params);
}
diff --git a/src/PVE/QemuMigrate.pm b/src/PVE/QemuMigrate.pm
index 8da6f15d..ae40195c 100644
--- a/src/PVE/QemuMigrate.pm
+++ b/src/PVE/QemuMigrate.pm
@@ -312,6 +312,16 @@ sub prepare {
}
}
+ my $target_version = PVE::QemuServer::Helpers::get_node_pvecfg_version($self->{node});
+ if (
+ $target_version
+ && !PVE::QemuServer::Helpers::pvecfg_min_version($target_version, 9, 2, 5)
+ && PVE::QemuServer::Agent::get_qga_key($conf, 'sync-time-on-resume')
+ ) {
+ die "target node '$self->{node}' is too old to support 'sync-time-on-resume'"
+ . " (needs pve-manager >= 9.2.5); disable the option before migrating\n";
+ }
+
my $vollist = PVE::QemuServer::get_vm_volumes($conf);
my $storages = {};
@@ -1770,6 +1780,9 @@ sub phase3_cleanup {
$self->{errors} = 1;
}
}
+ $self->log('info', "guest clock sync requested, will run on target after resume")
+ if !$self->{errors}
+ && PVE::QemuServer::Agent::should_sync_time_on_resume($conf->{agent});
}
if (
diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
index cdf66e89..e30e89e7 100644
--- a/src/PVE/QemuServer.pm
+++ b/src/PVE/QemuServer.pm
@@ -5179,7 +5179,10 @@ sub vmconfig_update_agent {
die "skip\n" if !$conf->{$opt};
- my $hotplug_options = { fstrim_cloned_disks => 1 };
+ my $hotplug_options = {
+ fstrim_cloned_disks => 1,
+ 'sync-time-on-resume' => 1,
+ };
my $old_agent = parse_guest_agent($conf->{agent});
my $agent = parse_guest_agent($value);
@@ -6006,6 +6009,19 @@ sub vm_start_nolock {
);
}
+ # $resume = suspend-to-disk, $statefile = rollback with RAM. Rollback carries the snapshot's
+ # agent setting in params; hibernate resume falls back to the live config
+ if ($resume || ($statefile && !$migratedfrom)) {
+ my $agent = $params->{agent} // $conf->{agent};
+ if (PVE::QemuServer::Agent::should_sync_time_on_resume(
+ $agent,
+ $params->{'sync-time-on-resume'},
+ )) {
+ eval { PVE::QemuServer::Agent::guest_set_time($vmid); 1 };
+ warn "could not sync guest time after resume from saved state - $@" if $@;
+ }
+ }
+
return $res;
}
diff --git a/src/PVE/QemuServer/Agent.pm b/src/PVE/QemuServer/Agent.pm
index be6df443..35818b97 100644
--- a/src/PVE/QemuServer/Agent.pm
+++ b/src/PVE/QemuServer/Agent.pm
@@ -4,6 +4,8 @@ use v5.36;
use JSON;
use MIME::Base64 qw(decode_base64 encode_base64);
+use Time::HiRes qw(time);
+use POSIX qw(strftime);
use PVE::JSONSchema;
@@ -52,6 +54,25 @@ our $agent_fmt = {
optional => 1,
default => 1,
},
+ # FIXME: MAJOR VERSION: 10.0.0 - reconsider setting option on as default
+ 'sync-time-on-resume' => {
+ description =>
+ "Synchronize the guest's clock through QGA after operations that can leave"
+ . " the guest's clock behind.",
+ verbose_description =>
+ "Whether to issue the 'guest-set-time' QEMU guest agent command to synchronize the"
+ . " guest's clock to the host's current time after operations that can leave the guest's"
+ . " clock behind. This happens when resuming from hibernation, resuming from a paused"
+ . " state, taking a snapshot, after a migration, and after rolling back to a snapshot"
+ . " that includes RAM. In these cases the guest's clock may still reflect an earlier"
+ . " time. The time is only synchronized when the QEMU Guest Agent option is enabled"
+ . " in the guest's configuration and the agent is running inside of the guest. For"
+ . " resume, hibernation resume and snapshot rollback, the synchronization can be"
+ . " overridden per operation.",
+ type => 'boolean',
+ optional => 1,
+ default => 0,
+ },
type => {
description => "Select the agent type",
type => 'string',
@@ -332,4 +353,44 @@ sub guest_fs_freeze_applicable($agent_str, $vmid, $logfunc = undef) {
return 1;
}
+=head3 should_sync_time_on_resume
+
+Returns whether the guest's clock should be synchronized to the host's via the QEMU Guest Agent
+when the VM is resumed from saved state. Requires the guest agent to be enabled. An explicit
+C<$override> (from an API parameter) takes precedence over the C<sync-time-on-resume> agent
+config property when defined; otherwise the config value is used (defaulting to off). Does B<not>
+check whether the agent is actually running.
+
+=cut
+
+sub should_sync_time_on_resume($agent_str, $override = undef) {
+ my $agent = parse_guest_agent($agent_str);
+ return 0 if !$agent->{enabled};
+ return $override ? 1 : 0 if defined($override);
+ return $agent->{'sync-time-on-resume'} // 0;
+}
+
+=head3 guest_set_time
+
+Sets the guest's clock to the current host time via the QEMU Guest Agent's
+C<guest-set-time> command. The host time is passed explicitly (as nanoseconds
+since the UNIX epoch) because the argument-less form reads the time from the
+guest's RTC, which isn't supported on all platforms (e.g. Windows); passing
+the value directly works regardless of guest OS.
+
+=cut
+
+sub guest_set_time($vmid, $quiet = undef) {
+ if (!qga_check_running($vmid, 1)) {
+ print "skipping guest set time - agent configured but not running?\n" if !$quiet;
+ return;
+ }
+ my $time_ns = Time::HiRes::time() * 1_000_000_000;
+ my $res = PVE::QemuServer::Monitor::mon_cmd($vmid, 'guest-set-time', time => int($time_ns));
+ check_agent_error($res, "unable to set guest time");
+ my $time_str = POSIX::strftime("%F %T", localtime($time_ns / 1_000_000_000));
+ print "synced guest clock via guest agent to $time_str\n" if !$quiet;
+ return;
+}
+
1;
diff --git a/src/PVE/QemuServer/RunState.pm b/src/PVE/QemuServer/RunState.pm
index bbbcc88e..a72aed4d 100644
--- a/src/PVE/QemuServer/RunState.pm
+++ b/src/PVE/QemuServer/RunState.pm
@@ -128,7 +128,7 @@ sub vm_suspend {
# 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) = @_;
+ my ($vmid, $skiplock, $nocheck, $sync_time, $quiet) = @_;
PVE::QemuConfig->lock_config(
$vmid,
@@ -180,6 +180,15 @@ sub vm_resume {
if $resume_cmd eq 'cont';
mon_cmd($vmid, $resume_cmd);
+
+ if (
+ $resume_cmd eq 'cont'
+ && PVE::QemuServer::Agent::should_sync_time_on_resume($conf->{agent},
+ $sync_time)
+ ) {
+ eval { PVE::QemuServer::Agent::guest_set_time($vmid, $quiet); };
+ warn "could not sync guest time after resume - $@" if $@;
+ }
},
);
}
--
2.47.3
^ permalink raw reply related [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-07-03 14:12 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-03 14:12 [PATCH guest-common/manager/qemu-server v4 0/3] fix #5032: add guest time sync via QGA Jakob Klocker
2026-07-03 14:12 ` [PATCH pve-guest-common v4 1/3] AbstractConfig: allow passing options to snapshot_rollback Jakob Klocker
2026-07-03 14:12 ` [PATCH pve-manager v4 2/3] fix #5032: ui: qemu agent: add sync-time-on-resume option Jakob Klocker
2026-07-03 14:12 ` [PATCH qemu-server v4 3/3] fix #5032: agent: sync guest time via QGA when the clock falls behind Jakob Klocker
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox