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