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