use base qw (PVE::VZDump::Plugin);
+my $default_mount_point = "/mnt/vzsnap0";
+
my $rsync_vm = sub {
- my ($self, $task, $from, $to, $text) = @_;
+ my ($self, $task, $to, $text) = @_;
+ my $disks = $task->{disks};
+ my $from = $disks->[0]->{dir} . '/';
$self->loginfo ("starting $text sync $from to $to");
- my $starttime = time();
-
my $opts = $self->{vzdump}->{opts};
- my $rsyncopts = "--stats -x -X --numeric-ids";
+ my @xattr = $task->{no_xattrs} ? () : ('-X', '-A');
- $rsyncopts .= " --bwlimit=$opts->{bwlimit}" if $opts->{bwlimit};
-
- $self->cmd ("rsync $rsyncopts -aH --delete --no-whole-file --inplace '$from' '$to'");
+ my $rsync = ['rsync', '--stats', @xattr, '--numeric-ids',
+ '-aH', '--delete', '--no-whole-file', '--inplace',
+ '--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}};
+ my $starttime = time();
+ # 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}";
+ }
+ $self->cmd([@$rsync, $to]);
my $delay = time () - $starttime;
$self->loginfo ("$text sync finished ($delay seconds)");
return wantarray ? ($running, $running ? 'running' : 'stopped') : $running;
}
-my $loop_mount_image = sub {
- my ($image_path, $mountpoint) = @_;
-
- my $loopdev;
- my $mounted;
- eval {
- my $parser = sub {
- my $line = shift;
- $loopdev = $line if $line =~m|^/dev/loop\d+$|;
- };
- PVE::Tools::run_command(['losetup', '--find', '--show', $image_path], outfunc => $parser);
-
- File::Path::mkpath($mountpoint);
- PVE::Tools::run_command(['mount', '-t', 'ext4', $loopdev, $mountpoint]);
- $mounted = 1;
- };
- if (my $err = $@) {
- if ($mounted) {
- eval { PVE::Tools::run_command(['umount', '-d', $mountpoint]) };
- warn $@ if $@;
- } else {
- eval { PVE::Tools::run_command(['losetup', '-d', $loopdev]) if $loopdev; };
- warn $@ if $@;
- }
- die $err;
- }
+my $check_mountpoint_empty = sub {
+ my ($mountpoint) = @_;
+
+ die "mountpoint '$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 "mountpoint '$mountpoint' not empty\n";
+ });
};
sub prepare {
my ($self, $task, $vmid, $mode) = @_;
my $conf = $self->{vmlist}->{$vmid} = PVE::LXC::load_config($vmid);
-
- PVE::LXC::foreach_mountpoint($conf, sub {
- my ($ms, $mountpoint) = @_;
-
- return if $ms eq 'rootfs';
- # TODO: implement support for mountpoints
- die "unable to backup mountpoint '$ms' - feature not implemented\n";
- });
+ my $storage_cfg = $self->{storecfg};
my $running = PVE::LXC::check_running($vmid);
- my $diskinfo = {};
- $task->{diskinfo} = $diskinfo;
+ my $disks = $task->{disks} = [];
+ my $exclude_dirs = $task->{exclude_dirs} = [];
$task->{hostname} = $conf->{'hostname'} || "CT$vmid";
- my $rootinfo = PVE::LXC::parse_ct_mountpoint($conf->{rootfs});
- my $volid = $rootinfo->{volume};
+ my ($id_map, $rootuid, $rootgid) = PVE::LXC::parse_id_maps($conf);
+ $task->{userns_cmd} = PVE::LXC::userns_command($id_map);
- # fixme: when do we deactivate ??
- PVE::Storage::activate_volumes($self->{storecfg}, [$volid]) if $volid;
+ my $volid_list = [];
+ PVE::LXC::foreach_mountpoint($conf, sub {
+ my ($name, $data) = @_;
+ my $volid = $data->{volume};
+ my $mount = $data->{mp};
+ my $type = $data->{type};
- if ($mode eq 'snapshot') {
+ return if !$volid || !$mount;
- die "mode failure - storage does not support snapshots (no volid)\n"
- if !$volid;
+ if ($name ne 'rootfs' && !$data->{backup}) {
+ push @$exclude_dirs, $mount;
+ return;
+ }
- die "mode failure - storage does not support snapshots\n"
- if !PVE::Storage::volume_has_feature($self->{storecfg}, 'snapshot', $volid);
-
- my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
+ push @$disks, $data;
+ push @$volid_list, $volid
+ if $type eq 'volume';
+ });
- my $scfg = PVE::Storage::storage_config($self->{storecfg}, $sid);
+ if ($mode eq 'snapshot') {
+ if (!PVE::LXC::has_feature('vzdump', $conf, $storage_cfg)) {
+ die "mode failure - some volumes do not support snapshots\n";
+ }
- # we only handle well known types for now, because the storage
- # library dos not handle mount/unmount of snapshots
+ unlock_vm($self, $vmid);
- if ($scfg->{type} ne 'zfs') {
- $diskinfo->{mountpoint} = "/mnt/vzsnap0";
- } else {
- die "mode failure - storage does not support snapshot mount\n"
+ if ($conf->{snapshots} && $conf->{snapshots}->{vzdump}) {
+ $self->loginfo("found old vzdump snapshot (force removal)");
+ PVE::LXC::snapshot_delete($vmid, 'vzdump', 1);
}
-
- PVE::Storage::volume_snapshot($self->{storecfg}, $volid, '__vzdump__');
- $task->{cleanup}->{snap_volid} = $volid;
-
- die "implement me";
-
+
+ my $rootdir = $default_mount_point;
+ mkpath $rootdir;
+ &$check_mountpoint_empty($rootdir);
+
+ # set snapshot_count (freezes CT if snapshot_count > 1)
+ $task->{snapshot_count} = scalar(@$volid_list);
} elsif ($mode eq 'stop') {
- my $mountpoint = "/mnt/vzsnap0";
- my $path = PVE::Storage::path($self->{storecfg}, $volid);
- &$loop_mount_image($path, $mountpoint);
- $task->{cleanup}->{snapshot_mount} = 1;
- $diskinfo->{dir} = $diskinfo->{mountpoint} = $mountpoint;
- $task->{snapdir} = $diskinfo->{dir};
+ my $rootdir = $default_mount_point;
+ mkpath $rootdir;
+ &$check_mountpoint_empty($rootdir);
+ PVE::Storage::activate_volumes($storage_cfg, $volid_list);
} elsif ($mode eq 'suspend') {
- my $tasks_fn = "/sys/fs/cgroup/cpu/lxc/$vmid/tasks";
- my $init_pid = PVE::Tools::file_read_firstline($tasks_fn);
- if ($init_pid =~ m/^(\d+)$/) {
- $diskinfo->{dir} = "/proc/$1/root";
- } else {
- die "unable to find container init task\n";
+ 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-rprivate', '/']);
+ }
}
sub lock_vm {
my ($self, $vmid) = @_;
- PVE::LXC::lock_aquire($vmid);
+ my $lockconfig = sub {
+ my ($self, $vmid) = @_;
+
+ my $conf = PVE::LXC::load_config($vmid);
+
+ PVE::LXC::check_lock($conf);
+ $conf->{lock} = 'backup';
+
+ PVE::LXC::write_config($vmid, $conf);
+ };
+
+ PVE::LXC::lock_container($vmid, 10, $lockconfig, ($self, $vmid));
}
sub unlock_vm {
my ($self, $vmid) = @_;
- PVE::LXC::lock_release($vmid);
+ my $unlockconfig = sub {
+ my ($self, $vmid) = @_;
+
+ my $conf = PVE::LXC::load_config($vmid);
+
+ if ($conf->{lock} && $conf->{lock} eq 'backup') {
+ delete $conf->{lock};
+ PVE::LXC::write_config($vmid, $conf);
+ }
+ };
+
+ PVE::LXC::lock_container($vmid, 60, $unlockconfig, ($self, $vmid));
+}
+
+sub snapshot {
+ my ($self, $task, $vmid) = @_;
+
+ $self->loginfo("create storage snapshot 'vzdump'");
+
+ # todo: freeze/unfreeze if we have more than one volid
+ PVE::LXC::snapshot_create($vmid, 'vzdump', "vzdump backup snapshot");
+ $task->{cleanup}->{remove_snapshot} = 1;
+
+ # reload config
+ my $conf = $self->{vmlist}->{$vmid} = PVE::LXC::load_config($vmid);
+ die "unable to read vzdump snapshot config - internal error"
+ if !($conf->{snapshots} && $conf->{snapshots}->{vzdump});
+
+ my $disks = $task->{disks};
+ #todo: reevaluate bind/dev mount handling when implementing snapshots for mps
+ my $volid_list = [map { $_->{volume} } @$disks];
+
+ my $rootdir = $default_mount_point;
+ my $storage_cfg = $self->{storecfg};
+
+ PVE::Storage::activate_volumes($storage_cfg, $volid_list, 'vzdump');
+ foreach my $disk (@$disks) {
+ $disk->{dir} = "${rootdir}$disk->{mp}";
+ PVE::LXC::mountpoint_mount($disk, $rootdir, $storage_cfg, 'vzdump');
+ }
+
+ $task->{snapdir} = $rootdir;
}
sub copy_data_phase1 {
my ($self, $task) = @_;
- $self->$rsync_vm($task, "$task->{diskinfo}->{dir}/", $task->{snapdir}, "first");
+ if (my $mntinfo = PVE::VZDump::get_mount_info($task->{snapdir})) {
+ if ($mntinfo->{fstype} =~ /^nfs4?/) {
+ warn "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->{diskinfo}->{dir}/", $task->{snapdir}, "final");
+ $self->$rsync_vm($task, $task->{snapdir}, "final");
}
sub stop_vm {
my ($self, $task, $vmid) = @_;
$self->cmd("lxc-stop -n $vmid");
+
+ # make sure container is stopped
+ $self->cmd("lxc-wait -n $vmid -s STOPPED");
}
sub start_vm {
sub assemble {
my ($self, $task, $vmid) = @_;
- my $dir = $task->{snapdir};
-
- $task->{cleanup}->{etc_vzdump} = 1;
+ my $tmpdir = $task->{tmpdir};
- mkpath "$dir/etc/vzdump/";
+ mkpath "$tmpdir/etc/vzdump/";
my $conf = PVE::LXC::load_config($vmid);
+ delete $conf->{lock};
delete $conf->{snapshots};
delete $conf->{'pve.parent'};
- PVE::Tools::file_set_contents("$dir/etc/vzdump/pct.conf", PVE::LXC::write_pct_config("/lxc/$vmid.conf", $conf));
+ PVE::Tools::file_set_contents("$tmpdir/etc/vzdump/pct.conf", PVE::LXC::write_pct_config("/lxc/$vmid.conf", $conf));
+
+ my $firewall ="/etc/pve/firewall/$vmid.fw";
+ if (-e $firewall) {
+ PVE::Tools::file_copy($firewall, "$tmpdir/etc/vzdump/pct.fw");
+ $task->{fw} = 1;
+ }
}
sub archive {
my ($self, $task, $vmid, $filename, $comp) = @_;
-
- my $findexcl = $self->{vzdump}->{findexcl};
- my $findargs = join (' ', @$findexcl) . ' -print0';
- my $opts = $self->{vzdump}->{opts};
- my $srcdir = $task->{diskinfo}->{dir};
+ my $disks = $task->{disks};
+ my @sources;
+
+ if ($task->{mode} eq 'stop') {
+ my $rootdir = $default_mount_point;
+ my $storage_cfg = $self->{storecfg};
+ foreach my $disk (@$disks) {
+ $disk->{dir} = "${rootdir}$disk->{mp}";
+ PVE::LXC::mountpoint_mount($disk, $rootdir, $storage_cfg);
+ # 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;
+ } 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 $taropts = "--totals --sparse --numeric-owner --no-recursion --xattrs --one-file-system";
+ my $userns_cmd = $task->{userns_cmd};
+ my $tar = [@$userns_cmd, 'tar', 'cpf', '-', '--totals',
+ @$PVE::LXC::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 we disable this optimization for now
+ # we disable this optimization for now
#if ($snapdir eq $task->{tmpdir} && $snapdir =~ m|^$opts->{dumpdir}/|) {
- # $taropts .= " --remove-files"; # try to save space
+ # push @$tar, "--remove-files"; # try to save space
#}
- my $cmd = "(";
+ # 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";
+ push @$tar, '--no-anchored', '--exclude=lost+found' if $userns_cmd;
+ push @$tar, '--anchored';
+ push @$tar, map { "--exclude=.$_" } @{$self->{vzdump}->{findexcl}};
- $cmd .= "cd $snapdir;find . $findargs|sed 's/\\\\/\\\\\\\\/g'|";
- $cmd .= "tar cpf - $taropts ./etc/vzdump/pct.conf --null -T -";
- my $bwl = $opts->{bwlimit}*1024; # bandwidth limit for cstream
- $cmd .= "|cstream -t $bwl" if $opts->{bwlimit};
- $cmd .= "|$comp" if $comp;
+ push @$tar, @sources;
+
+ my $cmd = [ $tar ];
- $cmd .= ")";
+ 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}));
+ push @{$cmd->[-1]}, \(">&" . fileno($opts->{stdout}));
} else {
- $self->cmd ("$cmd >$filename");
+ push @{$cmd->[-1]}, \(">" . PVE::Tools::shellquote($filename));
}
+ $self->cmd($cmd);
}
sub cleanup {
my ($self, $task, $vmid) = @_;
- my $di = $task->{diskinfo};
+ my $conf = PVE::LXC::load_config($vmid);
- if ($task->{cleanup}->{snapshot_mount}) {
- # Note: sleep to avoid 'device is busy' message.
- # Seems Kernel need some time to cleanup open file list,
- # for example when we stop the tar with kill (stop task)
- # We use -d to automatically free used loop devices
- sleep(1);
- $self->cmd_noerr("umount -d $di->{mountpoint}");
+ if ($task->{mode} ne 'suspend') {
+ my $rootdir = $default_mount_point;
+ my $disks = $task->{disks};
+ foreach my $disk (reverse @$disks) {
+ PVE::Tools::run_command(['umount', '-l', '-d', $disk->{dir}]) if $disk->{dir};
+ }
}
- if (my $volid = $task->{cleanup}->{snap_volid}) {
- eval { PVE::Storage::volume_snapshot_delete($self->{storecfg}, $volid, '__vzdump__'); };
- warn $@ if $@;
- }
-
- if ($task->{cleanup}->{etc_vzdump}) {
- my $dir = "$task->{snapdir}/etc/vzdump";
- eval { rmtree $dir if -d $dir; };
- $self->logerr ($@) if $@;
+ if ($task->{cleanup}->{remove_snapshot}) {
+ $self->loginfo("remove vzdump snapshot");
+ PVE::LXC::snapshot_delete($vmid, 'vzdump', 0);
}
-
}
1;