X-Git-Url: https://git.proxmox.com/?p=pve-storage.git;a=blobdiff_plain;f=PVE%2FStorage%2FBTRFSPlugin.pm;h=63d307df6e8f4b5fa6cbb9239cb8481a4e9cc735;hp=5360dca7184e3e7070c2a5568bc604bdb5778e30;hb=HEAD;hpb=f449cddc799699bfa0b37f9cdf2b40f5392fb280 diff --git a/PVE/Storage/BTRFSPlugin.pm b/PVE/Storage/BTRFSPlugin.pm deleted file mode 100644 index 5360dca..0000000 --- a/PVE/Storage/BTRFSPlugin.pm +++ /dev/null @@ -1,911 +0,0 @@ -package PVE::Storage::BTRFSPlugin; - -use strict; -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::Path qw(mkpath); -use IO::Dir; -use POSIX qw(EEXIST); - -use PVE::Tools qw(run_command dir_glob_foreach); - -use PVE::Storage::DirPlugin; - -use constant { - BTRFS_FIRST_FREE_OBJECTID => 256, - FS_NOCOW_FL => 0x00800000, - FS_IOC_GETFLAGS => 0x40086602, - FS_IOC_SETFLAGS => 0x80086601, - BTRFS_MAGIC => 0x9123683e, -}; - -# Configuration (similar to DirPlugin) - -sub type { - return 'btrfs'; -} - -sub plugindata { - return { - content => [ - { - images => 1, - rootdir => 1, - vztmpl => 1, - iso => 1, - backup => 1, - snippets => 1, - none => 1, - }, - { images => 1, rootdir => 1 }, - ], - 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, - }, - }; -} - -sub options { - return { - path => { fixed => 1 }, - nodes => { optional => 1 }, - shared => { optional => 1 }, - disable => { optional => 1 }, - maxfiles => { optional => 1 }, - content => { optional => 1 }, - format => { optional => 1 }, - is_mountpoint => { optional => 1 }, - nocow => { optional => 1 }, - # TODO: The new variant of mkdir with `populate` vs `create`... - }; -} - -# Storage implementation -# -# We use the same volume names are directory plugins, but map *raw* disk image file names into a -# subdirectory. -# -# `vm-VMID-disk-ID.raw` -# -> `images/VMID/vm-VMID-disk-ID/disk.raw` -# where the `vm-VMID-disk-ID/` subdirectory is a btrfs subvolume - -# Reuse `DirPlugin`'s `check_config`. This simply checks for invalid paths. -sub check_config { - my ($self, $sectionId, $config, $create, $skipSchemaCheck) = @_; - 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) = @_; - - my $path = $scfg->{path}; - if (!defined($scfg->{mkdir}) || $scfg->{mkdir}) { - mkpath $path; - } - - my $mp = PVE::Storage::DirPlugin::parse_is_mountpoint($scfg); - if (defined($mp) && !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 { - my ($class, $storeid, $scfg, $cache) = @_; - return PVE::Storage::DirPlugin::status($class, $storeid, $scfg, $cache); -} - -# TODO: sub get_volume_notes {} - -# TODO: sub update_volume_notes {} - -# croak would not include the caller from within this module -sub __error { - my ($msg) = @_; - my (undef, $f, $n) = caller(1); - die "$msg at $f: $n\n"; -} - -# Given a name (eg. `vm-VMID-disk-ID.raw`), take the part up to the format suffix as the name of -# the subdirectory (subvolume). -sub raw_name_to_dir($) { - my ($raw) = @_; - - # For the subvolume directory Strip the `.` suffix: - if ($raw =~ /^(.*)\.raw$/) { - return $1; - } - - __error "internal error: bad disk name: $raw"; -} - -sub raw_file_to_subvol($) { - my ($file) = @_; - - if ($file =~ m|^(.*)/disk\.raw$|) { - return "$1"; - } - - __error "internal error: bad raw path: $file"; -} - -sub filesystem_path { - my ($class, $scfg, $volname, $snapname) = @_; - - my ($vtype, $name, $vmid, undef, undef, $isBase, $format) = - $class->parse_volname($volname); - - my $path = $class->get_subdir($scfg, $vtype); - - $path .= "/$vmid" if $vtype eq 'images'; - - if ($format eq 'raw') { - my $dir = raw_name_to_dir($name); - if ($snapname) { - $dir .= "\@$snapname"; - } - $path .= "/$dir/disk.raw"; - } elsif ($format eq 'subvol') { - $path .= "/$name"; - if ($snapname) { - $path .= "\@$snapname"; - } - } else { - $path .= "/$name"; - } - - return wantarray ? ($path, $vmid, $vtype) : $path; -} - -sub btrfs_cmd { - my ($class, $cmd, $outfunc) = @_; - - my $msg = ''; - my $func; - if (defined($outfunc)) { - $func = sub { - my $part = &$outfunc(@_); - $msg .= $part if defined($part); - }; - } else { - $func = sub { $msg .= "$_[0]\n" }; - } - run_command(['btrfs', '-q', @$cmd], errmsg => 'btrfs error', outfunc => $func); - - return $msg; -} - -sub btrfs_get_subvol_id { - my ($class, $path) = @_; - my $info = $class->btrfs_cmd(['subvolume', 'show', '--', $path]); - if ($info !~ /^\s*(?:Object|Subvolume) ID:\s*(\d+)$/m) { - die "failed to get btrfs subvolume ID from: $info\n"; - } - return $1; -} - -my sub chattr : prototype($$$) { - my ($fh, $mask, $xor) = @_; - - my $flags = pack('L!', 0); - ioctl($fh, FS_IOC_GETFLAGS, $flags) or die "FS_IOC_GETFLAGS failed - $!\n"; - $flags = pack('L!', (unpack('L!', $flags) & $mask) ^ $xor); - ioctl($fh, FS_IOC_SETFLAGS, $flags) or die "FS_IOC_SETFLAGS failed - $!\n"; - return 1; -} - -sub create_base { - my ($class, $storeid, $scfg, $volname) = @_; - - my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = - $class->parse_volname($volname); - - my $newname = $name; - $newname =~ s/^vm-/base-/; - - # If we're not working with a 'raw' file, which is the only thing that's "different" for btrfs, - # or a subvolume, we forward to the DirPlugin - if ($format ne 'raw' && $format ne 'subvol') { - return PVE::Storage::DirPlugin::create_base(@_); - } - - my $path = $class->filesystem_path($scfg, $volname); - my $newvolname = $basename ? "$basevmid/$basename/$vmid/$newname" : "$vmid/$newname"; - my $newpath = $class->filesystem_path($scfg, $newvolname); - - my $subvol = $path; - my $newsubvol = $newpath; - if ($format eq 'raw') { - $subvol = raw_file_to_subvol($subvol); - $newsubvol = raw_file_to_subvol($newsubvol); - } - - rename($subvol, $newsubvol) - || die "rename '$subvol' to '$newsubvol' failed - $!\n"; - eval { $class->btrfs_cmd(['property', 'set', $newsubvol, 'ro', 'true']) }; - warn $@ if $@; - - return $newvolname; -} - -sub clone_image { - my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_; - - my ($vtype, $basename, $basevmid, undef, undef, $isBase, $format) = - $class->parse_volname($volname); - - # If we're not working with a 'raw' file, which is the only thing that's "different" for btrfs, - # or a subvolume, we forward to the DirPlugin - if ($format ne 'raw' && $format ne 'subvol') { - return PVE::Storage::DirPlugin::clone_image(@_); - } - - my $imagedir = $class->get_subdir($scfg, 'images'); - $imagedir .= "/$vmid"; - mkpath $imagedir; - - my $path = $class->filesystem_path($scfg, $volname); - my $newname = $class->find_free_diskname($storeid, $scfg, $vmid, $format, 1); - - # For btrfs subvolumes we don't actually need the "link": - #my $newvolname = "$basevmid/$basename/$vmid/$newname"; - my $newvolname = "$vmid/$newname"; - my $newpath = $class->filesystem_path($scfg, $newvolname); - - my $subvol = $path; - my $newsubvol = $newpath; - if ($format eq 'raw') { - $subvol = raw_file_to_subvol($subvol); - $newsubvol = raw_file_to_subvol($newsubvol); - } - - $class->btrfs_cmd(['subvolume', 'snapshot', '--', $subvol, $newsubvol]); - - return $newvolname; -} - -sub alloc_image { - my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_; - - if ($fmt ne 'raw' && $fmt ne 'subvol') { - return PVE::Storage::DirPlugin::alloc_image(@_); - } - - # From Plugin.pm: - - my $imagedir = $class->get_subdir($scfg, 'images') . "/$vmid"; - - mkpath $imagedir; - - $name = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt, 1) if !$name; - - my (undef, $tmpfmt) = PVE::Storage::Plugin::parse_name_dir($name); - - die "illegal name '$name' - wrong extension for format ('$tmpfmt != '$fmt')\n" - if $tmpfmt ne $fmt; - - # End copy from Plugin.pm - - my $subvol = "$imagedir/$name"; - # .raw is not part of the directory name - $subvol =~ s/\.raw$//; - - die "disk image '$subvol' already exists\n" if -e $subvol; - - my $path; - if ($fmt eq 'raw') { - $path = "$subvol/disk.raw"; - } - - if ($fmt eq 'subvol' && !!$size) { - # NOTE: `btrfs send/recv` actually drops quota information so supporting subvolumes with - # quotas doesn't play nice with send/recv. - die "btrfs quotas are currently not supported, use an unsized subvolume or a raw file\n"; - } - - $class->btrfs_cmd(['subvolume', 'create', '--', $subvol]); - - eval { - if ($fmt eq 'subvol') { - # Nothing to do for now... - - # This is how we *would* do it: - # # Use the subvol's default 0/$id qgroup - # eval { - # # This call should happen at storage creation instead and therefore governed by a - # # configuration option! - # # $class->btrfs_cmd(['quota', 'enable', $subvol]); - # my $id = $class->btrfs_get_subvol_id($subvol); - # $class->btrfs_cmd(['qgroup', 'limit', "${size}k", "0/$id", $subvol]); - # }; - } 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) if $scfg->{nocow}; - truncate($fh, $size * 1024) - or die "failed to set file size for '$path' - $!\n"; - close($fh); - } else { - die "internal format error (format = $fmt)\n"; - } - }; - - if (my $err = $@) { - eval { $class->btrfs_cmd(['subvolume', 'delete', '--', $subvol]); }; - warn $@ if $@; - die $err; - } - - return "$vmid/$name"; -} - -# Same as btrfsprogs does: -my sub path_is_subvolume : prototype($) { - my ($path) = @_; - my @stat = stat($path) - or die "stat failed on '$path' - $!\n"; - my ($ino, $mode) = @stat[1, 2]; - return S_ISDIR($mode) && $ino == BTRFS_FIRST_FREE_OBJECTID; -} - -my $BTRFS_VOL_REGEX = qr/((?:vm|base|subvol)-\d+-disk-\d+(?:\.subvol)?)(?:\@(\S+))$/; - -# Calls `$code->($volume, $name, $snapshot)` for each subvol in a directory matching our volume -# regex. -my sub foreach_subvol : prototype($$) { - my ($dir, $code) = @_; - - dir_glob_foreach($dir, $BTRFS_VOL_REGEX, sub { - my ($volume, $name, $snapshot) = ($1, $2, $3); - return if !path_is_subvolume("$dir/$volume"); - $code->($volume, $name, $snapshot); - }) -} - -sub free_image { - my ($class, $storeid, $scfg, $volname, $isBase, $_format) = @_; - - 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(@_); - } - - my $path = $class->filesystem_path($scfg, $volname); - - my $subvol = $path; - if ($format eq 'raw') { - $subvol = raw_file_to_subvol($path); - } - - my $dir = dirname($subvol); - my @snapshot_vols; - foreach_subvol($dir, sub { - my ($volume, $name, $snapshot) = @_; - return if !defined $snapshot; - push @snapshot_vols, "$dir/$volume"; - }); - - $class->btrfs_cmd(['subvolume', 'delete', '--', @snapshot_vols, $subvol]); - # try to cleanup directory to not clutter storage with empty $vmid dirs if - # all images from a guest got deleted - rmdir($dir); - - return undef; -} - -# Currently not used because quotas clash with send/recv. -# my sub btrfs_subvol_quota { -# my ($class, $path) = @_; -# my $id = '0/' . $class->btrfs_get_subvol_id($path); -# my $search = qr/^\Q$id\E\s+(\d)+\s+\d+\s+(\d+)\s*$/; -# my ($used, $size); -# $class->btrfs_cmd(['qgroup', 'show', '--raw', '-rf', '--', $path], sub { -# return if defined($size); -# if ($_[0] =~ $search) { -# ($used, $size) = ($1, $2); -# } -# }); -# if (!defined($size)) { -# # syslog should include more information: -# syslog('err', "failed to get subvolume size for: $path (id $id)"); -# # UI should only see the last path component: -# $path =~ s|^.*/||; -# die "failed to get subvolume size for $path\n"; -# } -# return wantarray ? ($used, $size) : $size; -# } - -sub volume_size_info { - my ($class, $scfg, $storeid, $volname, $timeout) = @_; - - my $path = $class->filesystem_path($scfg, $volname); - - my $format = ($class->parse_volname($volname))[6]; - - if ($format eq 'subvol') { - my $ctime = (stat($path))[10]; - my ($used, $size) = (0, 0); - #my ($used, $size) = btrfs_subvol_quota($class, $path); # uses wantarray - return wantarray ? ($size, 'subvol', $used, undef, $ctime) : 1; - } - - return PVE::Storage::Plugin::file_size_info($path, $timeout); -} - -sub volume_resize { - my ($class, $scfg, $storeid, $volname, $size, $running) = @_; - - my $format = ($class->parse_volname($volname))[6]; - if ($format eq 'subvol') { - my $path = $class->filesystem_path($scfg, $volname); - my $id = '0/' . $class->btrfs_get_subvol_id($path); - $class->btrfs_cmd(['qgroup', 'limit', '--', "${size}k", "0/$id", $path]); - return undef; - } - - return PVE::Storage::Plugin::volume_resize(@_); -} - -sub volume_snapshot { - my ($class, $scfg, $storeid, $volname, $snap) = @_; - - my ($name, $vmid, $format) = ($class->parse_volname($volname))[1,2,6]; - if ($format ne 'subvol' && $format ne 'raw') { - return PVE::Storage::Plugin::volume_snapshot(@_); - } - - my $path = $class->filesystem_path($scfg, $volname); - my $snap_path = $class->filesystem_path($scfg, $volname, $snap); - - if ($format eq 'raw') { - $path = raw_file_to_subvol($path); - $snap_path = raw_file_to_subvol($snap_path); - } - - my $snapshot_dir = $class->get_subdir($scfg, 'images') . "/$vmid"; - mkpath $snapshot_dir; - - $class->btrfs_cmd(['subvolume', 'snapshot', '-r', '--', $path, $snap_path]); - return undef; -} - -sub volume_rollback_is_possible { - my ($class, $scfg, $storeid, $volname, $snap) = @_; - - return 1; -} - -sub volume_snapshot_rollback { - my ($class, $scfg, $storeid, $volname, $snap) = @_; - - my ($name, $format) = ($class->parse_volname($volname))[1,6]; - - if ($format ne 'subvol' && $format ne 'raw') { - return PVE::Storage::Plugin::volume_snapshot_rollback(@_); - } - - my $path = $class->filesystem_path($scfg, $volname); - my $snap_path = $class->filesystem_path($scfg, $volname, $snap); - - if ($format eq 'raw') { - $path = raw_file_to_subvol($path); - $snap_path = raw_file_to_subvol($snap_path); - } - - # Simple version would be: - # rename old to temp - # create new - # on error rename temp back - # But for atomicity in case the rename after create-failure *also* fails, we create the new - # subvol first, then use RENAME_EXCHANGE, - my $tmp_path = "$path.tmp.$$"; - $class->btrfs_cmd(['subvolume', 'snapshot', '--', $snap_path, $tmp_path]); - # The paths are absolute, so pass -1 as file descriptors. - my $ok = PVE::Tools::renameat2(-1, $tmp_path, -1, $path, &PVE::Tools::RENAME_EXCHANGE); - - eval { $class->btrfs_cmd(['subvolume', 'delete', '--', $tmp_path]) }; - warn "failed to remove '$tmp_path' subvolume: $@" if $@; - - if (!$ok) { - die "failed to rotate '$tmp_path' into place at '$path' - $!\n"; - } - - return undef; -} - -sub volume_snapshot_delete { - my ($class, $scfg, $storeid, $volname, $snap, $running) = @_; - - my ($name, $vmid, $format) = ($class->parse_volname($volname))[1,2,6]; - - if ($format ne 'subvol' && $format ne 'raw') { - return PVE::Storage::Plugin::volume_snapshot_delete(@_); - } - - my $path = $class->filesystem_path($scfg, $volname, $snap); - - if ($format eq 'raw') { - $path = raw_file_to_subvol($path); - } - - $class->btrfs_cmd(['subvolume', 'delete', '--', $path]); - - return undef; -} - -sub volume_has_feature { - my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_; - - my $features = { - snapshot => { - current => { qcow2 => 1, raw => 1, subvol => 1 }, - snap => { qcow2 => 1, raw => 1, subvol => 1 } - }, - clone => { - base => { qcow2 => 1, raw => 1, subvol => 1, vmdk => 1 }, - current => { raw => 1 }, - snap => { raw => 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 } }, - }; - - my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = - $class->parse_volname($volname); - - my $key = undef; - if ($snapname) { - $key = 'snap'; - } else { - $key = $isBase ? 'base' : 'current'; - } - - return 1 if defined($features->{$feature}->{$key}->{$format}); - - return undef; -} - -sub list_images { - my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_; - my $imagedir = $class->get_subdir($scfg, 'images'); - - my $res = []; - - # Copied from Plugin.pm, with file_size_info calls adapted: - foreach my $fn (<$imagedir/[0-9][0-9]*/*>) { - # different to in Plugin.pm the regex below also excludes '@' as valid file name - next if $fn !~ m@^(/.+/(\d+)/([^/\@.]+(?:\.(qcow2|vmdk|subvol))?))$@; - $fn = $1; # untaint - - my $owner = $2; - my $name = $3; - my $ext = $4; - - next if !$vollist && defined($vmid) && ($owner ne $vmid); - - my $volid = "$storeid:$owner/$name"; - my ($size, $format, $used, $parent, $ctime); - - if (!$ext) { # raw - $volid .= '.raw'; - ($size, $format, $used, $parent, $ctime) = PVE::Storage::Plugin::file_size_info("$fn/disk.raw"); - } elsif ($ext eq 'subvol') { - ($used, $size) = (0, 0); - #($used, $size) = btrfs_subvol_quota($class, $fn); - $format = 'subvol'; - } else { - ($size, $format, $used, $parent, $ctime) = PVE::Storage::Plugin::file_size_info($fn); - } - next if !($format && defined($size)); - - if ($vollist) { - next if ! grep { $_ eq $volid } @$vollist; - } - - my $info = { - volid => $volid, format => $format, - size => $size, vmid => $owner, used => $used, parent => $parent, - }; - - $info->{ctime} = $ctime if $ctime; - - push @$res, $info; - } - - return $res; -} - -sub volume_export_formats { - my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_; - - # 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 { - 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 { - 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