btrfs: add 'btrfs' import/export format
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
committed by
Thomas Lamprecht
parent
3cc29a0487
commit
a0e3e224ea
@ -30,7 +30,7 @@ use PVE::CLIHandler;
|
||||
|
||||
use base qw(PVE::CLIHandler);
|
||||
|
||||
my $KNOWN_EXPORT_FORMATS = ['raw+size', 'tar+size', 'qcow2+size', 'vmdk+size', 'zfs'];
|
||||
my $KNOWN_EXPORT_FORMATS = ['raw+size', 'tar+size', 'qcow2+size', 'vmdk+size', 'zfs', 'btrfs'];
|
||||
|
||||
my $nodename = PVE::INotify::nodename();
|
||||
|
||||
|
||||
@ -692,7 +692,7 @@ sub storage_migrate {
|
||||
|
||||
my $migration_snapshot;
|
||||
if (!defined($snapshot)) {
|
||||
if ($scfg->{type} eq 'zfspool') {
|
||||
if ($scfg->{type} eq 'zfspool' || $scfg->{type} eq 'btrfs') {
|
||||
$migration_snapshot = 1;
|
||||
$snapshot = '__migration__';
|
||||
}
|
||||
|
||||
@ -9,8 +9,9 @@ use Fcntl qw(S_ISDIR O_WRONLY O_CREAT O_EXCL);
|
||||
use File::Basename qw(dirname);
|
||||
use File::Path qw(mkpath);
|
||||
use IO::Dir;
|
||||
use POSIX qw(EEXIST);
|
||||
|
||||
use PVE::Tools qw(run_command);
|
||||
use PVE::Tools qw(run_command dir_glob_foreach);
|
||||
|
||||
use PVE::Storage::DirPlugin;
|
||||
|
||||
@ -612,23 +613,250 @@ sub list_images {
|
||||
return $res;
|
||||
}
|
||||
|
||||
# For now we don't implement `btrfs send/recv` as it needs some updates to our import/export API
|
||||
# first!
|
||||
|
||||
sub volume_export_formats {
|
||||
return PVE::Storage::DirPlugin::volume_export_formats(@_);
|
||||
}
|
||||
my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
|
||||
|
||||
sub volume_export {
|
||||
return PVE::Storage::DirPlugin::volume_export(@_);
|
||||
# We can do whatever `DirPlugin` can do.
|
||||
my @result = PVE::Storage::Plugin::volume_export_formats(@_);
|
||||
|
||||
# `btrfs send` only works on snapshots:
|
||||
return @result if !defined $snapshot;
|
||||
|
||||
# Incremental stream with snapshots is only supported if the snapshots are listed (new api):
|
||||
return @result if defined($base_snapshot) && $with_snapshots && ref($with_snapshots) ne 'ARRAY';
|
||||
|
||||
# Otherwise we do also support `with_snapshots`.
|
||||
|
||||
# Finally, `btrfs send` only works on formats where we actually use btrfs subvolumes:
|
||||
my $format = ($class->parse_volname($volname))[6];
|
||||
return @result if $format ne 'raw' && $format ne 'subvol';
|
||||
|
||||
return ('btrfs', @result);
|
||||
}
|
||||
|
||||
sub volume_import_formats {
|
||||
return PVE::Storage::DirPlugin::volume_import_formats(@_);
|
||||
my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
|
||||
|
||||
# Same as export-formats, beware the parameter order:
|
||||
return volume_export_formats(
|
||||
$class,
|
||||
$scfg,
|
||||
$storeid,
|
||||
$volname,
|
||||
$snapshot,
|
||||
$base_snapshot,
|
||||
$with_snapshots,
|
||||
);
|
||||
}
|
||||
|
||||
sub volume_export {
|
||||
my (
|
||||
$class,
|
||||
$scfg,
|
||||
$storeid,
|
||||
$fh,
|
||||
$volname,
|
||||
$format,
|
||||
$snapshot,
|
||||
$base_snapshot,
|
||||
$with_snapshots,
|
||||
) = @_;
|
||||
|
||||
if ($format ne 'btrfs') {
|
||||
return PVE::Storage::Plugin::volume_export(@_);
|
||||
}
|
||||
|
||||
die "format 'btrfs' only works on snapshots\n"
|
||||
if !defined $snapshot;
|
||||
|
||||
die "'btrfs' format in incremental mode requires snapshots to be listed explicitly\n"
|
||||
if defined($base_snapshot) && $with_snapshots && ref($with_snapshots) ne 'ARRAY';
|
||||
|
||||
my $volume_format = ($class->parse_volname($volname))[6];
|
||||
|
||||
die "btrfs-sending volumes of type $volume_format ('$volname') is not supported\n"
|
||||
if $volume_format ne 'raw' && $volume_format ne 'subvol';
|
||||
|
||||
my $path = $class->path($scfg, $volname, $storeid);
|
||||
|
||||
if ($volume_format eq 'raw') {
|
||||
$path = raw_file_to_subvol($path);
|
||||
}
|
||||
|
||||
my $cmd = ['btrfs', '-q', 'send', '-e'];
|
||||
if ($base_snapshot) {
|
||||
my $base = $class->path($scfg, $volname, $storeid, $base_snapshot);
|
||||
if ($volume_format eq 'raw') {
|
||||
$base = raw_file_to_subvol($base);
|
||||
}
|
||||
push @$cmd, '-p', $base;
|
||||
}
|
||||
push @$cmd, '--';
|
||||
if (ref($with_snapshots) eq 'ARRAY') {
|
||||
push @$cmd, (map { "$path\@$_" } ($with_snapshots // [])->@*), $path;
|
||||
} else {
|
||||
dir_glob_foreach(dirname($path), $BTRFS_VOL_REGEX, sub {
|
||||
push @$cmd, "$path\@$_[2]" if !(defined($snapshot) && $_[2] eq $snapshot);
|
||||
});
|
||||
}
|
||||
$path .= "\@$snapshot" if defined($snapshot);
|
||||
push @$cmd, $path;
|
||||
|
||||
run_command($cmd, output => '>&'.fileno($fh));
|
||||
return;
|
||||
}
|
||||
|
||||
sub volume_import {
|
||||
return PVE::Storage::DirPlugin::volume_import(@_);
|
||||
my (
|
||||
$class,
|
||||
$scfg,
|
||||
$storeid,
|
||||
$fh,
|
||||
$volname,
|
||||
$format,
|
||||
$snapshot,
|
||||
$base_snapshot,
|
||||
$with_snapshots,
|
||||
$allow_rename,
|
||||
) = @_;
|
||||
|
||||
if ($format ne 'btrfs') {
|
||||
return PVE::Storage::Plugin::volume_import(@_);
|
||||
}
|
||||
|
||||
die "format 'btrfs' only works on snapshots\n"
|
||||
if !defined $snapshot;
|
||||
|
||||
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $volume_format) =
|
||||
$class->parse_volname($volname);
|
||||
|
||||
die "btrfs-receiving volumes of type $volume_format ('$volname') is not supported\n"
|
||||
if $volume_format ne 'raw' && $volume_format ne 'subvol';
|
||||
|
||||
if (defined($base_snapshot)) {
|
||||
my $path = $class->path($scfg, $volname, $storeid, $base_snapshot);
|
||||
die "base snapshot '$base_snapshot' not found - no such directory '$path'\n"
|
||||
if !path_is_subvolume($path);
|
||||
}
|
||||
|
||||
my $destination = $class->filesystem_path($scfg, $volname);
|
||||
if ($volume_format eq 'raw') {
|
||||
$destination = raw_file_to_subvol($destination);
|
||||
}
|
||||
|
||||
if (!defined($base_snapshot) && -e $destination) {
|
||||
die "volume $volname already exists\n" if !$allow_rename;
|
||||
$volname = $class->find_free_diskname($storeid, $scfg, $vmid, $volume_format, 1);
|
||||
}
|
||||
|
||||
my $imagedir = $class->get_subdir($scfg, $vtype);
|
||||
$imagedir .= "/$vmid" if $vtype eq 'images';
|
||||
|
||||
my $tmppath = "$imagedir/recv.$vmid.tmp";
|
||||
mkdir($imagedir); # FIXME: if $scfg->{mkdir};
|
||||
if (!mkdir($tmppath)) {
|
||||
die "temp receive directory already exists at '$tmppath', incomplete concurrent import?\n"
|
||||
if $! == EEXIST;
|
||||
die "failed to create temporary receive directory at '$tmppath' - $!\n";
|
||||
}
|
||||
|
||||
my $dh = IO::Dir->new($tmppath)
|
||||
or die "failed to open temporary receive directory '$tmppath' - $!\n";
|
||||
eval {
|
||||
run_command(['btrfs', '-q', 'receive', '-e', '--', $tmppath], input => '<&'.fileno($fh));
|
||||
|
||||
# Analyze the received subvolumes;
|
||||
my ($diskname, $found_snapshot, @snapshots);
|
||||
$dh->rewind;
|
||||
while (defined(my $entry = $dh->read)) {
|
||||
next if $entry eq '.' || $entry eq '..';
|
||||
next if $entry !~ /^$BTRFS_VOL_REGEX$/;
|
||||
my ($cur_diskname, $cur_snapshot) = ($1, $2);
|
||||
|
||||
die "send stream included a non-snapshot subvolume\n"
|
||||
if !defined($cur_snapshot);
|
||||
|
||||
if (!defined($diskname)) {
|
||||
$diskname = $cur_diskname;
|
||||
} else {
|
||||
die "multiple disks contained in stream ('$diskname' vs '$cur_diskname')\n"
|
||||
if $diskname ne $cur_diskname;
|
||||
}
|
||||
|
||||
if ($cur_snapshot eq $snapshot) {
|
||||
$found_snapshot = 1;
|
||||
} else {
|
||||
push @snapshots, $cur_snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
die "send stream did not contain the expected current snapshot '$snapshot'\n"
|
||||
if !$found_snapshot;
|
||||
|
||||
# Rotate the disk into place, first the current state:
|
||||
# Note that read-only subvolumes cannot be moved into different directories, but for the
|
||||
# "current" state we also want a writable copy, so start with that:
|
||||
$class->btrfs_cmd(['property', 'set', "$tmppath/$diskname\@$snapshot", 'ro', 'false']);
|
||||
PVE::Tools::renameat2(
|
||||
-1,
|
||||
"$tmppath/$diskname\@$snapshot",
|
||||
-1,
|
||||
$destination,
|
||||
&PVE::Tools::RENAME_NOREPLACE,
|
||||
) or die "failed to move received snapshot '$tmppath/$diskname\@$snapshot'"
|
||||
. " into place at '$destination' - $!\n";
|
||||
|
||||
# Now recreate the actual snapshot:
|
||||
$class->btrfs_cmd([
|
||||
'subvolume',
|
||||
'snapshot',
|
||||
'-r',
|
||||
'--',
|
||||
$destination,
|
||||
"$destination\@$snapshot",
|
||||
]);
|
||||
|
||||
# Now go through the remaining snapshots (if any)
|
||||
foreach my $snap (@snapshots) {
|
||||
$class->btrfs_cmd(['property', 'set', "$tmppath/$diskname\@$snap", 'ro', 'false']);
|
||||
PVE::Tools::renameat2(
|
||||
-1,
|
||||
"$tmppath/$diskname\@$snap",
|
||||
-1,
|
||||
"$destination\@$snap",
|
||||
&PVE::Tools::RENAME_NOREPLACE,
|
||||
) or die "failed to move received snapshot '$tmppath/$diskname\@$snap'"
|
||||
. " into place at '$destination\@$snap' - $!\n";
|
||||
eval { $class->btrfs_cmd(['property', 'set', "$destination\@$snap", 'ro', 'true']) };
|
||||
warn "failed to make $destination\@$snap read-only - $!\n" if $@;
|
||||
}
|
||||
};
|
||||
my $err = $@;
|
||||
|
||||
eval {
|
||||
# Cleanup all the received snapshots we did not move into place, so we can remove the temp
|
||||
# directory.
|
||||
if ($dh) {
|
||||
$dh->rewind;
|
||||
while (defined(my $entry = $dh->read)) {
|
||||
next if $entry eq '.' || $entry eq '..';
|
||||
eval { $class->btrfs_cmd(['subvolume', 'delete', '--', "$tmppath/$entry"]) };
|
||||
warn $@ if $@;
|
||||
}
|
||||
$dh->close; undef $dh;
|
||||
}
|
||||
if (!rmdir($tmppath)) {
|
||||
warn "failed to remove temporary directory '$tmppath' - $!\n"
|
||||
}
|
||||
};
|
||||
warn $@ if $@;
|
||||
if ($err) {
|
||||
# clean up if the directory ended up being empty after an error
|
||||
rmdir($tmppath);
|
||||
die $err;
|
||||
}
|
||||
|
||||
return "$storeid:$volname";
|
||||
}
|
||||
|
||||
1
|
||||
|
||||
Reference in New Issue
Block a user