]> git.proxmox.com Git - pve-storage.git/blobdiff - PVE/Storage/BTRFSPlugin.pm
rbd: fix variable declared in conditional statement
[pve-storage.git] / PVE / Storage / BTRFSPlugin.pm
index 370d848a212e6f029b01ef56ee7b0bdd9c9584cd..1db4e4f8712fd23ebb8e2eba6292ac836cfd6432 100644 (file)
@@ -6,11 +6,12 @@ use warnings;
 use base qw(PVE::Storage::Plugin);
 
 use Fcntl qw(S_ISDIR O_WRONLY O_CREAT O_EXCL);
-use File::Basename qw(dirname);
+use File::Basename qw(basename dirname);
 use File::Path qw(mkpath);
 use IO::Dir;
+use POSIX qw(EEXIST);
 
-use PVE::Tools qw(run_command);
+use PVE::Tools qw(run_command dir_glob_foreach);
 
 use PVE::Storage::DirPlugin;
 
@@ -19,6 +20,7 @@ use constant {
     FS_NOCOW_FL => 0x00800000,
     FS_IOC_GETFLAGS => 0x40086602,
     FS_IOC_SETFLAGS => 0x80086601,
+    BTRFS_MAGIC => 0x9123683e,
 };
 
 # Configuration (similar to DirPlugin)
@@ -41,7 +43,20 @@ sub plugindata {
            },
            { images => 1, rootdir => 1 },
        ],
-       format => [ { raw => 1, qcow2 => 1, vmdk => 1, subvol => 1 }, 'raw', ],
+       format => [ { raw => 1, subvol => 1 }, 'raw', ],
+    };
+}
+
+sub properties {
+    return {
+       nocow => {
+           description => "Set the NOCOW flag on files."
+               . " Disables data checksumming and causes data errors to be unrecoverable from"
+               . " while allowing direct I/O. Only use this if data does not need to be any more"
+               . " safe than on a single ext4 formatted disk with no underlying raid system.",
+           type => 'boolean',
+           default => 0,
+       },
     };
 }
 
@@ -52,9 +67,14 @@ sub options {
        shared => { optional => 1 },
        disable => { optional => 1 },
        maxfiles => { optional => 1 },
+       'prune-backups' => { optional => 1 },
+       'max-protected-backups' => { optional => 1 },
        content => { optional => 1 },
        format => { optional => 1 },
        is_mountpoint => { optional => 1 },
+       nocow => { optional => 1 },
+       mkdir => { optional => 1 },
+       preallocation => { optional => 1 },
        # TODO: The new variant of mkdir with  `populate` vs `create`...
     };
 }
@@ -74,9 +94,43 @@ sub check_config {
     return PVE::Storage::DirPlugin::check_config($self, $sectionId, $config, $create, $skipSchemaCheck);
 }
 
