]> git.proxmox.com Git - pve-storage.git/blob - PVE/Storage/BTRFSPlugin.pm
btrfs: add 'btrfs' import/export format
[pve-storage.git] / PVE / Storage / BTRFSPlugin.pm
1 package PVE::Storage::BTRFSPlugin;
2
3 use strict;
4 use warnings;
5
6 use base qw(PVE::Storage::Plugin);
7
8 use Fcntl qw(S_ISDIR O_WRONLY O_CREAT O_EXCL);
9 use File::Basename qw(dirname);
10 use File::Path qw(mkpath);
11 use IO::Dir;
12 use POSIX qw(EEXIST);
13
14 use PVE::Tools qw(run_command dir_glob_foreach);
15
16 use PVE::Storage::DirPlugin;
17
18 use constant {
19 BTRFS_FIRST_FREE_OBJECTID => 256,
20 FS_NOCOW_FL => 0x00800000,
21 FS_IOC_GETFLAGS => 0x40086602,
22 FS_IOC_SETFLAGS => 0x80086601,
23 };
24
25 # Configuration (similar to DirPlugin)
26
27 sub type {
28 return 'btrfs';
29 }
30
31 sub plugindata {
32 return {
33 content => [
34 {
35 images => 1,
36 rootdir => 1,
37 vztmpl => 1,
38 iso => 1,
39 backup => 1,
40 snippets => 1,
41 none => 1,
42 },
43 { images => 1, rootdir => 1 },
44 ],
45 format => [ { raw => 1, qcow2 => 1, vmdk => 1, subvol => 1 }, 'raw', ],
46 };
47 }
48
49 sub options {
50 return {
51 path => { fixed => 1 },
52 nodes => { optional => 1 },
53 shared => { optional => 1 },
54 disable => { optional => 1 },
55 maxfiles => { optional => 1 },
56 content => { optional => 1 },
57 format => { optional => 1 },
58 is_mountpoint => { optional => 1 },
59 # TODO: The new variant of mkdir with `populate` vs `create`...
60 };
61 }
62
63 # Storage implementation
64 #
65 # We use the same volume names are directory plugins, but map *raw* disk image file names into a
66 # subdirectory.
67 #
68 # `vm-VMID-disk-ID.raw`
69 # -> `images/VMID/vm-VMID-disk-ID/disk.raw`
70 # where the `vm-VMID-disk-ID/` subdirectory is a btrfs subvolume
71
72 # Reuse `DirPlugin`'s `check_config`. This simply checks for invalid paths.
73 sub check_config {
74 my ($self, $sectionId, $config, $create, $skipSchemaCheck) = @_;
75 return PVE::Storage::DirPlugin::check_config($self, $sectionId, $config, $create, $skipSchemaCheck);
76 }
77
78 sub activate_storage {
79 my ($class, $storeid, $scfg, $cache) = @_;
80 return PVE::Storage::DirPlugin::activate_storage($class, $storeid, $scfg, $cache);
81 }
82
83 sub status {
84 my ($class, $storeid, $scfg, $cache) = @_;
85 return PVE::Storage::DirPlugin::status($class, $storeid, $scfg, $cache);
86 }
87
88 # TODO: sub get_volume_notes {}
89
90 # TODO: sub update_volume_notes {}
91
92 # croak would not include the caller from within this module
93 sub __error {
94 my ($msg) = @_;
95 my (undef, $f, $n) = caller(1);
96 die "$msg at $f: $n\n";
97 }
98
99 # Given a name (eg. `vm-VMID-disk-ID.raw`), take the part up to the format suffix as the name of
100 # the subdirectory (subvolume).
101 sub raw_name_to_dir($) {
102 my ($raw) = @_;
103
104 # For the subvolume directory Strip the `.<format>` suffix:
105 if ($raw =~ /^(.*)\.raw$/) {
106 return $1;
107 }
108
109 __error "internal error: bad disk name: $raw";
110 }
111
112 sub raw_file_to_subvol($) {
113 my ($file) = @_;
114
115 if ($file =~ m|^(.*)/disk\.raw$|) {
116 return "$1";
117 }
118
119 __error "internal error: bad raw path: $file";
120 }
121
122 sub filesystem_path {
123 my ($class, $scfg, $volname, $snapname) = @_;
124
125 my ($vtype, $name, $vmid, undef, undef, $isBase, $format) =
126 $class->parse_volname($volname);
127
128 my $path = $class->get_subdir($scfg, $vtype);
129
130 $path .= "/$vmid" if $vtype eq 'images';
131
132 if ($format eq 'raw') {
133 my $dir = raw_name_to_dir($name);
134 if ($snapname) {
135 $dir .= "\@$snapname";
136 }
137 $path .= "/$dir/disk.raw";
138 } elsif ($format eq 'subvol') {
139 $path .= "/$name";
140 if ($snapname) {
141 $path .= "\@$snapname";
142 }
143 } else {
144 $path .= "/$name";
145 }
146
147 return wantarray ? ($path, $vmid, $vtype) : $path;
148 }
149
150 sub btrfs_cmd {
151 my ($class, $cmd, $outfunc) = @_;
152
153 my $msg = '';
154 my $func;
155 if (defined($outfunc)) {
156 $func = sub {
157 my $part = &$outfunc(@_);
158 $msg .= $part if defined($part);
159 };
160 } else {
161 $func = sub { $msg .= "$_[0]\n" };
162 }
163 run_command(['btrfs', '-q', @$cmd], errmsg => 'btrfs error', outfunc => $func);
164
165 return $msg;
166 }
167
168 sub btrfs_get_subvol_id {
169 my ($class, $path) = @_;
170 my $info = $class->btrfs_cmd(['subvolume', 'show', '--', $path]);
171 if ($info !~ /^\s*(?:Object|Subvolume) ID:\s*(\d+)$/m) {
172 die "failed to get btrfs subvolume ID from: $info\n";
173 }
174 return $1;
175 }
176
177 my sub chattr : prototype($$$) {
178 my ($fh, $mask, $xor) = @_;
179
180 my $flags = pack('L!', 0);
181 ioctl($fh, FS_IOC_GETFLAGS, $flags) or die "FS_IOC_GETFLAGS failed - $!\n";
182 $flags = pack('L!', (unpack('L!', $flags) & $mask) ^ $xor);
183 ioctl($fh, FS_IOC_SETFLAGS, $flags) or die "FS_IOC_SETFLAGS failed - $!\n";
184 return 1;
185 }
186
187 sub create_base {
188 my ($class, $storeid, $scfg, $volname) = @_;
189
190 my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) =
191 $class->parse_volname($volname);
192
193 my $newname = $name;
194 $newname =~ s/^vm-/base-/;
195
196 # If we're not working with a 'raw' file, which is the only thing that's "different" for btrfs,
197 # or a subvolume, we forward to the DirPlugin
198 if ($format ne 'raw' && $format ne 'subvol') {
199 return PVE::Storage::DirPlugin::create_base(@_);
200 }
201
202 my $path = $class->filesystem_path($scfg, $volname);
203 my $newvolname = $basename ? "$basevmid/$basename/$vmid/$newname" : "$vmid/$newname";
204 my $newpath = $class->filesystem_path($scfg, $newvolname);
205
206 my $subvol = $path;
207 my $newsubvol = $newpath;
208 if ($format eq 'raw') {
209 $subvol = raw_file_to_subvol($subvol);
210 $newsubvol = raw_file_to_subvol($newsubvol);
211 }
212
213 rename($subvol, $newsubvol)
214 || die "rename '$subvol' to '$newsubvol' failed - $!\n";
215 eval { $class->btrfs_cmd(['property', 'set', $newsubvol, 'ro', 'true']) };
216 warn $@ if $@;
217
218 return $newvolname;
219 }
220
221 sub clone_image {
222 my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
223
224 my ($vtype, $basename, $basevmid, undef, undef, $isBase, $format) =
225 $class->parse_volname($volname);
226
227 # If we're not working with a 'raw' file, which is the only thing that's "different" for btrfs,
228 # or a subvolume, we forward to the DirPlugin
229 if ($format ne 'raw' && $format ne 'subvol') {
230 return PVE::Storage::DirPlugin::clone_image(@_);
231 }
232
233 my $imagedir = $class->get_subdir($scfg, 'images');
234 $imagedir .= "/$vmid";
235 mkpath $imagedir;
236
237 my $path = $class->filesystem_path($scfg, $volname);
238 my $newname = $class->find_free_diskname($storeid, $scfg, $vmid, $format, 1);
239
240 # For btrfs subvolumes we don't actually need the "link":
241 #my $newvolname = "$basevmid/$basename/$vmid/$newname";
242 my $newvolname = "$vmid/$newname";
243 my $newpath = $class->filesystem_path($scfg, $newvolname);
244
245 my $subvol = $path;
246 my $newsubvol = $newpath;
247 if ($format eq 'raw') {
248 $subvol = raw_file_to_subvol($subvol);
249 $newsubvol = raw_file_to_subvol($newsubvol);
250 }
251
252 $class->btrfs_cmd(['subvolume', 'snapshot', '--', $subvol, $newsubvol]);
253
254 return $newvolname;
255 }
256
257 sub alloc_image {
258 my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
259
260 if ($fmt ne 'raw' && $fmt ne 'subvol') {
261 return PVE::Storage::DirPlugin::alloc_image(@_);
262 }
263
264 # From Plugin.pm:
265
266 my $imagedir = $class->get_subdir($scfg, 'images') . "/$vmid";
267
268 mkpath $imagedir;
269
270 $name = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt, 1) if !$name;
271
272 my (undef, $tmpfmt) = PVE::Storage::Plugin::parse_name_dir($name);
273
274 die "illegal name '$name' - wrong extension for format ('$tmpfmt != '$fmt')\n"
275 if $tmpfmt ne $fmt;
276
277 # End copy from Plugin.pm
278
279 my $subvol = "$imagedir/$name";
280 # .raw is not part of the directory name
281 $subvol =~ s/\.raw$//;
282
283 die "disk image '$subvol' already exists\n" if -e $subvol;
284
285 my $path;
286 if ($fmt eq 'raw') {
287 $path = "$subvol/disk.raw";
288 }
289
290 if ($fmt eq 'subvol' && !!$size) {
291 # NOTE: `btrfs send/recv` actually drops quota information so supporting subvolumes with
292 # quotas doesn't play nice with send/recv.
293 die "btrfs quotas are currently not supported, use an unsized subvolume or a raw file\n";
294 }
295
296 $class->btrfs_cmd(['subvolume', 'create', '--', $subvol]);
297
298 eval {
299 if ($fmt eq 'subvol') {
300 # Nothing to do for now...
301
302 # This is how we *would* do it:
303 # # Use the subvol's default 0/$id qgroup
304 # eval {
305 # # This call should happen at storage creation instead and therefore governed by a
306 # # configuration option!
307 # # $class->btrfs_cmd(['quota', 'enable', $subvol]);
308 # my $id = $class->btrfs_get_subvol_id($subvol);
309 # $class->btrfs_cmd(['qgroup', 'limit', "${size}k", "0/$id", $subvol]);
310 # };
311 } elsif ($fmt eq 'raw') {
312 sysopen my $fh, $path, O_WRONLY | O_CREAT | O_EXCL
313 or die "failed to create raw file '$path' - $!\n";
314 chattr($fh, ~FS_NOCOW_FL, FS_NOCOW_FL);
315 truncate($fh, $size * 1024)
316 or die "failed to set file size for '$path' - $!\n";
317 close($fh);
318 } else {
319 die "internal format error (format = $fmt)\n";
320 }
321 };
322
323 if (my $err = $@) {
324 eval { $class->btrfs_cmd(['subvolume', 'delete', '--', $subvol]); };
325 warn $@ if $@;
326 die $err;
327 }
328
329 return "$vmid/$name";
330 }
331
332 # Same as btrfsprogs does:
333 my sub path_is_subvolume : prototype($) {
334 my ($path) = @_;
335 my @stat = stat($path)
336 or die "stat failed on '$path' - $!\n";
337 my ($ino, $mode) = @stat[1, 2];
338 return S_ISDIR($mode) && $ino == BTRFS_FIRST_FREE_OBJECTID;
339 }
340
341 my $BTRFS_VOL_REGEX = qr/((?:vm|base|subvol)-\d+-disk-\d+(?:\.subvol)?)(?:\@(\S+))$/;
342
343 # Calls `$code->($volume, $name, $snapshot)` for each subvol in a directory matching our volume
344 # regex.
345 my sub foreach_subvol : prototype($$) {
346 my ($dir, $code) = @_;
347
348 dir_glob_foreach($dir, $BTRFS_VOL_REGEX, sub {
349 my ($volume, $name, $snapshot) = ($1, $2, $3);
350 return if !path_is_subvolume("$dir/$volume");
351 $code->($volume, $name, $snapshot);
352 })
353 }
354
355 sub free_image {
356 my ($class, $storeid, $scfg, $volname, $isBase, $_format) = @_;
357
358 my (undef, undef, $vmid, undef, undef, undef, $format) =
359 $class->parse_volname($volname);
360
361 if ($format ne 'subvol' && $format ne 'raw') {
362 return PVE::Storage::DirPlugin::free_image(@_);
363 }
364
365 my $path = $class->filesystem_path($scfg, $volname);
366
367 my $subvol = $path;
368 if ($format eq 'raw') {
369 $subvol = raw_file_to_subvol($path);
370 }
371
372 my $dir = dirname($subvol);
373 my @snapshot_vols;
374 foreach_subvol($dir, sub {
375 my ($volume, $name, $snapshot) = @_;
376 return if !defined $snapshot;
377 push @snapshot_vols, "$dir/$volume";
378 });
379
380 $class->btrfs_cmd(['subvolume', 'delete', '--', @snapshot_vols, $subvol]);
381 # try to cleanup directory to not clutter storage with empty $vmid dirs if
382 # all images from a guest got deleted
383 rmdir($dir);
384
385 return undef;
386 }
387
388 # Currently not used because quotas clash with send/recv.
389 # my sub btrfs_subvol_quota {
390 # my ($class, $path) = @_;
391 # my $id = '0/' . $class->btrfs_get_subvol_id($path);
392 # my $search = qr/^\Q$id\E\s+(\d)+\s+\d+\s+(\d+)\s*$/;
393 # my ($used, $size);
394 # $class->btrfs_cmd(['qgroup', 'show', '--raw', '-rf', '--', $path], sub {
395 # return if defined($size);
396 # if ($_[0] =~ $search) {
397 # ($used, $size) = ($1, $2);
398 # }
399 # });
400 # if (!defined($size)) {
401 # # syslog should include more information:
402 # syslog('err', "failed to get subvolume size for: $path (id $id)");
403 # # UI should only see the last path component:
404 # $path =~ s|^.*/||;
405 # die "failed to get subvolume size for $path\n";
406 # }
407 # return wantarray ? ($used, $size) : $size;
408 # }
409
410 sub volume_size_info {
411 my ($class, $scfg, $storeid, $volname, $timeout) = @_;
412
413 my $path = $class->filesystem_path($scfg, $volname);
414
415 my $format = ($class->parse_volname($volname))[6];
416
417 if ($format eq 'subvol') {
418 my $ctime = (stat($path))[10];
419 my ($used, $size) = (0, 0);
420 #my ($used, $size) = btrfs_subvol_quota($class, $path); # uses wantarray
421 return wantarray ? ($size, 'subvol', $used, undef, $ctime) : 1;
422 }
423
424 return PVE::Storage::Plugin::file_size_info($path, $timeout);
425 }
426
427 sub volume_resize {
428 my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
429
430 my $format = ($class->parse_volname($volname))[6];
431 if ($format eq 'subvol') {
432 my $path = $class->filesystem_path($scfg, $volname);
433 my $id = '0/' . $class->btrfs_get_subvol_id($path);
434 $class->btrfs_cmd(['qgroup', 'limit', '--', "${size}k", "0/$id", $path]);
435 return undef;
436 }
437
438 return PVE::Storage::Plugin::volume_resize(@_);
439 }
440
441 sub volume_snapshot {
442 my ($class, $scfg, $storeid, $volname, $snap) = @_;
443
444 my ($name, $vmid, $format) = ($class->parse_volname($volname))[1,2,6];
445 if ($format ne 'subvol' && $format ne 'raw') {
446 return PVE::Storage::Plugin::volume_snapshot(@_);
447 }
448
449 my $path = $class->filesystem_path($scfg, $volname);
450 my $snap_path = $class->filesystem_path($scfg, $volname, $snap);
451
452 if ($format eq 'raw') {
453 $path = raw_file_to_subvol($path);
454 $snap_path = raw_file_to_subvol($snap_path);
455 }
456
457 my $snapshot_dir = $class->get_subdir($scfg, 'images') . "/$vmid";
458 mkpath $snapshot_dir;
459
460 $class->btrfs_cmd(['subvolume', 'snapshot', '-r', '--', $path, $snap_path]);
461 return undef;
462 }
463
464 sub volume_rollback_is_possible {
465 my ($class, $scfg, $storeid, $volname, $snap) = @_;
466
467 return 1;
468 }
469
470 sub volume_snapshot_rollback {
471 my ($class, $scfg, $storeid, $volname, $snap) = @_;
472
473 my ($name, $format) = ($class->parse_volname($volname))[1,6];
474
475 if ($format ne 'subvol' && $format ne 'raw') {
476 return PVE::Storage::Plugin::volume_snapshot_rollback(@_);
477 }
478
479 my $path = $class->filesystem_path($scfg, $volname);
480 my $snap_path = $class->filesystem_path($scfg, $volname, $snap);
481
482 if ($format eq 'raw') {
483 $path = raw_file_to_subvol($path);
484 $snap_path = raw_file_to_subvol($snap_path);
485 }
486
487 # Simple version would be:
488 # rename old to temp
489 # create new
490 # on error rename temp back
491 # But for atomicity in case the rename after create-failure *also* fails, we create the new
492 # subvol first, then use RENAME_EXCHANGE,
493 my $tmp_path = "$path.tmp.$$";
494 $class->btrfs_cmd(['subvolume', 'snapshot', '--', $snap_path, $tmp_path]);
495 # The paths are absolute, so pass -1 as file descriptors.
496 my $ok = PVE::Tools::renameat2(-1, $tmp_path, -1, $path, &PVE::Tools::RENAME_EXCHANGE);
497
498 eval { $class->btrfs_cmd(['subvolume', 'delete', '--', $tmp_path]) };
499 warn "failed to remove '$tmp_path' subvolume: $@" if $@;
500
501 if (!$ok) {
502 die "failed to rotate '$tmp_path' into place at '$path' - $!\n";
503 }
504
505 return undef;
506 }
507
508 sub volume_snapshot_delete {
509 my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
510
511 my ($name, $vmid, $format) = ($class->parse_volname($volname))[1,2,6];
512
513 if ($format ne 'subvol' && $format ne 'raw') {
514 return PVE::Storage::Plugin::volume_snapshot_delete(@_);
515 }
516
517 my $path = $class->filesystem_path($scfg, $volname, $snap);
518
519 if ($format eq 'raw') {
520 $path = raw_file_to_subvol($path);
521 }
522
523 $class->btrfs_cmd(['subvolume', 'delete', '--', $path]);
524
525 return undef;
526 }
527
528 sub volume_has_feature {
529 my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
530
531 my $features = {
532 snapshot => {
533 current => { qcow2 => 1, raw => 1, subvol => 1 },
534 snap => { qcow2 => 1, raw => 1, subvol => 1 }
535 },
536 clone => {
537 base => { qcow2 => 1, raw => 1, subvol => 1, vmdk => 1 },
538 current => { raw => 1 },
539 snap => { raw => 1 },
540 },
541 template => { current => { qcow2 => 1, raw => 1, vmdk => 1, subvol => 1 } },
542 copy => {
543 base => { qcow2 => 1, raw => 1, subvol => 1, vmdk => 1 },
544 current => { qcow2 => 1, raw => 1, subvol => 1, vmdk => 1 },
545 snap => { qcow2 => 1, raw => 1, subvol => 1 },
546 },
547 sparseinit => { base => {qcow2 => 1, raw => 1, vmdk => 1 },
548 current => {qcow2 => 1, raw => 1, vmdk => 1 } },
549 };
550
551 my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) =
552 $class->parse_volname($volname);
553
554 my $key = undef;
555 if ($snapname) {
556 $key = 'snap';
557 } else {
558 $key = $isBase ? 'base' : 'current';
559 }
560
561 return 1 if defined($features->{$feature}->{$key}->{$format});
562
563 return undef;
564 }
565
566 sub list_images {
567 my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
568 my $imagedir = $class->get_subdir($scfg, 'images');
569
570 my $res = [];
571
572 # Copied from Plugin.pm, with file_size_info calls adapted:
573 foreach my $fn (<$imagedir/[0-9][0-9]*/*>) {
574 # different to in Plugin.pm the regex below also excludes '@' as valid file name
575 next if $fn !~ m@^(/.+/(\d+)/([^/\@.]+(?:\.(qcow2|vmdk|subvol))?))$@;
576 $fn = $1; # untaint
577
578 my $owner = $2;
579 my $name = $3;
580 my $ext = $4;
581
582 next if !$vollist && defined($vmid) && ($owner ne $vmid);
583
584 my $volid = "$storeid:$owner/$name";
585 my ($size, $format, $used, $parent, $ctime);
586
587 if (!$ext) { # raw
588 $volid .= '.raw';
589 ($size, $format, $used, $parent, $ctime) = PVE::Storage::Plugin::file_size_info("$fn/disk.raw");
590 } elsif ($ext eq 'subvol') {
591 ($used, $size) = (0, 0);
592 #($used, $size) = btrfs_subvol_quota($class, $fn);
593 $format = 'subvol';
594 } else {
595 ($size, $format, $used, $parent, $ctime) = PVE::Storage::Plugin::file_size_info($fn);
596 }
597 next if !($format && defined($size));
598
599 if ($vollist) {
600 next if ! grep { $_ eq $volid } @$vollist;
601 }
602
603 my $info = {
604 volid => $volid, format => $format,
605 size => $size, vmid => $owner, used => $used, parent => $parent,
606 };
607
608 $info->{ctime} = $ctime if $ctime;
609
610 push @$res, $info;
611 }
612
613 return $res;
614 }
615
616 sub volume_export_formats {
617 my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
618
619 # We can do whatever `DirPlugin` can do.
620 my @result = PVE::Storage::Plugin::volume_export_formats(@_);
621
622 # `btrfs send` only works on snapshots:
623 return @result if !defined $snapshot;
624
625 # Incremental stream with snapshots is only supported if the snapshots are listed (new api):
626 return @result if defined($base_snapshot) && $with_snapshots && ref($with_snapshots) ne 'ARRAY';
627
628 # Otherwise we do also support `with_snapshots`.
629
630 # Finally, `btrfs send` only works on formats where we actually use btrfs subvolumes:
631 my $format = ($class->parse_volname($volname))[6];
632 return @result if $format ne 'raw' && $format ne 'subvol';
633
634 return ('btrfs', @result);
635 }
636
637 sub volume_import_formats {
638 my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
639
640 # Same as export-formats, beware the parameter order:
641 return volume_export_formats(
642 $class,
643 $scfg,
644 $storeid,
645 $volname,
646 $snapshot,
647 $base_snapshot,
648 $with_snapshots,
649 );
650 }
651
652 sub volume_export {
653 my (
654 $class,
655 $scfg,
656 $storeid,
657 $fh,
658 $volname,
659 $format,
660 $snapshot,
661 $base_snapshot,
662 $with_snapshots,
663 ) = @_;
664
665 if ($format ne 'btrfs') {
666 return PVE::Storage::Plugin::volume_export(@_);
667 }
668
669 die "format 'btrfs' only works on snapshots\n"
670 if !defined $snapshot;
671
672 die "'btrfs' format in incremental mode requires snapshots to be listed explicitly\n"
673 if defined($base_snapshot) && $with_snapshots && ref($with_snapshots) ne 'ARRAY';
674
675 my $volume_format = ($class->parse_volname($volname))[6];
676
677 die "btrfs-sending volumes of type $volume_format ('$volname') is not supported\n"
678 if $volume_format ne 'raw' && $volume_format ne 'subvol';
679
680 my $path = $class->path($scfg, $volname, $storeid);
681
682 if ($volume_format eq 'raw') {
683 $path = raw_file_to_subvol($path);
684 }
685
686 my $cmd = ['btrfs', '-q', 'send', '-e'];
687 if ($base_snapshot) {
688 my $base = $class->path($scfg, $volname, $storeid, $base_snapshot);
689 if ($volume_format eq 'raw') {
690 $base = raw_file_to_subvol($base);
691 }
692 push @$cmd, '-p', $base;
693 }
694 push @$cmd, '--';
695 if (ref($with_snapshots) eq 'ARRAY') {
696 push @$cmd, (map { "$path\@$_" } ($with_snapshots // [])->@*), $path;
697 } else {
698 dir_glob_foreach(dirname($path), $BTRFS_VOL_REGEX, sub {
699 push @$cmd, "$path\@$_[2]" if !(defined($snapshot) && $_[2] eq $snapshot);
700 });
701 }
702 $path .= "\@$snapshot" if defined($snapshot);
703 push @$cmd, $path;
704
705 run_command($cmd, output => '>&'.fileno($fh));
706 return;
707 }
708
709 sub volume_import {
710 my (
711 $class,
712 $scfg,
713 $storeid,
714 $fh,
715 $volname,
716 $format,
717 $snapshot,
718 $base_snapshot,
719 $with_snapshots,
720 $allow_rename,
721 ) = @_;
722
723 if ($format ne 'btrfs') {
724 return PVE::Storage::Plugin::volume_import(@_);
725 }
726
727 die "format 'btrfs' only works on snapshots\n"
728 if !defined $snapshot;
729
730 my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $volume_format) =
731 $class->parse_volname($volname);
732
733 die "btrfs-receiving volumes of type $volume_format ('$volname') is not supported\n"
734 if $volume_format ne 'raw' && $volume_format ne 'subvol';
735
736 if (defined($base_snapshot)) {
737 my $path = $class->path($scfg, $volname, $storeid, $base_snapshot);
738 die "base snapshot '$base_snapshot' not found - no such directory '$path'\n"
739 if !path_is_subvolume($path);
740 }
741
742 my $destination = $class->filesystem_path($scfg, $volname);
743 if ($volume_format eq 'raw') {
744 $destination = raw_file_to_subvol($destination);
745 }
746
747 if (!defined($base_snapshot) && -e $destination) {
748 die "volume $volname already exists\n" if !$allow_rename;
749 $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $volume_format, 1);
750 }
751
752 my $imagedir = $class->get_subdir($scfg, $vtype);
753 $imagedir .= "/$vmid" if $vtype eq 'images';
754
755 my $tmppath = "$imagedir/recv.$vmid.tmp";
756 mkdir($imagedir); # FIXME: if $scfg->{mkdir};
757 if (!mkdir($tmppath)) {
758 die "temp receive directory already exists at '$tmppath', incomplete concurrent import?\n"
759 if $! == EEXIST;
760 die "failed to create temporary receive directory at '$tmppath' - $!\n";
761 }
762
763 my $dh = IO::Dir->new($tmppath)
764 or die "failed to open temporary receive directory '$tmppath' - $!\n";
765 eval {
766 run_command(['btrfs', '-q', 'receive', '-e', '--', $tmppath], input => '<&'.fileno($fh));
767
768 # Analyze the received subvolumes;
769 my ($diskname, $found_snapshot, @snapshots);
770 $dh->rewind;
771 while (defined(my $entry = $dh->read)) {
772 next if $entry eq '.' || $entry eq '..';
773 next if $entry !~ /^$BTRFS_VOL_REGEX$/;
774 my ($cur_diskname, $cur_snapshot) = ($1, $2);
775
776 die "send stream included a non-snapshot subvolume\n"
777 if !defined($cur_snapshot);
778
779 if (!defined($diskname)) {
780 $diskname = $cur_diskname;
781 } else {
782 die "multiple disks contained in stream ('$diskname' vs '$cur_diskname')\n"
783 if $diskname ne $cur_diskname;
784 }
785
786 if ($cur_snapshot eq $snapshot) {
787 $found_snapshot = 1;
788 } else {
789 push @snapshots, $cur_snapshot;
790 }
791 }
792
793 die "send stream did not contain the expected current snapshot '$snapshot'\n"
794 if !$found_snapshot;
795
796 # Rotate the disk into place, first the current state:
797 # Note that read-only subvolumes cannot be moved into different directories, but for the
798 # "current" state we also want a writable copy, so start with that:
799 $class->btrfs_cmd(['property', 'set', "$tmppath/$diskname\@$snapshot", 'ro', 'false']);
800 PVE::Tools::renameat2(
801 -1,
802 "$tmppath/$diskname\@$snapshot",
803 -1,
804 $destination,
805 &PVE::Tools::RENAME_NOREPLACE,
806 ) or die "failed to move received snapshot '$tmppath/$diskname\@$snapshot'"
807 . " into place at '$destination' - $!\n";
808
809 # Now recreate the actual snapshot:
810 $class->btrfs_cmd([
811 'subvolume',
812 'snapshot',
813 '-r',
814 '--',
815 $destination,
816 "$destination\@$snapshot",
817 ]);
818
819 # Now go through the remaining snapshots (if any)
820 foreach my $snap (@snapshots) {
821 $class->btrfs_cmd(['property', 'set', "$tmppath/$diskname\@$snap", 'ro', 'false']);
822 PVE::Tools::renameat2(
823 -1,
824 "$tmppath/$diskname\@$snap",
825 -1,
826 "$destination\@$snap",
827 &PVE::Tools::RENAME_NOREPLACE,
828 ) or die "failed to move received snapshot '$tmppath/$diskname\@$snap'"
829 . " into place at '$destination\@$snap' - $!\n";
830 eval { $class->btrfs_cmd(['property', 'set', "$destination\@$snap", 'ro', 'true']) };
831 warn "failed to make $destination\@$snap read-only - $!\n" if $@;
832 }
833 };
834 my $err = $@;
835
836 eval {
837 # Cleanup all the received snapshots we did not move into place, so we can remove the temp
838 # directory.
839 if ($dh) {
840 $dh->rewind;
841 while (defined(my $entry = $dh->read)) {
842 next if $entry eq '.' || $entry eq '..';
843 eval { $class->btrfs_cmd(['subvolume', 'delete', '--', "$tmppath/$entry"]) };
844 warn $@ if $@;
845 }
846 $dh->close; undef $dh;
847 }
848 if (!rmdir($tmppath)) {
849 warn "failed to remove temporary directory '$tmppath' - $!\n"
850 }
851 };
852 warn $@ if $@;
853 if ($err) {
854 # clean up if the directory ended up being empty after an error
855 rmdir($tmppath);
856 die $err;
857 }
858
859 return "$storeid:$volname";
860 }
861
862 1