]> git.proxmox.com Git - pve-container.git/blobdiff - src/PVE/VZDump/LXC.pm
Use foreach_volume instead of foreach_mountpoint-variants
[pve-container.git] / src / PVE / VZDump / LXC.pm
index 21fb3c11f80d5c7e67a01b89140762f99d6accc3..2d003d0f45cf7fb04a92682cfc6b416aa5817195 100644 (file)
@@ -2,31 +2,55 @@ package PVE::VZDump::LXC;
 
 use strict;
 use warnings;
-use File::Path;
+
 use File::Basename;
-use PVE::INotify;
+use File::Path;
+
 use PVE::Cluster qw(cfs_read_file);
+use PVE::INotify;
+use PVE::LXC::Config;
+use PVE::LXC;
 use PVE::Storage;
+use PVE::Tools;
 use PVE::VZDump;
-use PVE::LXC;
 
 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, $first) = @_;
 
+    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',
+                 ($first ? '--sparse' : '--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)");
@@ -34,7 +58,7 @@ my $rsync_vm = sub {
 
 sub new {
     my ($class, $vzdump) = @_;
-    
+
     PVE::VZDump::check_bin('lxc-stop');
     PVE::VZDump::check_bin('lxc-start');
     PVE::VZDump::check_bin('lxc-freeze');
@@ -44,7 +68,7 @@ sub new {
 
     $self->{vzdump} = $vzdump;
     $self->{storecfg} = PVE::Storage::config();
-    
+
     $self->{vmlist} = PVE::LXC::config_list();
 
     return $self;
@@ -58,152 +82,193 @@ sub vm_status {
     my ($self, $vmid) = @_;
 
     my $running = PVE::LXC::check_running($vmid) ? 1 : 0;
-   
-    return wantarray ? ($running, $running ? 'running' : 'stopped') : $running; 
+
+    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 "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::load_config($vmid);
+    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 $diskinfo = {};
-    $task->{diskinfo} = $diskinfo;
+    my $disks = $task->{disks} = [];
+    my $exclude_dirs = $task->{exclude_dirs} = [];
 
-    $task->{hostname} = $conf->{'lxc.utsname'} || "CT$vmid";
+    $task->{hostname} = $conf->{'hostname'} || "CT$vmid";
 
-    my $volid = $conf->{'pve.volid'};
+    my ($id_map, $rootuid, $rootgid) = PVE::LXC::parse_id_maps($conf);
+    $task->{userns_cmd} = PVE::LXC::userns_command($id_map);
+    $task->{rootuid} = $rootuid;
+    $task->{rootgid} = $rootgid;
 
-    # fixme: whe do we deactivate ??
-    PVE::Storage::activate_volumes($self->{storecfg}, [$volid]) if $volid;
+    my $volids = $task->{volids} = [];
+    PVE::LXC::Config->foreach_volume($conf, sub {
+       my ($name, $data) = @_;
+       my $volid = $data->{volume};
+       my $mount = $data->{mp};
+       my $type = $data->{type};
 
-    my $rootfs = $conf->{'lxc.rootfs'};
+       return if !$volid || !$mount;
 
-    if ($mode eq 'snapshot') {
+       if (!PVE::LXC::Config->mountpoint_backup_enabled($name, $data)) {
+           push @$exclude_dirs, $mount;
+           $self->loginfo("excluding $type mount point $name ('$mount') from backup");
+           return;
+       }
 
-       die "mode failure - storage does not support snapshots (no volid)\n" 
-           if !$volid;
+       $data->{name} = $name;
 
-       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, 1);
+       # immutable raw base images need RO mount
+       if ($conf->{template} && !defined($data->{ro})) {
+           $data->{ro} = 1;
+       }
+       push @$disks, $data;
+       push @$volids, $volid
+           if $type eq 'volume';
+    });
 
-       my $scfg = PVE::Storage::storage_config($self->{storecfg}, $sid);
+    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";
+       }
 
-       # we only handle well known types for now, because the storage
-       # library dos not handle mount/unmount of snapshots
 
-       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::Config->lock_config($vmid, sub {
+               $self->unlock_vm($vmid);
+               PVE::LXC::Config->snapshot_delete($vmid, 'vzdump', 1);
+               $self->lock_vm($vmid);
+           });
        }
-       
-       PVE::Storage::volume_snapshot($self->{storecfg}, $volid, '__vzdump__');
-       $task->{cleanup}->{snap_volid} = $volid;
-       
-       # $diskinfo->{dir} = $rootfs;
-       die "implement me";
-       
-    } else {
 
-       if ($rootfs =~ m!^/! && -d $rootfs) {
-           $diskinfo->{dir} = $rootfs;
-       } else {
-           if ($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;
-           } 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";
-               }
-           } else {
-               die "unknown mode '$mode'\n"; # should not happen
-           }
+       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 eq 'suspend') {
-           $task->{snapdir} = $task->{tmpdir};
-       } else {
-           $task->{snapdir} = $diskinfo->{dir};
-       }
+    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::lock_aquire($vmid);
+    PVE::LXC::Config->set_lock($vmid, 'backup');
 }
 
 sub unlock_vm {
     my ($self, $vmid) = @_;
 
-    PVE::LXC::lock_release($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->{rootuid}, $task->{rootgid});
+    }
+
+    $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?/) {
+           $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", 1);
 }
 
 sub copy_data_phase2 {
     my ($self, $task) = @_;
 
-    $self->$rsync_vm ($task, "$task->{diskinfo}->{dir}/", $task->{snapdir}, "final");
+    $self->$rsync_vm($task, $task->{snapdir}, "final", 0);
 }
 
 sub stop_vm {
     my ($self, $task, $vmid) = @_;
 
-    $self->cmd("lxc-stop -n $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) = @_;
 
-    $self->cmd ("lxc-start -n $vmid");
+    $self->cmd(['systemctl', 'start', "pve-container\@$vmid"]);
 }
 
 sub suspend_vm {
@@ -221,78 +286,162 @@ sub resume_vm {
 sub assemble {
     my ($self, $task, $vmid) = @_;
 
-    my $conffile = PVE::LXC::config_file($vmid);
+    my $opts = $self->{vzdump}->{opts};
 
-    my $dir = $task->{snapdir};
+    my $conf = PVE::LXC::Config->load_config($vmid);
+    delete $conf->{lock};
+    delete $conf->{snapshots};
+    delete $conf->{parent};
+    delete $conf->{pending};
 
-    $task->{cleanup}->{etc_vzdump} = 1;
+    my $tmpdir = $task->{tmpdir};
 
-    mkpath "$dir/etc/vzdump/";
-    $self->cmd ("cp '$conffile' '$dir/etc/vzdump/lxc.conf'");
+    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 ($opts->{scfg}->{type} eq '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 $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 $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->{rootuid}, $task->{rootgid});
+           # 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};
+
+    if ($opts->{scfg}->{type} eq 'pbs') {
 
-    my $taropts = "--totals --sparse --numeric-owner --no-recursion --xattrs --one-file-system";
+       my $rootdir = $default_mount_point;
+       my $param = [];
 
-    # 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
-    #if ($snapdir eq $task->{tmpdir} && $snapdir =~ m|^$opts->{dumpdir}/|) {
-    #       $taropts .= " --remove-files"; # try to save space
-    #}
+       push @$param, "pct.conf:$tmpdir/etc/vzdump/pct.conf";
 
-    my $cmd = "(";
+       my $fw_conf = "$tmpdir/etc/vzdump/pct.fw";
+       if (-f $fw_conf) {
+           push @$param, "fw.conf:$fw_conf";
+       }
+
+       push @$param, "root.pxar:$rootdir";
+
+       foreach my $disk (@$disks) {
+           push @$param, '--include-dev', $disk->{dir};
+       }
+
+       push @$param, '--skip-lost-and-found' if $userns_cmd;
 
-    $cmd .= "cd $snapdir;find . $findargs|sed 's/\\\\/\\\\\\\\/g'|";
-    $cmd .= "tar cpf - $taropts etc/vzdump/lxc.conf --null -T -";
-    my $bwl = $opts->{bwlimit}*1024; # bandwidth limit for cstream
-    $cmd .= "|cstream -t $bwl" if $opts->{bwlimit};
-    $cmd .= "|$comp" if $comp;
+       push @$param, '--backup-type', 'ct';
+       push @$param, '--backup-id', $vmid;
+       push @$param, '--backup-time', $task->{backup_time};
 
-    $cmd .= ")";
+       my $logfunc = sub { my $line = shift; $self->loginfo($line); };
+       PVE::Storage::PBSPlugin::run_raw_client_cmd(
+           $opts->{scfg}, $opts->{storage}, 'backup', $param,
+           logfunc => $logfunc, userns_cmd => $userns_cmd);
 
-    if ($opts->{stdout}) {
-       $self->cmd ($cmd, output => ">&" . fileno($opts->{stdout}));
     } else {
-       $self->cmd ("$cmd >$filename");
+
+       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";
+       push @$tar, '--no-anchored', '--exclude=lost+found' if $userns_cmd;
+       push @$tar, '--anchored';
+       push @$tar, map { "--exclude=.$_" } @{$self->{vzdump}->{findexcl}};
+
+       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 $di = $task->{diskinfo};
+    my $conf = PVE::LXC::Config->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}->{remove_snapshot}) {
+       $self->loginfo("remove vzdump snapshot");
+       PVE::LXC::Config->snapshot_delete($vmid, 'vzdump', 0);
     }
-    
-    if ($task->{cleanup}->{etc_vzdump}) {
-       my $dir = "$task->{snapdir}/etc/vzdump";
-       eval { rmtree $dir if -d $dir; };
-       $self->logerr ($@) if $@;
-    }
-
 }
 
 1;