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