From 47f37b53622cdb52bb8c0cda081239b239a20f8e Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Fri, 12 May 2017 11:56:06 +0200 Subject: [PATCH] pvesm: import/export commands --- PVE/CLI/pvesm.pm | 139 +++++++++++++++++++++++++++++++++++ PVE/Storage.pm | 22 ++++++ PVE/Storage/Plugin.pm | 11 +++ PVE/Storage/ZFSPoolPlugin.pm | 62 ++++++++++++++++ 4 files changed, 234 insertions(+) diff --git a/PVE/CLI/pvesm.pm b/PVE/CLI/pvesm.pm index 3b99436..ba2c91b 100755 --- a/PVE/CLI/pvesm.pm +++ b/PVE/CLI/pvesm.pm @@ -21,6 +21,8 @@ use PVE::CLIHandler; use base qw(PVE::CLIHandler); +my $KNOWN_EXPORT_FORMATS = ['zfs']; + my $nodename = PVE::INotify::nodename(); sub setup_environment { @@ -144,6 +146,141 @@ my $print_status = sub { } }; +__PACKAGE__->register_method ({ + name => 'export', + path => 'export', + method => 'GET', + description => "Export a volume.", + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + volume => { + description => "Volume identifier", + type => 'string', + completion => \&PVE::Storage::complete_volume, + }, + format => { + description => "Export stream format", + type => 'string', + enum => $KNOWN_EXPORT_FORMATS, + }, + filename => { + description => "Destination file name", + type => 'string', + }, + base => { + description => "Snapshot to start an incremental stream from", + type => 'string', + pattern => qr/[a-z0-9_\-]{1,40}/, + maxLength => 40, + optional => 1, + }, + snapshot => { + description => "Snapshot to export", + type => 'string', + pattern => qr/[a-z0-9_\-]{1,40}/, + maxLength => 40, + optional => 1, + }, + 'with-snapshots' => { + description => + "Whether to include intermediate snapshots in the stream", + type => 'boolean', + optional => 1, + default => 0, + }, + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $filename = $param->{filename}; + + my $outfh; + if ($filename eq '-') { + $outfh = \*STDOUT; + } else { + open($outfh, '>', $filename) + or die "open($filename): $!\n"; + } + + eval { + my $cfg = PVE::Storage::config(); + PVE::Storage::volume_export($cfg, $outfh, $param->{volume}, $param->{format}, + $param->{snapshot}, $param->{base}, $param->{'with-snapshots'}); + }; + my $err = $@; + if ($filename ne '-') { + close($outfh); + unlink($filename) if $err; + } + die $err if $err; + return; + } +}); + +__PACKAGE__->register_method ({ + name => 'import', + path => 'import', + method => 'PUT', + description => "Import a volume.", + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + volume => { + description => "Volume identifier", + type => 'string', + completion => \&PVE::Storage::complete_volume, + }, + format => { + description => "Import stream format", + type => 'string', + enum => $KNOWN_EXPORT_FORMATS, + }, + filename => { + description => "Source file name", + type => 'string', + }, + base => { + description => "Base snapshot of an incremental stream", + type => 'string', + pattern => qr/[a-z0-9_\-]{1,40}/, + maxLength => 40, + optional => 1, + }, + 'with-snapshots' => { + description => + "Whether the stream includes intermediate snapshots", + type => 'boolean', + optional => 1, + default => 0, + }, + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $filename = $param->{filename}; + + my $infh; + if ($filename eq '-') { + $infh = \*STDIN; + } else { + open($infh, '<', $filename) + or die "open($filename): $!\n"; + } + + my $cfg = PVE::Storage::config(); + PVE::Storage::volume_import($cfg, $infh, $param->{volume}, $param->{format}, + $param->{base}, $param->{'with-snapshots'}); + return; + } +}); + our $cmddef = { add => [ "PVE::API2::Storage::Config", 'create', ['type', 'storage'] ], set => [ "PVE::API2::Storage::Config", 'update', ['storage'] ], @@ -217,6 +354,8 @@ our $cmddef = { }], path => [ __PACKAGE__, 'path', ['volume']], extractconfig => [__PACKAGE__, 'extractconfig', ['volume']], + export => [ __PACKAGE__, 'export', ['volume', 'format', 'filename']], + import => [ __PACKAGE__, 'import', ['volume', 'format', 'filename']], }; 1; diff --git a/PVE/Storage.pm b/PVE/Storage.pm index a4125d5..7046a8e 100755 --- a/PVE/Storage.pm +++ b/PVE/Storage.pm @@ -1453,6 +1453,28 @@ sub extract_vzdump_config { } } +sub volume_export { + my ($cfg, $fh, $volid, $format, $snapshot, $base_snapshot, $with_snapshots) = @_; + + my ($storeid, $volname) = parse_volume_id($volid, 1); + die "cannot export volume '$volid'\n" if !$storeid; + my $scfg = storage_config($cfg, $storeid); + my $plugin = PVE::Storage::Plugin->lookup($scfg->{type}); + return $plugin->volume_export($scfg, $storeid, $fh, $volname, $format, + $snapshot, $base_snapshot, $with_snapshots); +} + +sub volume_import { + my ($cfg, $fh, $volid, $format, $base_snapshot, $with_snapshots) = @_; + + my ($storeid, $volname) = parse_volume_id($volid, 1); + die "cannot import into volume '$volid'\n" if !$storeid; + my $scfg = storage_config($cfg, $storeid); + my $plugin = PVE::Storage::Plugin->lookup($scfg->{type}); + return $plugin->volume_import($scfg, $storeid, $fh, $volname, $format, + $base_snapshot, $with_snapshots); +} + # bash completion helper sub complete_storage { diff --git a/PVE/Storage/Plugin.pm b/PVE/Storage/Plugin.pm index b10e2d9..b7ec261 100644 --- a/PVE/Storage/Plugin.pm +++ b/PVE/Storage/Plugin.pm @@ -888,5 +888,16 @@ sub check_connection { return 1; } +# Export a volume into a file handle as a stream of desired format. +sub volume_export { + my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots) = @_; + die "volume export not implemented for $class"; +} + +# Import data from a stream, creating a new or replacing or adding to an existing volume. +sub volume_import { + my ($class, $scfg, $storeid, $fh, $volname, $format, $base_snapshot, $with_snapshots) = @_; + die "volume import not implemented for $class"; +} 1; diff --git a/PVE/Storage/ZFSPoolPlugin.pm b/PVE/Storage/ZFSPoolPlugin.pm index 0636ee1..72da44d 100644 --- a/PVE/Storage/ZFSPoolPlugin.pm +++ b/PVE/Storage/ZFSPoolPlugin.pm @@ -652,4 +652,66 @@ sub volume_has_feature { return undef; } +sub volume_export { + my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots) = @_; + + die "unsupported export stream format for $class: $format\n" + if $format ne 'zfs'; + + die "$class storage can only export snapshots\n" + if !defined($snapshot); + + my $fd = fileno($fh); + die "internal error: invalid file handle for volume_export\n" + if !defined($fd); + $fd = ">&$fd"; + + # For zfs we always create a replication stream (-R) which means the remote + # side will always delete non-existing source snapshots. This should work + # for all our use cases. + my $cmd = ['zfs', 'send', '-Rpv']; + if (defined($base_snapshot)) { + my $arg = $with_snapshots ? '-I' : '-i'; + push @$cmd, $arg, $base_snapshot; + } + push @$cmd, '--', "$scfg->{pool}/$volname\@$snapshot"; + + run_command($cmd, output => $fd); + + return; +} + +sub volume_import { + my ($class, $scfg, $storeid, $fh, $volname, $format, $base_snapshot, $with_snapshots) = @_; + + die "unsupported import stream format for $class: $format\n" + if $format ne 'zfs'; + + my $fd = fileno($fh); + die "internal error: invalid file handle for volume_import\n" + if !defined($fd); + + my $zfspath = "$scfg->{pool}/$volname"; + my $suffix = defined($base_snapshot) ? "\@$base_snapshot" : ''; + my $exists = 0 == run_command(['zfs', 'get', '-H', 'name', $zfspath.$suffix], + noerr => 1, errfunc => sub {}); + if (defined($base_snapshot)) { + die "base snapshot '$zfspath\@$base_snapshot' doesn't exist\n" if !$exists; + } else { + die "volume '$zfspath' already exists\n" if $exists; + } + + eval { run_command(['zfs', 'recv', '-F', '--', $zfspath], input => "<&$fd") }; + if (my $err = $@) { + if (defined($base_snapshot)) { + eval { run_command(['zfs', 'rollback', '-r', '--', "$zfspath\@$base_snapshot"]) }; + } else { + eval { run_command(['zfs', 'destroy', '-r', '--', $zfspath]) }; + } + die $err; + } + + return; +} + 1; -- 2.39.2