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