+my sub getfsmagic($) {
+    my ($path) = @_;
+    # The field type sizes in `struct statfs` are defined in a rather annoying way, and we only
+    # need the first field, which is a `long` for our supported platforms.
+    # Should be moved to pve-rs, so this can be the problem of the `libc` crate ;-)
+    # Just round up and extract what we need:
+    my $buf = pack('x160');
+    if (0 != syscall(&PVE::Syscall::SYS_statfs, $path, $buf)) {
+       die "statfs on '$path' failed - $!\n";
+    }
+
+    return unpack('L!', $buf);
+}
+
+my sub assert_btrfs($) {
+    my ($path) = @_;
+    die "'$path' is not a btrfs file system\n"
+       if getfsmagic($path) != BTRFS_MAGIC;
+}
+
 sub activate_storage {
     my ($class, $storeid, $scfg, $cache) = @_;
-    return PVE::Storage::DirPlugin::activate_storage($class, $storeid, $scfg, $cache);
+
+    my $path = $scfg->{path};
+    if (!defined($scfg->{mkdir}) || $scfg->{mkdir}) {
+       mkpath $path;
+    }
+
+    my $mp = PVE::Storage::DirPlugin::parse_is_mountpoint($scfg);
+    if (defined($mp) && !PVE::Storage::DirPlugin::path_is_mounted($mp, $cache->{mountdata})) {
+       die "unable to activate storage '$storeid' - directory is expected to be a mount point but"
+       ." is not mounted: '$mp'\n";
+    }
+
+    assert_btrfs($path); # only assert this stuff now, ensures $path is there and better UX
+
+    $class->SUPER::activate_storage($storeid, $scfg, $cache);
 }
 
 sub status {
@@ -84,9 +138,22 @@ sub status {
     return PVE::Storage::DirPlugin::status($class, $storeid, $scfg, $cache);
 }
 
-# TODO: sub get_volume_notes {}
+sub get_volume_attribute {
+    my ($class, $scfg, $storeid, $volname, $attribute) = @_;
+    return PVE::Storage::DirPlugin::get_volume_attribute($class, $scfg, $storeid, $volname, $attribute);
+}
 
-# TODO: sub update_volume_notes {}
+sub update_volume_attribute {
+    my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_;
+    return PVE::Storage::DirPlugin::update_volume_attribute(
+       $class,
+       $scfg,
+       $storeid,
+       $volname,
+       $attribute,
+       $value,
+    );
+}
 
 # croak would not include the caller from within this module
 sub __error {
@@ -128,13 +195,13 @@ sub filesystem_path {
 
     $path .= "/$vmid" if $vtype eq 'images';
 
-    if ($format eq 'raw') {
+    if (defined($format) && $format eq 'raw') {
        my $dir = raw_name_to_dir($name);
        if ($snapname) {
            $dir .= "\@$snapname";
        }
        $path .= "/$dir/disk.raw";
-    } elsif ($format eq 'subvol') {
+    } elsif (defined($format) && $format eq 'subvol') {
        $path .= "/$name";
        if ($snapname) {
            $path .= "\@$snapname";
@@ -257,7 +324,7 @@ sub alloc_image {
     my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
 
     if ($fmt ne 'raw' && $fmt ne 'subvol') {
-       return PVE::Storage::DirPlugin::alloc_image(@_);
+       return $class->SUPER::alloc_image($storeid, $scfg, $vmid, $fmt, $name, $size);
     }
 
     # From Plugin.pm:
@@ -310,7 +377,7 @@ sub alloc_image {
        } elsif ($fmt eq 'raw') {
            sysopen my $fh, $path, O_WRONLY | O_CREAT | O_EXCL
                or die "failed to create raw file '$path' - $!\n";
-           chattr($fh, ~FS_NOCOW_FL, FS_NOCOW_FL);
+           chattr($fh, ~FS_NOCOW_FL, FS_NOCOW_FL) if $scfg->{nocow};
            truncate($fh, $size * 1024)
                or die "failed to set file size for '$path' - $!\n";
            close($fh);
@@ -357,8 +424,8 @@ sub free_image {
     my (undef, undef, $vmid, undef, undef, undef, $format) =
        $class->parse_volname($volname);
 
-    if ($format ne 'subvol' && $format ne 'raw') {
-       return PVE::Storage::DirPlugin::free_image(@_);
+    if (!defined($format) || ($format ne 'subvol' && $format ne 'raw')) {
+       return $class->SUPER::free_image($storeid, $scfg, $volname, $isBase, $_format);
     }
 
     my $path = $class->filesystem_path($scfg, $volname);
@@ -369,9 +436,11 @@ sub free_image {
     }
 
     my $dir = dirname($subvol);
+    my $basename = basename($subvol);
     my @snapshot_vols;
     foreach_subvol($dir, sub {
        my ($volume, $name, $snapshot) = @_;
+       return if $name ne $basename;
        return if !defined $snapshot;
        push @snapshot_vols, "$dir/$volume";
     });
@@ -413,7 +482,7 @@ sub volume_size_info {
 
     my $format = ($class->parse_volname($volname))[6];
 
-    if ($format eq 'subvol') {
+    if (defined($format) && $format eq 'subvol') {
        my $ctime = (stat($path))[10];
        my ($used, $size) = (0, 0);
        #my ($used, $size) = btrfs_subvol_quota($class, $path); # uses wantarray
@@ -461,7 +530,7 @@ sub volume_snapshot {
 }
 
 sub volume_rollback_is_possible {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_; 
+    my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_;
 
     return 1; 
 }
@@ -537,18 +606,21 @@ sub volume_has_feature {
            current => { raw => 1 },
            snap => { raw => 1 },
        },
-       template => { current => { qcow2 => 1, raw => 1, vmdk => 1, subvol => 1 } },
+       template => {
+           current => { qcow2 => 1, raw => 1, vmdk => 1, subvol => 1 },
+       },
        copy => {
            base => { qcow2 => 1, raw => 1, subvol => 1, vmdk => 1 },
            current => { qcow2 => 1, raw => 1, subvol => 1, vmdk => 1 },
            snap => { qcow2 => 1, raw => 1, subvol => 1 },
        },
-       sparseinit => { base => {qcow2 => 1, raw => 1, vmdk => 1 },
-                       current => {qcow2 => 1, raw => 1, vmdk => 1 } },
+       sparseinit => {
+           base => { qcow2 => 1, raw => 1, vmdk => 1 },
+           current => { qcow2 => 1, raw => 1, vmdk => 1 },
+       },
     };
 
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) =
-       $class->parse_volname($volname);
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = $class->parse_volname($volname);
 
     my $key = undef;
     if ($snapname) {
@@ -612,23 +684,250 @@ sub list_images {
     return $res;
 }
 
-# For now we don't implement `btrfs send/recv` as it needs some updates to our import/export API
-# first!
-
 sub volume_export_formats {
-    return PVE::Storage::DirPlugin::volume_export_formats(@_);
-}
+    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
 
-sub volume_export {
-    return PVE::Storage::DirPlugin::volume_export(@_);
+    # We can do whatever `DirPlugin` can do.
+    my @result = PVE::Storage::Plugin::volume_export_formats(@_);
+
+    # `btrfs send` only works on snapshots:
+    return @result if !defined $snapshot;
+
+    # Incremental stream with snapshots is only supported if the snapshots are listed (new api):
+    return @result if defined($base_snapshot) && $with_snapshots && ref($with_snapshots) ne 'ARRAY';
+
+    # Otherwise we do also support `with_snapshots`.
+
+    # Finally, `btrfs send` only works on formats where we actually use btrfs subvolumes:
+    my $format = ($class->parse_volname($volname))[6];
+    return @result if $format ne 'raw' && $format ne 'subvol';
+
+    return ('btrfs', @result);
 }
 
 sub volume_import_formats {
-    return PVE::Storage::DirPlugin::volume_import_formats(@_);
+    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
+
+    # Same as export-formats, beware the parameter order:
+    return volume_export_formats(
+       $class,
+       $scfg,
+       $storeid,
+       $volname,
+       $snapshot,
+       $base_snapshot,
+       $with_snapshots,
+    );
+}
+
+sub volume_export {
+    my (
+       $class,
+       $scfg,
+       $storeid,
+       $fh,
+       $volname,
+       $format,
+       $snapshot,
+       $base_snapshot,
+       $with_snapshots,
+    ) = @_;
+
+    if ($format ne 'btrfs') {
+       return PVE::Storage::Plugin::volume_export(@_);
+    }
+
+    die "format 'btrfs' only works on snapshots\n"
+       if !defined $snapshot;
+
+    die "'btrfs' format in incremental mode requires snapshots to be listed explicitly\n"
+       if defined($base_snapshot) && $with_snapshots && ref($with_snapshots) ne 'ARRAY';
+
+    my $volume_format = ($class->parse_volname($volname))[6];
+
+    die "btrfs-sending volumes of type $volume_format ('$volname') is not supported\n"
+       if $volume_format ne 'raw' && $volume_format ne 'subvol';
+
+    my $path = $class->path($scfg, $volname, $storeid);
+
+    if ($volume_format eq 'raw') {
+       $path = raw_file_to_subvol($path);
+    }
+
+    my $cmd = ['btrfs', '-q', 'send', '-e'];
+    if ($base_snapshot) {
+       my $base = $class->path($scfg, $volname, $storeid, $base_snapshot);
+       if ($volume_format eq 'raw') {
+           $base = raw_file_to_subvol($base);
+       }
+       push @$cmd, '-p', $base;
+    }
+    push @$cmd, '--';
+    if (ref($with_snapshots) eq 'ARRAY') {
+       push @$cmd, (map { "$path\@$_" } ($with_snapshots // [])->@*), $path;
+    } else {
+       dir_glob_foreach(dirname($path), $BTRFS_VOL_REGEX, sub {
+           push @$cmd, "$path\@$_[2]" if !(defined($snapshot) && $_[2] eq $snapshot);
+       });
+    }
+    $path .= "\@$snapshot" if defined($snapshot);
+    push @$cmd, $path;
+
+    run_command($cmd, output => '>&'.fileno($fh));
+    return;
 }
 
 sub volume_import {
-    return PVE::Storage::DirPlugin::volume_import(@_);
+    my (
+       $class,
+       $scfg,
+       $storeid,
+       $fh,
+       $volname,
+       $format,
+       $snapshot,
+       $base_snapshot,
+       $with_snapshots,
+       $allow_rename,
+    ) = @_;
+
+    if ($format ne 'btrfs') {
+       return PVE::Storage::Plugin::volume_import(@_);
+    }
+
+    die "format 'btrfs' only works on snapshots\n"
+       if !defined $snapshot;
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $volume_format) =
+       $class->parse_volname($volname);
+
+    die "btrfs-receiving volumes of type $volume_format ('$volname') is not supported\n"
+       if $volume_format ne 'raw' && $volume_format ne 'subvol';
+
+    if (defined($base_snapshot)) {
+       my $path = $class->path($scfg, $volname, $storeid, $base_snapshot);
+       die "base snapshot '$base_snapshot' not found - no such directory '$path'\n"
+           if !path_is_subvolume($path);
+    }
+
+    my $destination = $class->filesystem_path($scfg, $volname);
+    if ($volume_format eq 'raw') {
+       $destination = raw_file_to_subvol($destination);
+    }
+
+    if (!defined($base_snapshot) && -e $destination) {
+       die "volume $volname already exists\n" if !$allow_rename;
+       $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $volume_format, 1);
+    }
+
+    my $imagedir = $class->get_subdir($scfg, $vtype);
+    $imagedir .= "/$vmid" if $vtype eq 'images';
+
+    my $tmppath = "$imagedir/recv.$vmid.tmp";
+    mkdir($imagedir); # FIXME: if $scfg->{mkdir};
+    if (!mkdir($tmppath)) {
+       die "temp receive directory already exists at '$tmppath', incomplete concurrent import?\n"
+           if $! == EEXIST;
+       die "failed to create temporary receive directory at '$tmppath' - $!\n";
+    }
+
+    my $dh = IO::Dir->new($tmppath)
+       or die "failed to open temporary receive directory '$tmppath' - $!\n";
+    eval {
+       run_command(['btrfs', '-q', 'receive', '-e', '--', $tmppath], input => '<&'.fileno($fh));
+
+       # Analyze the received subvolumes;
+       my ($diskname, $found_snapshot, @snapshots);
+       $dh->rewind;
+       while (defined(my $entry = $dh->read)) {
+           next if $entry eq '.' || $entry eq '..';
+           next if $entry !~ /^$BTRFS_VOL_REGEX$/;
+           my ($cur_diskname, $cur_snapshot) = ($1, $2);
+
+           die "send stream included a non-snapshot subvolume\n"
+               if !defined($cur_snapshot);
+
+           if (!defined($diskname)) {
+               $diskname = $cur_diskname;
+           } else {
+               die "multiple disks contained in stream ('$diskname' vs '$cur_diskname')\n"
+                   if $diskname ne $cur_diskname;
+           }
+
+           if ($cur_snapshot eq $snapshot) {
+               $found_snapshot = 1;
+           } else {
+               push @snapshots, $cur_snapshot;
+           }
+       }
+
+       die "send stream did not contain the expected current snapshot '$snapshot'\n"
+           if !$found_snapshot;
+
+       # Rotate the disk into place, first the current state:
+       # Note that read-only subvolumes cannot be moved into different directories, but for the
+       # "current" state we also want a writable copy, so start with that:
+       $class->btrfs_cmd(['property', 'set', "$tmppath/$diskname\@$snapshot", 'ro', 'false']);
+       PVE::Tools::renameat2(
+           -1,
+           "$tmppath/$diskname\@$snapshot",
+           -1,
+           $destination,
+           &PVE::Tools::RENAME_NOREPLACE,
+       ) or die "failed to move received snapshot '$tmppath/$diskname\@$snapshot'"
+           . " into place at '$destination' - $!\n";
+
+       # Now recreate the actual snapshot:
+       $class->btrfs_cmd([
+           'subvolume',
+           'snapshot',
+           '-r',
+           '--',
+           $destination,
+           "$destination\@$snapshot",
+       ]);
+
+       # Now go through the remaining snapshots (if any)
+       foreach my $snap (@snapshots) {
+           $class->btrfs_cmd(['property', 'set', "$tmppath/$diskname\@$snap", 'ro', 'false']);
+           PVE::Tools::renameat2(
+               -1,
+               "$tmppath/$diskname\@$snap",
+               -1,
+               "$destination\@$snap",
+               &PVE::Tools::RENAME_NOREPLACE,
+           ) or die "failed to move received snapshot '$tmppath/$diskname\@$snap'"
+               . " into place at '$destination\@$snap' - $!\n";
+           eval { $class->btrfs_cmd(['property', 'set', "$destination\@$snap", 'ro', 'true']) };
+           warn "failed to make $destination\@$snap read-only - $!\n" if $@;
+       }
+    };
+    my $err = $@;
+
+    eval {
+       # Cleanup all the received snapshots we did not move into place, so we can remove the temp
+       # directory.
+       if ($dh) {
+           $dh->rewind;
+           while (defined(my $entry = $dh->read)) {
+               next if $entry eq '.' || $entry eq '..';
+               eval { $class->btrfs_cmd(['subvolume', 'delete', '--', "$tmppath/$entry"]) };
+               warn $@ if $@;
+           }
+           $dh->close; undef $dh;
+       }
+       if (!rmdir($tmppath)) {
+           warn "failed to remove temporary directory '$tmppath' - $!\n"
+       }
+    };
+    warn $@ if $@;
+    if ($err) {
+       # clean up if the directory ended up being empty after an error
+       rmdir($tmppath);
+       die $err;
+    }
+
+    return "$storeid:$volname";
 }
 
 1