1 package PVE
::Storage
::BTRFSPlugin
;
6 use base
qw(PVE::Storage::Plugin);
8 use Fcntl
qw(S_ISDIR O_WRONLY O_CREAT O_EXCL);
9 use File
::Basename
qw(basename dirname);
10 use File
::Path
qw(mkpath);
14 use PVE
::Tools
qw(run_command dir_glob_foreach);
16 use PVE
::Storage
::DirPlugin
;
19 BTRFS_FIRST_FREE_OBJECTID
=> 256,
20 FS_NOCOW_FL
=> 0x00800000,
21 FS_IOC_GETFLAGS
=> 0x40086602,
22 FS_IOC_SETFLAGS
=> 0x80086601,
23 BTRFS_MAGIC
=> 0x9123683e,
26 # Configuration (similar to DirPlugin)
44 { images
=> 1, rootdir
=> 1 },
46 format
=> [ { raw
=> 1, subvol
=> 1 }, 'raw', ],
53 description
=> "Set the NOCOW flag on files."
54 . " Disables data checksumming and causes data errors to be unrecoverable from"
55 . " while allowing direct I/O. Only use this if data does not need to be any more"
56 . " safe than on a single ext4 formatted disk with no underlying raid system.",
65 path
=> { fixed
=> 1 },
66 nodes
=> { optional
=> 1 },
67 shared
=> { optional
=> 1 },
68 disable
=> { optional
=> 1 },
69 maxfiles
=> { optional
=> 1 },
70 'prune-backups'=> { optional
=> 1 },
71 content
=> { optional
=> 1 },
72 format
=> { optional
=> 1 },
73 is_mountpoint
=> { optional
=> 1 },
74 nocow
=> { optional
=> 1 },
75 mkdir => { optional
=> 1 },
76 preallocation
=> { optional
=> 1 },
77 # TODO: The new variant of mkdir with `populate` vs `create`...
81 # Storage implementation
83 # We use the same volume names are directory plugins, but map *raw* disk image file names into a
86 # `vm-VMID-disk-ID.raw`
87 # -> `images/VMID/vm-VMID-disk-ID/disk.raw`
88 # where the `vm-VMID-disk-ID/` subdirectory is a btrfs subvolume
90 # Reuse `DirPlugin`'s `check_config`. This simply checks for invalid paths.
92 my ($self, $sectionId, $config, $create, $skipSchemaCheck) = @_;
93 return PVE
::Storage
::DirPlugin
::check_config
($self, $sectionId, $config, $create, $skipSchemaCheck);
96 my sub getfsmagic
($) {
98 # The field type sizes in `struct statfs` are defined in a rather annoying way, and we only
99 # need the first field, which is a `long` for our supported platforms.
100 # Should be moved to pve-rs, so this can be the problem of the `libc` crate ;-)
101 # Just round up and extract what we need:
102 my $buf = pack('x160');
103 if (0 != syscall(&PVE
::Syscall
::SYS_statfs
, $path, $buf)) {
104 die "statfs on '$path' failed - $!\n";
107 return unpack('L!', $buf);
110 my sub assert_btrfs
($) {
112 die "'$path' is not a btrfs file system\n"
113 if getfsmagic
($path) != BTRFS_MAGIC
;
116 sub activate_storage
{
117 my ($class, $storeid, $scfg, $cache) = @_;
119 my $path = $scfg->{path
};
120 if (!defined($scfg->{mkdir}) || $scfg->{mkdir}) {
124 my $mp = PVE
::Storage
::DirPlugin
::parse_is_mountpoint
($scfg);
125 if (defined($mp) && !PVE
::Storage
::DirPlugin
::path_is_mounted
($mp, $cache->{mountdata
})) {
126 die "unable to activate storage '$storeid' - directory is expected to be a mount point but"
127 ." is not mounted: '$mp'\n";
130 assert_btrfs
($path); # only assert this stuff now, ensures $path is there and better UX
132 $class->SUPER::activate_storage
($storeid, $scfg, $cache);
136 my ($class, $storeid, $scfg, $cache) = @_;
137 return PVE
::Storage
::DirPlugin
::status
($class, $storeid, $scfg, $cache);
140 # TODO: sub get_volume_notes {}
142 # TODO: sub update_volume_notes {}
144 # croak would not include the caller from within this module
147 my (undef, $f, $n) = caller(1);
148 die "$msg at $f: $n\n";
151 # Given a name (eg. `vm-VMID-disk-ID.raw`), take the part up to the format suffix as the name of
152 # the subdirectory (subvolume).
153 sub raw_name_to_dir
($) {
156 # For the subvolume directory Strip the `.<format>` suffix:
157 if ($raw =~ /^(.*)\.raw$/) {
161 __error
"internal error: bad disk name: $raw";
164 sub raw_file_to_subvol
($) {
167 if ($file =~ m
|^(.*)/disk\
.raw
$|) {
171 __error
"internal error: bad raw path: $file";
174 sub filesystem_path
{
175 my ($class, $scfg, $volname, $snapname) = @_;
177 my ($vtype, $name, $vmid, undef, undef, $isBase, $format) =
178 $class->parse_volname($volname);
180 my $path = $class->get_subdir($scfg, $vtype);
182 $path .= "/$vmid" if $vtype eq 'images';
184 if (defined($format) && $format eq 'raw') {
185 my $dir = raw_name_to_dir
($name);
187 $dir .= "\@$snapname";
189 $path .= "/$dir/disk.raw";
190 } elsif (defined($format) && $format eq 'subvol') {
193 $path .= "\@$snapname";
199 return wantarray ?
($path, $vmid, $vtype) : $path;
203 my ($class, $cmd, $outfunc) = @_;
207 if (defined($outfunc)) {
209 my $part = &$outfunc(@_);
210 $msg .= $part if defined($part);
213 $func = sub { $msg .= "$_[0]\n" };
215 run_command
(['btrfs', '-q', @$cmd], errmsg
=> 'btrfs error', outfunc
=> $func);
220 sub btrfs_get_subvol_id
{
221 my ($class, $path) = @_;
222 my $info = $class->btrfs_cmd(['subvolume', 'show', '--', $path]);
223 if ($info !~ /^\s*(?:Object|Subvolume) ID:\s*(\d+)$/m) {
224 die "failed to get btrfs subvolume ID from: $info\n";
229 my sub chattr
: prototype($$$) {
230 my ($fh, $mask, $xor) = @_;
232 my $flags = pack('L!', 0);
233 ioctl($fh, FS_IOC_GETFLAGS
, $flags) or die "FS_IOC_GETFLAGS failed - $!\n";
234 $flags = pack('L!', (unpack('L!', $flags) & $mask) ^ $xor);
235 ioctl($fh, FS_IOC_SETFLAGS
, $flags) or die "FS_IOC_SETFLAGS failed - $!\n";
240 my ($class, $storeid, $scfg, $volname) = @_;
242 my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) =
243 $class->parse_volname($volname);
246 $newname =~ s/^vm-/base-/;
248 # If we're not working with a 'raw' file, which is the only thing that's "different" for btrfs,
249 # or a subvolume, we forward to the DirPlugin
250 if ($format ne 'raw' && $format ne 'subvol') {
251 return PVE
::Storage
::DirPlugin
::create_base
(@_);
254 my $path = $class->filesystem_path($scfg, $volname);
255 my $newvolname = $basename ?
"$basevmid/$basename/$vmid/$newname" : "$vmid/$newname";
256 my $newpath = $class->filesystem_path($scfg, $newvolname);
259 my $newsubvol = $newpath;
260 if ($format eq 'raw') {
261 $subvol = raw_file_to_subvol
($subvol);
262 $newsubvol = raw_file_to_subvol
($newsubvol);
265 rename($subvol, $newsubvol)
266 || die "rename '$subvol' to '$newsubvol' failed - $!\n";
267 eval { $class->btrfs_cmd(['property', 'set', $newsubvol, 'ro', 'true']) };
274 my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
276 my ($vtype, $basename, $basevmid, undef, undef, $isBase, $format) =
277 $class->parse_volname($volname);
279 # If we're not working with a 'raw' file, which is the only thing that's "different" for btrfs,
280 # or a subvolume, we forward to the DirPlugin
281 if ($format ne 'raw' && $format ne 'subvol') {
282 return PVE
::Storage
::DirPlugin
::clone_image
(@_);
285 my $imagedir = $class->get_subdir($scfg, 'images');
286 $imagedir .= "/$vmid";
289 my $path = $class->filesystem_path($scfg, $volname);
290 my $newname = $class->find_free_diskname($storeid, $scfg, $vmid, $format, 1);
292 # For btrfs subvolumes we don't actually need the "link":
293 #my $newvolname = "$basevmid/$basename/$vmid/$newname";
294 my $newvolname = "$vmid/$newname";
295 my $newpath = $class->filesystem_path($scfg, $newvolname);
298 my $newsubvol = $newpath;
299 if ($format eq 'raw') {
300 $subvol = raw_file_to_subvol
($subvol);
301 $newsubvol = raw_file_to_subvol
($newsubvol);
304 $class->btrfs_cmd(['subvolume', 'snapshot', '--', $subvol, $newsubvol]);
310 my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
312 if ($fmt ne 'raw' && $fmt ne 'subvol') {
313 return $class->SUPER::alloc_image
($storeid, $scfg, $vmid, $fmt, $name, $size);
318 my $imagedir = $class->get_subdir($scfg, 'images') . "/$vmid";
322 $name = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt, 1) if !$name;
324 my (undef, $tmpfmt) = PVE
::Storage
::Plugin
::parse_name_dir
($name);
326 die "illegal name '$name' - wrong extension for format ('$tmpfmt != '$fmt')\n"
329 # End copy from Plugin.pm
331 my $subvol = "$imagedir/$name";
332 # .raw is not part of the directory name
333 $subvol =~ s/\.raw$//;
335 die "disk image '$subvol' already exists\n" if -e
$subvol;
339 $path = "$subvol/disk.raw";
342 if ($fmt eq 'subvol' && !!$size) {
343 # NOTE: `btrfs send/recv` actually drops quota information so supporting subvolumes with
344 # quotas doesn't play nice with send/recv.
345 die "btrfs quotas are currently not supported, use an unsized subvolume or a raw file\n";
348 $class->btrfs_cmd(['subvolume', 'create', '--', $subvol]);
351 if ($fmt eq 'subvol') {
352 # Nothing to do for now...
354 # This is how we *would* do it:
355 # # Use the subvol's default 0/$id qgroup
357 # # This call should happen at storage creation instead and therefore governed by a
358 # # configuration option!
359 # # $class->btrfs_cmd(['quota', 'enable', $subvol]);
360 # my $id = $class->btrfs_get_subvol_id($subvol);
361 # $class->btrfs_cmd(['qgroup', 'limit', "${size}k", "0/$id", $subvol]);
363 } elsif ($fmt eq 'raw') {
364 sysopen my $fh, $path, O_WRONLY
| O_CREAT
| O_EXCL
365 or die "failed to create raw file '$path' - $!\n";
366 chattr
($fh, ~FS_NOCOW_FL
, FS_NOCOW_FL
) if $scfg->{nocow
};
367 truncate($fh, $size * 1024)
368 or die "failed to set file size for '$path' - $!\n";
371 die "internal format error (format = $fmt)\n";
376 eval { $class->btrfs_cmd(['subvolume', 'delete', '--', $subvol]); };
381 return "$vmid/$name";
384 # Same as btrfsprogs does:
385 my sub path_is_subvolume
: prototype($) {
387 my @stat = stat($path)
388 or die "stat failed on '$path' - $!\n";
389 my ($ino, $mode) = @stat[1, 2];
390 return S_ISDIR
($mode) && $ino == BTRFS_FIRST_FREE_OBJECTID
;
393 my $BTRFS_VOL_REGEX = qr/((?:vm|base|subvol)-\d+-disk-\d+(?:\.subvol)?)(?:\@(\S+))$/;
395 # Calls `$code->($volume, $name, $snapshot)` for each subvol in a directory matching our volume
397 my sub foreach_subvol
: prototype($$) {
398 my ($dir, $code) = @_;
400 dir_glob_foreach
($dir, $BTRFS_VOL_REGEX, sub {
401 my ($volume, $name, $snapshot) = ($1, $2, $3);
402 return if !path_is_subvolume
("$dir/$volume");
403 $code->($volume, $name, $snapshot);
408 my ($class, $storeid, $scfg, $volname, $isBase, $_format) = @_;
410 my (undef, undef, $vmid, undef, undef, undef, $format) =
411 $class->parse_volname($volname);
413 if (!defined($format) || ($format ne 'subvol' && $format ne 'raw')) {
414 return $class->SUPER::free_image
($storeid, $scfg, $volname, $isBase, $_format);
417 my $path = $class->filesystem_path($scfg, $volname);
420 if ($format eq 'raw') {
421 $subvol = raw_file_to_subvol
($path);
424 my $dir = dirname
($subvol);
425 my $basename = basename
($subvol);
427 foreach_subvol
($dir, sub {
428 my ($volume, $name, $snapshot) = @_;
429 return if $name ne $basename;
430 return if !defined $snapshot;
431 push @snapshot_vols, "$dir/$volume";
434 $class->btrfs_cmd(['subvolume', 'delete', '--', @snapshot_vols, $subvol]);
435 # try to cleanup directory to not clutter storage with empty $vmid dirs if
436 # all images from a guest got deleted
442 # Currently not used because quotas clash with send/recv.
443 # my sub btrfs_subvol_quota {
444 # my ($class, $path) = @_;
445 # my $id = '0/' . $class->btrfs_get_subvol_id($path);
446 # my $search = qr/^\Q$id\E\s+(\d)+\s+\d+\s+(\d+)\s*$/;
448 # $class->btrfs_cmd(['qgroup', 'show', '--raw', '-rf', '--', $path], sub {
449 # return if defined($size);
450 # if ($_[0] =~ $search) {
451 # ($used, $size) = ($1, $2);
454 # if (!defined($size)) {
455 # # syslog should include more information:
456 # syslog('err', "failed to get subvolume size for: $path (id $id)");
457 # # UI should only see the last path component:
459 # die "failed to get subvolume size for $path\n";
461 # return wantarray ? ($used, $size) : $size;
464 sub volume_size_info
{
465 my ($class, $scfg, $storeid, $volname, $timeout) = @_;
467 my $path = $class->filesystem_path($scfg, $volname);
469 my $format = ($class->parse_volname($volname))[6];
471 if (defined($format) && $format eq 'subvol') {
472 my $ctime = (stat($path))[10];
473 my ($used, $size) = (0, 0);
474 #my ($used, $size) = btrfs_subvol_quota($class, $path); # uses wantarray
475 return wantarray ?
($size, 'subvol', $used, undef, $ctime) : 1;
478 return PVE
::Storage
::Plugin
::file_size_info
($path, $timeout);
482 my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
484 my $format = ($class->parse_volname($volname))[6];
485 if ($format eq 'subvol') {
486 my $path = $class->filesystem_path($scfg, $volname);
487 my $id = '0/' . $class->btrfs_get_subvol_id($path);
488 $class->btrfs_cmd(['qgroup', 'limit', '--', "${size}k", "0/$id", $path]);
492 return PVE
::Storage
::Plugin
::volume_resize
(@_);
495 sub volume_snapshot
{
496 my ($class, $scfg, $storeid, $volname, $snap) = @_;
498 my ($name, $vmid, $format) = ($class->parse_volname($volname))[1,2,6];
499 if ($format ne 'subvol' && $format ne 'raw') {
500 return PVE
::Storage
::Plugin
::volume_snapshot
(@_);
503 my $path = $class->filesystem_path($scfg, $volname);
504 my $snap_path = $class->filesystem_path($scfg, $volname, $snap);
506 if ($format eq 'raw') {
507 $path = raw_file_to_subvol
($path);
508 $snap_path = raw_file_to_subvol
($snap_path);
511 my $snapshot_dir = $class->get_subdir($scfg, 'images') . "/$vmid";
512 mkpath
$snapshot_dir;
514 $class->btrfs_cmd(['subvolume', 'snapshot', '-r', '--', $path, $snap_path]);
518 sub volume_rollback_is_possible
{
519 my ($class, $scfg, $storeid, $volname, $snap) = @_;
524 sub volume_snapshot_rollback
{
525 my ($class, $scfg, $storeid, $volname, $snap) = @_;
527 my ($name, $format) = ($class->parse_volname($volname))[1,6];
529 if ($format ne 'subvol' && $format ne 'raw') {
530 return PVE
::Storage
::Plugin
::volume_snapshot_rollback
(@_);
533 my $path = $class->filesystem_path($scfg, $volname);
534 my $snap_path = $class->filesystem_path($scfg, $volname, $snap);
536 if ($format eq 'raw') {
537 $path = raw_file_to_subvol
($path);
538 $snap_path = raw_file_to_subvol
($snap_path);
541 # Simple version would be:
544 # on error rename temp back
545 # But for atomicity in case the rename after create-failure *also* fails, we create the new
546 # subvol first, then use RENAME_EXCHANGE,
547 my $tmp_path = "$path.tmp.$$";
548 $class->btrfs_cmd(['subvolume', 'snapshot', '--', $snap_path, $tmp_path]);
549 # The paths are absolute, so pass -1 as file descriptors.
550 my $ok = PVE
::Tools
::renameat2
(-1, $tmp_path, -1, $path, &PVE
::Tools
::RENAME_EXCHANGE
);
552 eval { $class->btrfs_cmd(['subvolume', 'delete', '--', $tmp_path]) };
553 warn "failed to remove '$tmp_path' subvolume: $@" if $@;
556 die "failed to rotate '$tmp_path' into place at '$path' - $!\n";
562 sub volume_snapshot_delete
{
563 my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
565 my ($name, $vmid, $format) = ($class->parse_volname($volname))[1,2,6];
567 if ($format ne 'subvol' && $format ne 'raw') {
568 return PVE
::Storage
::Plugin
::volume_snapshot_delete
(@_);
571 my $path = $class->filesystem_path($scfg, $volname, $snap);
573 if ($format eq 'raw') {
574 $path = raw_file_to_subvol
($path);
577 $class->btrfs_cmd(['subvolume', 'delete', '--', $path]);
582 sub volume_has_feature
{
583 my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
587 current
=> { qcow2
=> 1, raw
=> 1, subvol
=> 1 },
588 snap
=> { qcow2
=> 1, raw
=> 1, subvol
=> 1 }
591 base
=> { qcow2
=> 1, raw
=> 1, subvol
=> 1, vmdk
=> 1 },
592 current
=> { raw
=> 1 },
593 snap
=> { raw
=> 1 },
595 template
=> { current
=> { qcow2
=> 1, raw
=> 1, vmdk
=> 1, subvol
=> 1 } },
597 base
=> { qcow2
=> 1, raw
=> 1, subvol
=> 1, vmdk
=> 1 },
598 current
=> { qcow2
=> 1, raw
=> 1, subvol
=> 1, vmdk
=> 1 },
599 snap
=> { qcow2
=> 1, raw
=> 1, subvol
=> 1 },
601 sparseinit
=> { base
=> {qcow2
=> 1, raw
=> 1, vmdk
=> 1 },
602 current
=> {qcow2
=> 1, raw
=> 1, vmdk
=> 1 } },
605 my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) =
606 $class->parse_volname($volname);
612 $key = $isBase ?
'base' : 'current';
615 return 1 if defined($features->{$feature}->{$key}->{$format});
621 my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
622 my $imagedir = $class->get_subdir($scfg, 'images');
626 # Copied from Plugin.pm, with file_size_info calls adapted:
627 foreach my $fn (<$imagedir/[0-9][0-9]*/*>) {
628 # different to in Plugin.pm the regex below also excludes '@' as valid file name
629 next if $fn !~ m
@^(/.+/(\d
+)/([^/\
@.]+(?
:\
.(qcow2
|vmdk
|subvol
))?
))$@;
636 next if !$vollist && defined($vmid) && ($owner ne $vmid);
638 my $volid = "$storeid:$owner/$name";
639 my ($size, $format, $used, $parent, $ctime);
643 ($size, $format, $used, $parent, $ctime) = PVE
::Storage
::Plugin
::file_size_info
("$fn/disk.raw");
644 } elsif ($ext eq 'subvol') {
645 ($used, $size) = (0, 0);
646 #($used, $size) = btrfs_subvol_quota($class, $fn);
649 ($size, $format, $used, $parent, $ctime) = PVE
::Storage
::Plugin
::file_size_info
($fn);
651 next if !($format && defined($size));
654 next if ! grep { $_ eq $volid } @$vollist;
658 volid
=> $volid, format
=> $format,
659 size
=> $size, vmid
=> $owner, used
=> $used, parent
=> $parent,
662 $info->{ctime
} = $ctime if $ctime;
670 sub volume_export_formats
{
671 my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
673 # We can do whatever `DirPlugin` can do.
674 my @result = PVE
::Storage
::Plugin
::volume_export_formats
(@_);
676 # `btrfs send` only works on snapshots:
677 return @result if !defined $snapshot;
679 # Incremental stream with snapshots is only supported if the snapshots are listed (new api):
680 return @result if defined($base_snapshot) && $with_snapshots && ref($with_snapshots) ne 'ARRAY';
682 # Otherwise we do also support `with_snapshots`.
684 # Finally, `btrfs send` only works on formats where we actually use btrfs subvolumes:
685 my $format = ($class->parse_volname($volname))[6];
686 return @result if $format ne 'raw' && $format ne 'subvol';
688 return ('btrfs', @result);
691 sub volume_import_formats
{
692 my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
694 # Same as export-formats, beware the parameter order:
695 return volume_export_formats
(
719 if ($format ne 'btrfs') {
720 return PVE
::Storage
::Plugin
::volume_export
(@_);
723 die "format 'btrfs' only works on snapshots\n"
724 if !defined $snapshot;
726 die "'btrfs' format in incremental mode requires snapshots to be listed explicitly\n"
727 if defined($base_snapshot) && $with_snapshots && ref($with_snapshots) ne 'ARRAY';
729 my $volume_format = ($class->parse_volname($volname))[6];
731 die "btrfs-sending volumes of type $volume_format ('$volname') is not supported\n"
732 if $volume_format ne 'raw' && $volume_format ne 'subvol';
734 my $path = $class->path($scfg, $volname, $storeid);
736 if ($volume_format eq 'raw') {
737 $path = raw_file_to_subvol
($path);
740 my $cmd = ['btrfs', '-q', 'send', '-e'];
741 if ($base_snapshot) {
742 my $base = $class->path($scfg, $volname, $storeid, $base_snapshot);
743 if ($volume_format eq 'raw') {
744 $base = raw_file_to_subvol
($base);
746 push @$cmd, '-p', $base;
749 if (ref($with_snapshots) eq 'ARRAY') {
750 push @$cmd, (map { "$path\@$_" } ($with_snapshots // [])->@*), $path;
752 dir_glob_foreach
(dirname
($path), $BTRFS_VOL_REGEX, sub {
753 push @$cmd, "$path\@$_[2]" if !(defined($snapshot) && $_[2] eq $snapshot);
756 $path .= "\@$snapshot" if defined($snapshot);
759 run_command
($cmd, output
=> '>&'.fileno($fh));
777 if ($format ne 'btrfs') {
778 return PVE
::Storage
::Plugin
::volume_import
(@_);
781 die "format 'btrfs' only works on snapshots\n"
782 if !defined $snapshot;
784 my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $volume_format) =
785 $class->parse_volname($volname);
787 die "btrfs-receiving volumes of type $volume_format ('$volname') is not supported\n"
788 if $volume_format ne 'raw' && $volume_format ne 'subvol';
790 if (defined($base_snapshot)) {
791 my $path = $class->path($scfg, $volname, $storeid, $base_snapshot);
792 die "base snapshot '$base_snapshot' not found - no such directory '$path'\n"
793 if !path_is_subvolume
($path);
796 my $destination = $class->filesystem_path($scfg, $volname);
797 if ($volume_format eq 'raw') {
798 $destination = raw_file_to_subvol
($destination);
801 if (!defined($base_snapshot) && -e
$destination) {
802 die "volume $volname already exists\n" if !$allow_rename;
803 $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $volume_format, 1);
806 my $imagedir = $class->get_subdir($scfg, $vtype);
807 $imagedir .= "/$vmid" if $vtype eq 'images';
809 my $tmppath = "$imagedir/recv.$vmid.tmp";
810 mkdir($imagedir); # FIXME: if $scfg->{mkdir};
811 if (!mkdir($tmppath)) {
812 die "temp receive directory already exists at '$tmppath', incomplete concurrent import?\n"
814 die "failed to create temporary receive directory at '$tmppath' - $!\n";
817 my $dh = IO
::Dir-
>new($tmppath)
818 or die "failed to open temporary receive directory '$tmppath' - $!\n";
820 run_command
(['btrfs', '-q', 'receive', '-e', '--', $tmppath], input
=> '<&'.fileno($fh));
822 # Analyze the received subvolumes;
823 my ($diskname, $found_snapshot, @snapshots);
825 while (defined(my $entry = $dh->read)) {
826 next if $entry eq '.' || $entry eq '..';
827 next if $entry !~ /^$BTRFS_VOL_REGEX$/;
828 my ($cur_diskname, $cur_snapshot) = ($1, $2);
830 die "send stream included a non-snapshot subvolume\n"
831 if !defined($cur_snapshot);
833 if (!defined($diskname)) {
834 $diskname = $cur_diskname;
836 die "multiple disks contained in stream ('$diskname' vs '$cur_diskname')\n"
837 if $diskname ne $cur_diskname;
840 if ($cur_snapshot eq $snapshot) {
843 push @snapshots, $cur_snapshot;
847 die "send stream did not contain the expected current snapshot '$snapshot'\n"
850 # Rotate the disk into place, first the current state:
851 # Note that read-only subvolumes cannot be moved into different directories, but for the
852 # "current" state we also want a writable copy, so start with that:
853 $class->btrfs_cmd(['property', 'set', "$tmppath/$diskname\@$snapshot", 'ro', 'false']);
854 PVE
::Tools
::renameat2
(
856 "$tmppath/$diskname\@$snapshot",
859 &PVE
::Tools
::RENAME_NOREPLACE
,
860 ) or die "failed to move received snapshot '$tmppath/$diskname\@$snapshot'"
861 . " into place at '$destination' - $!\n";
863 # Now recreate the actual snapshot:
870 "$destination\@$snapshot",
873 # Now go through the remaining snapshots (if any)
874 foreach my $snap (@snapshots) {
875 $class->btrfs_cmd(['property', 'set', "$tmppath/$diskname\@$snap", 'ro', 'false']);
876 PVE
::Tools
::renameat2
(
878 "$tmppath/$diskname\@$snap",
880 "$destination\@$snap",
881 &PVE
::Tools
::RENAME_NOREPLACE
,
882 ) or die "failed to move received snapshot '$tmppath/$diskname\@$snap'"
883 . " into place at '$destination\@$snap' - $!\n";
884 eval { $class->btrfs_cmd(['property', 'set', "$destination\@$snap", 'ro', 'true']) };
885 warn "failed to make $destination\@$snap read-only - $!\n" if $@;
891 # Cleanup all the received snapshots we did not move into place, so we can remove the temp
895 while (defined(my $entry = $dh->read)) {
896 next if $entry eq '.' || $entry eq '..';
897 eval { $class->btrfs_cmd(['subvolume', 'delete', '--', "$tmppath/$entry"]) };
900 $dh->close; undef $dh;
902 if (!rmdir($tmppath)) {
903 warn "failed to remove temporary directory '$tmppath' - $!\n"
908 # clean up if the directory ended up being empty after an error
913 return "$storeid:$volname";