Upstream proxmox-git/pve-container state of the touched files before d7df6c1e5f

This commit is contained in:
upstream committed 2026-10-02 16:38:17 +02:00
1 parent 96e2453797
commit 73dec5df85
1 file changed
+599
+599
View File
@@ -0,0 +1,599 @@
package PVE::VZDump::LXC;
use strict;
use warnings;
use File::Basename;
use File::Path;
use POSIX qw(strftime);
use PVE::Cluster qw(cfs_read_file);
use PVE::GuestHelpers;
use PVE::INotify;
use PVE::LXC::Config;
use PVE::LXC::Namespaces;
use PVE::LXC;
use PVE::Storage;
use PVE::Tools;
use PVE::VZDump;
use base qw (PVE::VZDump::Plugin);
my $default_mount_point = "/mnt/vzsnap0";
my $rsync_vm = sub {
my ($self, $task, $to, $text) = @_;
my $disks = $task->{disks};
my $from = $disks->[0]->{dir};
$self->loginfo("starting $text sync $from to $to");
my $opts = $self->{vzdump}->{opts};
my @xattr = $task->{no_xattrs} ? () : ('-X', '-A');
my $rsync = [
'rsync',
'--stats',
'-h',
@xattr,
'--numeric-ids',
'-aH',
'--delete',
'--no-whole-file',
'--sparse',
'--one-file-system',
'--relative',
];
push @$rsync, "--bwlimit=$opts->{bwlimit}" if $opts->{bwlimit};
push @$rsync, map { "--exclude=$_" } @{ $self->{vzdump}->{findexcl} };
push @$rsync, map { "--exclude=$_" } @{ $task->{exclude_dirs} };
# See the rsync(1) manpage for --relative in conjunction with /./ in paths.
# This is the only way to have exclude-dirs work together with the
# --one-file-system option.
# This way we can pass multiple source paths and tell rsync which directory
# they're supposed to be relative to.
# Otherwise with eg. using multiple rsync commands means the --exclude
# directives need to be modified for every command as they are meant to be
# relative to the rootdir, while rsync treats them as relative to the
# source dir.
foreach my $disk (@$disks) {
push @$rsync, "$from/.$disk->{mp}";
}
my $transferred = '';
my $outfunc = sub {
return if $_[0] !~ /^Total transferred file size: (.+)$/;
$transferred = $1;
};
my $errfunc = sub { $self->logerr($_[0]); };
my $starttime = time();
PVE::Tools::run_command([@$rsync, $to], outfunc => $outfunc, errfunc => $errfunc);
my $delay = time() - $starttime;
$self->loginfo("$text sync finished - transferred $transferred in ${delay}s");
};
sub new {
my ($class, $vzdump) = @_;
PVE::VZDump::check_bin('lxc-stop');
PVE::VZDump::check_bin('lxc-start');
my $self = bless {}, $class;
$self->{vzdump} = $vzdump;
$self->{storecfg} = PVE::Storage::config();
$self->{vmlist} = PVE::LXC::config_list();
return $self;
}
sub type {
return 'lxc';
}
sub vm_status {
my ($self, $vmid) = @_;
my $running = PVE::LXC::check_running($vmid) ? 1 : 0;
return wantarray ? ($running, $running ? 'running' : 'stopped') : $running;
}
my $check_mountpoint_empty = sub {
my ($mountpoint) = @_;
die "mount point '$mountpoint' is not a directory\n" if !-d $mountpoint;
PVE::Tools::dir_glob_foreach(
$mountpoint,
qr/.*/,
sub {
my $entry = shift;
return if $entry eq '.' || $entry eq '..';
die "mount point '$mountpoint' not empty\n";
},
);
};
sub prepare {
my ($self, $task, $vmid, $mode) = @_;
my $conf = $self->{vmlist}->{$vmid} = PVE::LXC::Config->load_config($vmid);
my $storage_cfg = $self->{storecfg};
$self->loginfo("CT Name: $conf->{hostname}")
if defined($conf->{hostname});
my $running = PVE::LXC::check_running($vmid);
my $disks = $task->{disks} = [];
my $exclude_dirs = $task->{exclude_dirs} = [];
$task->{hostname} = $conf->{'hostname'} || "CT$vmid";
my ($id_map, $root_uid, $root_gid) = PVE::LXC::parse_id_maps($conf);
$task->{userns_cmd} = PVE::LXC::userns_command($id_map);
$task->{id_map} = $id_map;
$task->{root_uid} = $root_uid;
$task->{root_gid} = $root_gid;
my $volids = $task->{volids} = [];
my $backup_volumes = PVE::LXC::Config->get_backup_volumes($conf);
for my $volume (@{$backup_volumes}) {
my $name = $volume->{key};
my $included = $volume->{included};
my $volume_config = $volume->{volume_config};
my $volid = $volume_config->{volume};
my $mount = $volume_config->{mp};
next if !$volid || !$mount;
if (!$included) {
my $type = $volume_config->{type};
push @$exclude_dirs, $mount;
$self->loginfo(
"excluding $type mount point $name ('$mount') from backup ($volume->{reason})");
next;
}
$volume_config->{name} = $name;
# immutable raw base images need RO mount
if ($conf->{template} && !defined($volume_config->{ro})) {
$volume_config->{ro} = 1;
}
$self->loginfo("including mount point $name ('$mount') in backup");
push @$disks, $volume_config;
push @$volids, $volid if $included;
}
if ($mode eq 'snapshot') {
if (!PVE::LXC::Config->has_feature('snapshot', $conf, $storage_cfg, undef, undef, 1)) {
die "mode failure - some volumes do not support snapshots\n";
}
if ($conf->{snapshots} && $conf->{snapshots}->{vzdump}) {
$self->loginfo("found old vzdump snapshot (force removal)");
PVE::LXC::Config->lock_config(
$vmid,
sub {
$self->unlock_vm($vmid);
PVE::LXC::Config->snapshot_delete($vmid, 'vzdump', 1);
$self->lock_vm($vmid);
},
);
}
my $rootdir = $default_mount_point;
mkpath $rootdir;
&$check_mountpoint_empty($rootdir);
# set snapshot_count (freezes CT if snapshot_count > 1)
$task->{snapshot_count} = scalar(@$volids);
} elsif ($mode eq 'stop') {
my $rootdir = $default_mount_point;
mkpath $rootdir;
&$check_mountpoint_empty($rootdir);
} elsif ($mode eq 'suspend') {
my $pid = PVE::LXC::find_lxc_pid($vmid);
foreach my $disk (@$disks) {
$disk->{dir} = "/proc/$pid/root$disk->{mp}";
}
$task->{snapdir} = $task->{tmpdir};
} else {
unlock_vm($self, $vmid);
die "unknown mode '$mode'\n"; # should not happen
}
if ($mode ne 'suspend') {
# If we perform mount operations, let's unshare the mount namespace
# to not influence the running host.
PVE::Tools::unshare(PVE::Tools::CLONE_NEWNS);
PVE::Tools::run_command(['mount', '--make-rslave', '/']);
}
}
sub lock_vm {
my ($self, $vmid) = @_;
PVE::LXC::Config->set_lock($vmid, 'backup');
}
sub unlock_vm {
my ($self, $vmid) = @_;
PVE::LXC::Config->remove_lock($vmid, 'backup');
}
sub snapshot {
my ($self, $task, $vmid) = @_;
$self->loginfo("create storage snapshot 'vzdump'");
# todo: freeze/unfreeze if we have more than one volid
PVE::LXC::Config->lock_config(
$vmid,
sub {
$self->unlock_vm($vmid);
PVE::LXC::Config->snapshot_create($vmid, 'vzdump', 0, "vzdump backup snapshot");
$self->lock_vm($vmid);
},
);
$task->{cleanup}->{remove_snapshot} = 1;
# reload config
my $conf = $self->{vmlist}->{$vmid} = PVE::LXC::Config->load_config($vmid);
die "unable to read vzdump snapshot config - internal error"
if !($conf->{snapshots} && $conf->{snapshots}->{vzdump});
my $disks = $task->{disks};
my $volids = $task->{volids};
my $rootdir = $default_mount_point;
my $storage_cfg = $self->{storecfg};
PVE::Storage::activate_volumes($storage_cfg, $volids, 'vzdump');
foreach my $disk (@$disks) {
$disk->{dir} = "${rootdir}$disk->{mp}";
PVE::LXC::mountpoint_mount(
$disk, $rootdir, $storage_cfg, 'vzdump', $task->{root_uid}, $task->{root_gid},
);
}
$task->{snapdir} = $rootdir;
}
sub copy_data_phase1 {
my ($self, $task) = @_;
if (my $mntinfo = PVE::VZDump::get_mount_info($task->{snapdir})) {
if ($mntinfo->{fstype} =~ /^nfs4?/) {
$self->loginfo("temporary directory is on NFS, disabling xattr and acl"
. " support, consider configuring a local tmpdir via"
. " /etc/vzdump.conf\n");
$task->{no_xattrs} = 1;
}
}
$self->$rsync_vm($task, $task->{snapdir}, "first");
}
sub copy_data_phase2 {
my ($self, $task) = @_;
$self->$rsync_vm($task, $task->{snapdir}, "final");
}
sub stop_vm {
my ($self, $task, $vmid) = @_;
my $opts = $self->{vzdump}->{opts};
my $timeout = $opts->{stopwait} * 60;
PVE::LXC::vm_stop($vmid, 0, $timeout);
}
sub start_vm {
my ($self, $task, $vmid) = @_;
my $conf = PVE::LXC::Config->load_config($vmid);
PVE::LXC::vm_start($vmid, $conf);
}
sub suspend_vm {
my ($self, $task, $vmid) = @_;
PVE::LXC::freeze($vmid);
}
sub resume_vm {
my ($self, $task, $vmid) = @_;
PVE::LXC::thaw($vmid);
}
sub assemble {
my ($self, $task, $vmid) = @_;
my $opts = $self->{vzdump}->{opts};
my $conf = PVE::LXC::Config->load_config($vmid);
delete $conf->{lock};
delete $conf->{snapshots};
delete $conf->{parent};
delete $conf->{pending};
my $tmpdir = $task->{tmpdir};
mkpath "$tmpdir/etc/vzdump/";
PVE::Tools::file_set_contents(
"$tmpdir/etc/vzdump/pct.conf",
PVE::LXC::Config::write_pct_config("/lxc/$vmid.conf", $conf),
);
my $firewall = "/etc/pve/firewall/$vmid.fw";
my $fwconftmp = "$tmpdir/etc/vzdump/pct.fw";
if ($self->{vzdump}->{opts}->{pbs}) {
# fixme: do not store pct.conf and fw.conf into $tmpdir
if (-e $firewall) {
PVE::Tools::file_copy($firewall, $fwconftmp);
}
} else {
if (-e $firewall) {
PVE::Tools::file_copy($firewall, $fwconftmp);
} else {
PVE::Tools::file_set_contents($fwconftmp, '');
}
$task->{fw} = 1;
}
}
sub archive {
my ($self, $task, $vmid, $filename, $comp) = @_;
my $disks = $task->{disks};
my @sources;
if ($task->{mode} eq 'stop') {
my $storage_cfg = $self->{storecfg};
PVE::Storage::activate_volumes($storage_cfg, $task->{volids});
my $rootdir = $default_mount_point;
foreach my $disk (@$disks) {
$disk->{dir} = "${rootdir}$disk->{mp}";
PVE::LXC::mountpoint_mount(
$disk, $rootdir, $storage_cfg, undef, $task->{root_uid}, $task->{root_gid},
);
# add every enabled mountpoint (since we use --one-file-system)
# mp already starts with a / so we only need to add the dot
push @sources, ".$disk->{mp}";
}
$task->{snapdir} = $rootdir;
} elsif ($task->{mode} eq 'snapshot') {
# mounting the vzdump snapshots and setting $snapdir is already done,
# but we need to include all mountpoints here!
foreach my $disk (@$disks) {
push @sources, ".$disk->{mp}";
}
} else {
# the data was rsynced to a temporary location, only use '.' to avoid
# having mountpoints duplicated
push @sources, '.';
}
my $opts = $self->{vzdump}->{opts};
my $snapdir = $task->{snapdir};
my $tmpdir = $task->{tmpdir};
my $userns_cmd = $task->{userns_cmd};
my $findexcl = $self->{vzdump}->{findexcl};
if (my $backup_provider = $self->{vzdump}->{'backup-provider'}) {
$self->loginfo("starting external backup via " . $backup_provider->provider_name());
if (!scalar($task->{id_map}->@*) || $task->{root_uid} == 0 || $task->{root_gid} == 0) {
$self->log(
"warn",
"external backup of privileged container can only be restored as"
. " unprivileged which might not work in all cases",
);
}
my $mechanism = $backup_provider->backup_get_mechanism($vmid, 'lxc');
die "mechanism '$mechanism' requested by backup provider is not supported for containers\n"
if $mechanism ne 'directory';
$self->loginfo("using backup mechanism '$mechanism'");
my $guest_config = PVE::Tools::file_get_contents("$tmpdir/etc/vzdump/pct.conf");
my $firewall_file = "$tmpdir/etc/vzdump/pct.fw";
my $info = {
directory => $snapdir,
sources => [@sources],
'backup-user-id' => $task->{root_uid},
};
$info->{'firewall-config'} = PVE::Tools::file_get_contents($firewall_file)
if -e $firewall_file;
$info->{'bandwidth-limit'} = $opts->{bwlimit} * 1024 if $opts->{bwlimit};
$backup_provider->backup_container_prepare($vmid, $info);
if (scalar($task->{id_map}->@*)) {
PVE::LXC::Namespaces::run_in_userns(
sub {
$backup_provider->backup_container($vmid, $guest_config, $findexcl, $info);
},
$task->{id_map},
);
} else {
$backup_provider->backup_container($vmid, $guest_config, $findexcl, $info);
}
} elsif ($self->{vzdump}->{opts}->{pbs}) {
my $param = [];
push @$param, "pct.conf:$tmpdir/etc/vzdump/pct.conf";
my $fw_conf = "$tmpdir/etc/vzdump/pct.fw";
if (-f $fw_conf) {
push @$param, "fw.conf:$fw_conf";
}
my $rootdir = $snapdir;
push @$param, "root.pxar:$rootdir";
foreach my $disk (@sources) {
push @$param, '--include-dev', "$snapdir/$disk";
}
push @$param, '--skip-lost-and-found' if $userns_cmd;
push @$param, map { "--exclude=$_" } @$findexcl;
push @$param, '--backup-type', 'ct';
push @$param, '--backup-id', $vmid;
push @$param, '--backup-time', $task->{backup_time};
push @$param, '--change-detection-mode', $opts->{"pbs-change-detection-mode"}
if $opts->{"pbs-change-detection-mode"};
if (my $entries_max = $opts->{"performance"}->{"pbs-entries-max"}) {
push $param->@*, '--entries-max', $entries_max;
$self->loginfo(
"set max number of entries in memory for file-based backups to $entries_max");
}
my @storage = ($opts->{scfg}, $opts->{storage});
my $logfunc = sub { my $line = shift; $self->loginfo($line) };
PVE::Storage::PBSPlugin::run_raw_client_cmd(
@storage, 'backup', $param,
logfunc => $logfunc,
userns_cmd => $userns_cmd,
);
# FIXME: move to a pve-common helper, storage has a similar one
my $time_iso8601 = strftime('%FT%TZ', gmtime($task->{backup_time}));
my $snapshot_id = "ct/$vmid/$time_iso8601";
my $res =
eval { PVE::Storage::PBSPlugin::run_client_cmd(@storage, 'files', [$snapshot_id]) }
// [];
$task->{size} = 0;
$task->{size} += $_->{size} for @$res;
} else {
my $tar = [
@$userns_cmd,
'tar',
'cpf',
'-',
'--totals',
@PVE::Storage::Plugin::COMMON_TAR_FLAGS,
'--one-file-system',
'--warning=no-file-ignored',
];
# note: --remove-files does not work because we do not
# backup all files (filters). tar complains:
# Cannot rmdir: Directory not empty
# we disable this optimization for now
#if ($snapdir eq $task->{tmpdir} && $snapdir =~ m|^$opts->{dumpdir}/|) {
# push @$tar, "--remove-files"; # try to save space
#}
# The directory parameter can give an alternative directory as source.
# the second parameter gives the structure in the tar.
push @$tar, "--directory=$tmpdir", './etc/vzdump/pct.conf';
push @$tar, "./etc/vzdump/pct.fw" if $task->{fw};
push @$tar, "--directory=$snapdir";
my @findexcl_no_anchored = ();
my @findexcl_anchored = ();
foreach my $pattern (@{$findexcl}) {
if ($pattern !~ m|^/|) {
push @findexcl_no_anchored, $pattern;
} else {
push @findexcl_anchored, $pattern;
}
# NOTE rsync and proxmox-backup-client will match directories, but not files when there
# is a trailing slash, tar won't match either. For suspend mode, rsync already did the
# exclusion, so no need to warn.
# TODO PVE 9 - consider matching "$pattern*" and "$pattern.*" in this case, which will
# only include the empty directory to more closely align the behavior between different
# modes. Don't forget to update the docs!
$self->log("warn", "tar does not match exclusion with a trailing slash '$pattern'")
if $pattern =~ m|/$| && $task->{mode} ne 'suspend';
}
push @$tar, '--no-anchored';
push @$tar, '--exclude=lost+found' if $userns_cmd;
push @$tar, map { "--exclude=$_" } @findexcl_no_anchored;
push @$tar, '--anchored';
push @$tar, map { "--exclude=.$_" } @findexcl_anchored;
push @$tar, '--', @sources;
my $cmd = [$tar];
my $bwl = $opts->{bwlimit} * 1024; # bandwidth limit for cstream
push @$cmd, ['cstream', '-t', $bwl] if $opts->{bwlimit};
push @$cmd, [split(/\s+/, $comp)] if $comp;
if ($opts->{stdout}) {
$self->cmd($cmd, output => ">&" . fileno($opts->{stdout}));
} else {
push @{ $cmd->[-1] }, \(">" . PVE::Tools::shellquote($filename));
$self->cmd($cmd);
}
}
}
sub cleanup {
my ($self, $task, $vmid) = @_;
my $conf = PVE::LXC::Config->load_config($vmid);
if ($task->{mode} ne 'suspend') {
my $disks = $task->{disks};
foreach my $disk (reverse @$disks) {
PVE::Tools::run_command(['umount', '-l', '-d', $disk->{dir}]) if $disk->{dir};
}
}
if ($task->{cleanup}->{remove_snapshot}) {
my $lock_obtained;
my $do_delete = sub {
$lock_obtained = 1;
$self->loginfo("cleanup temporary 'vzdump' snapshot");
PVE::LXC::Config->snapshot_delete($vmid, 'vzdump', 0);
};
eval {
eval { PVE::GuestHelpers::guest_migration_lock($vmid, 1, $do_delete); };
if (my $err = $@) {
die $err if $lock_obtained;
$self->loginfo("waiting for active replication or other operation..");
PVE::GuestHelpers::guest_migration_lock($vmid, 600, $do_delete);
}
};
# will be cleaned up by next backup run anyway
warn "snapshot 'vzdump' was not (fully) removed - $@" if $@;
}
}
1;