use PVE::IPCC;
use PVE::JSONSchema;
use PVE::PBSClient;
+use PVE::RESTEnvironment qw(log_warn);
use PVE::QMPClient;
use PVE::Storage::Plugin;
use PVE::Storage::PBSPlugin;
use PVE::Storage;
use PVE::Tools;
use PVE::VZDump;
+use PVE::Format qw(render_duration render_bytes);
use PVE::QemuConfig;
use PVE::QemuServer;
if defined($conf->{name});
$self->{vm_was_running} = 1;
+ $self->{vm_was_paused} = 0;
if (!PVE::QemuServer::check_running($vmid)) {
$self->{vm_was_running} = 0;
+ } elsif (PVE::QemuServer::vm_is_paused($vmid)) {
+ $self->{vm_was_paused} = 1;
}
$task->{hostname} = $conf->{name};
if (!$volume->{included}) {
$self->loginfo("exclude disk '$name' '$volid' ($volume->{reason})");
next;
- } elsif ($self->{vm_was_running} && $volume_config->{iothread}) {
- if (!PVE::QemuServer::Machine::runs_at_least_qemu_version($vmid, 4, 0, 1)) {
- die "disk '$name' '$volid' (iothread=on) can't use backup feature with running QEMU " .
- "version < 4.0.1! Either set backup=no for this drive or upgrade QEMU and restart VM\n";
- }
+ } elsif ($self->{vm_was_running} && $volume_config->{iothread} &&
+ !PVE::QemuServer::Machine::runs_at_least_qemu_version($vmid, 4, 0, 1)) {
+ die "disk '$name' '$volid' (iothread=on) can't use backup feature with running QEMU " .
+ "version < 4.0.1! Either set backup=no for this drive or upgrade QEMU and restart VM\n";
} else {
my $log = "include disk '$name' '$volid'";
if (defined(my $size = $volume_config->{size})) {
}
next if !$path;
- my ($size, $format) = eval { PVE::Storage::volume_size_info($self->{storecfg}, $volid, 5) };
- die "no such volume '$volid'\n" if $@;
+ my ($size, $format);
+ if ($storeid) {
+ # The call in list context can be expensive for certain plugins like RBD, just get size
+ $size = eval { PVE::Storage::volume_size_info($self->{storecfg}, $volid, 5) };
+ die "cannot determine size of volume '$volid' - $@\n" if $@;
+
+ my $scfg = PVE::Storage::storage_config($self->{storecfg}, $storeid);
+ $format = PVE::QemuServer::qemu_img_format($scfg, $volname);
+ } else {
+ ($size, $format) = eval {
+ PVE::Storage::volume_size_info($self->{storecfg}, $volid, 5);
+ };
+ die "cannot determine size and format of volume '$volid' - $@\n" if $@;
+ }
my $diskinfo = {
path => $path,
qmdevice => "drive-$ds",
};
+ if ($ds eq 'tpmstate0') {
+ # TPM drive only exists for backup, which is reflected in the name
+ $diskinfo->{qmdevice} = 'drive-tpmstate0-backup';
+ $task->{tpmpath} = $path;
+ }
+
if (-b $path) {
$diskinfo->{type} = 'block';
} else {
sub suspend_vm {
my ($self, $task, $vmid) = @_;
+ return if $self->{vm_was_paused};
+
$self->cmd ("qm suspend $vmid --skiplock");
}
sub resume_vm {
my ($self, $task, $vmid) = @_;
+ return if $self->{vm_was_paused};
+
$self->cmd ("qm resume $vmid --skiplock");
}
my $found_snapshot;
my $found_pending;
+ my $found_cloudinit;
while (defined (my $line = <$conffd>)) {
next if $line =~ m/^\#vzdump\#/; # just to be sure
next if $line =~ m/^\#qmdump\#/; # just to be sure
if ($line =~ m/^\[(.*)\]\s*$/) {
if ($1 =~ m/PENDING/i) {
$found_pending = 1;
+ } elsif ($1 =~ m/special:cloudinit/) {
+ $found_cloudinit = 1;
} else {
$found_snapshot = 1;
}
}
- next if $found_snapshot || $found_pending; # skip all snapshots and pending changes config data
+ next if $found_snapshot || $found_pending || $found_cloudinit; # skip all snapshots,pending changes and cloudinit config data
if ($line =~ m/^unused\d+:\s*(\S+)\s*/) {
$self->loginfo("skip unused drive '$1' (not included into backup)");
}
}
-# number, [precision=1]
-my $num2str = sub {
- return sprintf( "%." . ( $_[1] || 1 ) . "f", $_[0] );
-};
-my sub bytes_to_human {
- my ($bytes, $precission) = @_;
-
- return $num2str->($bytes, $precission) . ' B' if $bytes < 1024;
- my $kb = $bytes/1024;
-
- return $num2str->($kb, $precission) . " KiB" if $kb < 1024;
- my $mb = $kb/1024;
-
- return $num2str->($mb, $precission) . " MiB" if $mb < 1024;
- my $gb = $mb/1024;
-
- return $num2str->($gb, $precission) . " GiB" if $gb < 1024;
- my $tb = $gb/1024;
-
- return $num2str->($tb, $precission) . " TiB";
-}
-my sub duration_to_human {
- my ($seconds) = @_;
-
- return sprintf('%2ds', $seconds) if $seconds < 60;
- my $minutes = $seconds / 60;
- $seconds = $seconds % 60;
-
- return sprintf('%2dm %2ds', $minutes, $seconds) if $minutes < 60;
- my $hours = $minutes / 60;
- $minutes = $minutes % 60;
-
- return sprintf('%2dh %2dm %2ds', $hours, $minutes, $seconds) if $hours < 24;
- my $days = $hours / 24;
- $hours = $hours % 24;
-
- return sprintf('%2dd %2dh %2dm', $days, $hours, $minutes);
-}
-
my $bitmap_action_to_human = sub {
my ($self, $info) = @_;
if ($info->{dirty} == 0) {
return "OK (drive clean)";
} else {
- my $size = bytes_to_human($info->{size});
- my $dirty = bytes_to_human($info->{dirty});
+ my $size = render_bytes($info->{size}, 1);
+ my $dirty = render_bytes($info->{dirty}, 1);
return "OK ($dirty of $size dirty)";
}
} elsif ($action eq "invalid") {
my ($mb, $delta) = @_;
return "0 B/s" if $mb <= 0;
my $bw = int(($mb / $delta));
- return bytes_to_human($bw) . "/s";
+ return render_bytes($bw, 1) . "/s";
};
my $target = 0;
$last_reused += $info->{size} - $info->{dirty};
}
if ($target < $total) {
- my $total_h = bytes_to_human($total);
- my $target_h = bytes_to_human($target);
+ my $total_h = render_bytes($total, 1);
+ my $target_h = render_bytes($target, 1);
$self->loginfo("using fast incremental mode (dirty-bitmap), $target_h dirty of $total_h total");
}
}
- my $first_round = 1;
my $last_finishing = 0;
while(1) {
my $status = mon_cmd($vmid, 'query-backup');
my $timediff = ($ctime - $last_time) || 1; # fixme
my $mbps_read = $get_mbps->($rbytes, $timediff);
my $mbps_write = $get_mbps->($wbytes, $timediff);
- my $target_h = bytes_to_human($target);
- my $transferred_h = bytes_to_human($transferred);
-
- if (!$has_query_bitmap && $first_round && $target != $total) { # FIXME: remove with PVE 7.0
- my $total_h = bytes_to_human($total);
- $self->loginfo("using fast incremental mode (dirty-bitmap), $target_h dirty of $total_h total");
- }
+ my $target_h = render_bytes($target, 1);
+ my $transferred_h = render_bytes($transferred, 1);
my $statusline = sprintf("%3d%% ($transferred_h of $target_h) in %s"
- .", read: $mbps_read, write: $mbps_write", $percent, duration_to_human($duration));
+ .", read: $mbps_read, write: $mbps_write", $percent, render_duration($duration));
my $res = $status->{status} || 'unknown';
if ($res ne 'active') {
$last_finishing = $status->{finishing};
}
sleep(1);
- $first_round = 0 if $first_round;
}
my $duration = time() - $starttime;
if ($last_zero) {
my $zero_per = $last_target ? int(($last_zero * 100)/$last_target) : 0;
- my $zero_h = bytes_to_human($last_zero, 2);
+ my $zero_h = render_bytes($last_zero);
$self->loginfo("backup is sparse: $zero_h (${zero_per}%) total zero data");
}
if ($reused) {
- my $reused_h = bytes_to_human($reused, 2);
+ my $reused_h = render_bytes($reused);
my $reuse_per = int($reused * 100 / $last_total);
$self->loginfo("backup was done incrementally, reused $reused_h (${reuse_per}%)");
}
if ($transferred) {
- my $transferred_h = bytes_to_human($transferred, 2);
+ my $transferred_h = render_bytes($transferred);
if ($duration) {
my $mbps = $get_mbps->($transferred, $duration);
$self->loginfo("transferred $transferred_h in $duration seconds ($mbps)");
};
};
+my $attach_tpmstate_drive = sub {
+ my ($self, $task, $vmid) = @_;
+
+ return if !$task->{tpmpath};
+
+ # unconditionally try to remove the tpmstate-named drive - it only exists
+ # for backing up, and avoids errors if left over from some previous event
+ eval { PVE::QemuServer::qemu_drivedel($vmid, "tpmstate0-backup"); };
+
+ $self->loginfo('attaching TPM drive to QEMU for backup');
+
+ my $drive = "file=$task->{tpmpath},if=none,read-only=on,id=drive-tpmstate0-backup";
+ $drive =~ s/\\/\\\\/g;
+ my $ret = PVE::QemuServer::Monitor::hmp_cmd($vmid, "drive_add auto \"$drive\"");
+ die "attaching TPM drive failed - $ret\n" if $ret !~ m/OK/s;
+};
+
+my $detach_tpmstate_drive = sub {
+ my ($task, $vmid) = @_;
+ return if !$task->{tpmpath} || !PVE::QemuServer::check_running($vmid);
+ eval { PVE::QemuServer::qemu_drivedel($vmid, "tpmstate0-backup"); };
+};
+
+my sub add_backup_performance_options {
+ my ($qmp_param, $perf, $qemu_support) = @_;
+
+ return if !$perf || scalar(keys $perf->%*) == 0;
+
+ if (!$qemu_support) {
+ my $settings_string = join(', ', sort keys $perf->%*);
+ log_warn("ignoring setting(s): $settings_string - issue checking if supported");
+ return;
+ }
+
+ if (defined($perf->{'max-workers'})) {
+ if ($qemu_support->{'backup-max-workers'}) {
+ $qmp_param->{'max-workers'} = int($perf->{'max-workers'});
+ } else {
+ log_warn("ignoring 'max-workers' setting - not supported by running QEMU");
+ }
+ }
+}
+
+sub get_and_check_pbs_encryption_config {
+ my ($self) = @_;
+
+ my $opts = $self->{vzdump}->{opts};
+ my $scfg = $opts->{scfg};
+
+ my $keyfile = PVE::Storage::PBSPlugin::pbs_encryption_key_file_name($scfg, $opts->{storage});
+ my $master_keyfile = PVE::Storage::PBSPlugin::pbs_master_pubkey_file_name($scfg, $opts->{storage});
+
+ if (-e $keyfile) {
+ if (-e $master_keyfile) {
+ $self->loginfo("enabling encryption with master key feature");
+ return ($keyfile, $master_keyfile);
+ } elsif ($scfg->{'master-pubkey'}) {
+ die "master public key configured but no key file found\n";
+ } else {
+ $self->loginfo("enabling encryption");
+ return ($keyfile, undef);
+ }
+ } else {
+ my $encryption_fp = $scfg->{'encryption-key'};
+ die "encryption configured ('$encryption_fp') but no encryption key file found!\n"
+ if $encryption_fp;
+ if (-e $master_keyfile) {
+ $self->log(
+ 'warn',
+ "backup target storage is configured with master-key, but no encryption key set!"
+ ." Ignoring master key settings and creating unencrypted backup."
+ );
+ }
+ return (undef, undef);
+ }
+ die "internal error - unhandled case for getting & checking PBS encryption ($keyfile, $master_keyfile)!";
+}
+
sub archive_pbs {
my ($self, $task, $vmid) = @_;
my $fingerprint = $scfg->{fingerprint};
my $repo = PVE::PBSClient::get_repository($scfg);
my $password = PVE::Storage::PBSPlugin::pbs_get_password($scfg, $opts->{storage});
- my $keyfile = PVE::Storage::PBSPlugin::pbs_encryption_key_file_name($scfg, $opts->{storage});
+ my ($keyfile, $master_keyfile) = $self->get_and_check_pbs_encryption_config();
my $diskcount = scalar(@{$task->{disks}});
- # proxmox-backup-client can only handle raw files and block devs
- # only use it (directly) for disk-less VMs
+ # proxmox-backup-client can only handle raw files and block devs, so only use it (directly) for
+ # disk-less VMs
if (!$diskcount) {
- my @pathlist;
$self->loginfo("backup contains no disks");
local $ENV{PBS_PASSWORD} = $password;
'--backup-id', "$vmid",
'--backup-time', $task->{backup_time},
];
+ if (defined(my $ns = $scfg->{namespace})) {
+ push @$cmd, '--ns', $ns;
+ }
+ if (defined($keyfile)) {
+ push @$cmd, '--keyfile', $keyfile;
+ push @$cmd, '--master-pubkey-file', $master_keyfile if defined($master_keyfile);
+ }
push @$cmd, "qemu-server.conf:$conffile";
push @$cmd, "fw.conf:$firewall" if -e $firewall;
my $devlist = _get_task_devlist($task);
$self->enforce_vm_running_for_backup($vmid);
- $self->register_qmeventd_handle($vmid);
+ $self->{qmeventd_fh} = PVE::QemuServer::register_qmeventd_handle($vmid);
my $backup_job_uuid;
eval {
};
my $qemu_support = eval { mon_cmd($vmid, "query-proxmox-support") };
- if (!$qemu_support) {
- die "PBS backups are not supported by the running QEMU version. Please make "
- . "sure you've installed the latest version and the VM has been restarted.\n";
+ my $err = $@;
+ if (!$qemu_support || $err) {
+ die "query-proxmox-support returned empty value\n" if !$err;
+ if ($err =~ m/The command query-proxmox-support has not been found/) {
+ die "PBS backups are not supported by the running QEMU version. Please make "
+ . "sure you've installed the latest version and the VM has been restarted.\n";
+ } else {
+ die "QMP command query-proxmox-support failed - $err\n";
+ }
}
+ # pve-qemu supports it since 5.2.0-1 (PVE 6.4), so safe to die since PVE 8
+ die "master key configured but running QEMU version does not support master keys\n"
+ if !defined($qemu_support->{'pbs-masterkey'}) && defined($master_keyfile);
+
+ $attach_tpmstate_drive->($self, $task, $vmid);
+
my $fs_frozen = $self->qga_fs_freeze($task, $vmid);
my $params = {
devlist => $devlist,
'config-file' => $conffile,
};
+ if (defined(my $ns = $scfg->{namespace})) {
+ $params->{'backup-ns'} = $ns;
+ }
+
$params->{speed} = $opts->{bwlimit}*1024 if $opts->{bwlimit};
+ add_backup_performance_options($params, $opts->{performance}, $qemu_support);
+
$params->{fingerprint} = $fingerprint if defined($fingerprint);
$params->{'firewall-file'} = $firewall if -e $firewall;
- if (-e $keyfile) {
- $self->loginfo("enabling encryption");
+
+ $params->{encrypt} = defined($keyfile) ? JSON::true : JSON::false;
+ if (defined($keyfile)) {
$params->{keyfile} = $keyfile;
- $params->{encrypt} = JSON::true;
- } else {
- $params->{encrypt} = JSON::false;
+ $params->{"master-keyfile"} = $master_keyfile if defined($master_keyfile);
}
my $is_template = PVE::QemuConfig->is_template($self->{vmlist}->{$vmid});
$params->{'use-dirty-bitmap'} = JSON::true
if $qemu_support->{'pbs-dirty-bitmap'} && !$is_template;
- $params->{timeout} = 60; # give some time to connect to the backup server
+ $params->{timeout} = 125; # give some time to connect to the backup server
my $res = eval { mon_cmd($vmid, "backup", %$params) };
my $qmperr = $@;
if ($err) {
$self->logerr($err);
$self->mon_backup_cancel($vmid);
+ $self->resume_vm_after_job_start($task, $vmid);
}
$self->restore_vm_power_state($vmid);
my $devlist = _get_task_devlist($task);
$self->enforce_vm_running_for_backup($vmid);
- $self->register_qmeventd_handle($vmid);
+ $self->{qmeventd_fh} = PVE::QemuServer::register_qmeventd_handle($vmid);
my $cpid;
my $backup_job_uuid;
die "interrupted by signal\n";
};
+ # Currently, failing to determine Proxmox support is not critical here, because it's only
+ # used for performance settings like 'max-workers'.
+ my $qemu_support = eval { mon_cmd($vmid, "query-proxmox-support") };
+ log_warn($@) if $@;
+
+ $attach_tpmstate_drive->($self, $task, $vmid);
+
my $outfh;
if ($opts->{stdout}) {
$outfh = $opts->{stdout};
devlist => $devlist
};
$params->{'firewall-file'} = $firewall if -e $firewall;
+ add_backup_performance_options($params, $opts->{performance}, $qemu_support);
$qmpclient->queue_cmd($vmid, $backup_cb, 'backup', %$params);
};
if ($err) {
$self->logerr($err);
$self->mon_backup_cancel($vmid);
+ $self->resume_vm_after_job_start($task, $vmid);
}
$self->restore_vm_power_state($vmid);
sub qga_fs_freeze {
my ($self, $task, $vmid) = @_;
- return if !$self->{vmlist}->{$vmid}->{agent} || $task->{mode} eq 'stop' || !$self->{vm_was_running};
+ return if !$self->{vmlist}->{$vmid}->{agent} || $task->{mode} eq 'stop' || !$self->{vm_was_running} || $self->{vm_was_paused};
if (!PVE::QemuServer::qga_check_running($vmid, 1)) {
$self->loginfo("skipping guest-agent 'fs-freeze', agent configured but not running?");
return;
}
+ my $freeze = PVE::QemuServer::get_qga_key($self->{vmlist}->{$vmid}, 'freeze-fs-on-backup') // 1;
+ if (!$freeze) {
+ $self->loginfo("skipping guest-agent 'fs-freeze', disabled in VM options");
+ return;
+ }
+
$self->loginfo("issuing guest-agent 'fs-freeze' command");
eval { mon_cmd($vmid, "guest-fsfreeze-freeze") };
$self->logerr($@) if $@;
die $@ if $@;
}
-sub register_qmeventd_handle {
- my ($self, $vmid) = @_;
-
- my $fh;
- my $peer = "/var/run/qmeventd.sock";
- my $count = 0;
-
- for (;;) {
- $count++;
- $fh = IO::Socket::UNIX->new(Peer => $peer, Blocking => 0, Timeout => 1);
- last if $fh;
- if ($! != EINTR && $! != EAGAIN) {
- $self->log("warn", "unable to connect to qmeventd socket (vmid: $vmid) - $!\n");
- return;
- }
- if ($count > 4) {
- $self->log("warn", "unable to connect to qmeventd socket (vmid: $vmid)"
- . " - timeout after $count retries\n");
- return;
- }
- usleep(25000);
- }
-
- # send handshake to mark VM as backing up
- print $fh to_json({vzdump => {vmid => "$vmid"}});
- $self->{qmeventd_fh} = $fh;
-}
-
-# resume VM againe once we got in a clear state (stop mode backup of running VM)
+# resume VM again once in a clear state (stop mode backup of running VM)
sub resume_vm_after_job_start {
my ($self, $task, $vmid) = @_;
- return if !$self->{vm_was_running};
+ return if !$self->{vm_was_running} || $self->{vm_was_paused};
if (my $stoptime = $task->{vmstoptime}) {
my $delay = time() - $task->{vmstoptime};
} else {
$self->loginfo("resuming VM again");
}
- mon_cmd($vmid, 'cont');
+ mon_cmd($vmid, 'cont', timeout => 45);
}
# stop again if VM was not running before
sub cleanup {
my ($self, $task, $vmid) = @_;
+ $detach_tpmstate_drive->($task, $vmid);
+
if ($self->{qmeventd_fh}) {
close($self->{qmeventd_fh});
}