All of them have a `+size` prefix to show that they're not "pure raw" or "pure tar" streams, because some storage may need to know in advance how much storage to allocate. The formats are explained in comments. PVE::Storage::Plugin now has default implementations for these for non-incremental streams exporting the current (rather than a snapshot state). To use qcow2 or vmdk formats $with_snapshots must be true, otherwise raw/tar will be used where $with_snapshots must be false.
374 lines
9.3 KiB
Perl
Executable File
374 lines
9.3 KiB
Perl
Executable File
package PVE::CLI::pvesm;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use POSIX qw(O_RDONLY O_WRONLY O_CREAT O_TRUNC);
|
|
use Fcntl ':flock';
|
|
use File::Path;
|
|
|
|
use PVE::SafeSyslog;
|
|
use PVE::Cluster;
|
|
use PVE::INotify;
|
|
use PVE::RPCEnvironment;
|
|
use PVE::Storage;
|
|
use PVE::API2::Storage::Config;
|
|
use PVE::API2::Storage::Content;
|
|
use PVE::API2::Storage::Status;
|
|
use PVE::API2::Storage::Scan;
|
|
use PVE::JSONSchema qw(get_standard_option);
|
|
|
|
use PVE::CLIHandler;
|
|
|
|
use base qw(PVE::CLIHandler);
|
|
|
|
my $KNOWN_EXPORT_FORMATS = ['raw+size', 'tar+size', 'qcow2+size', 'vmdk+size', 'zfs'];
|
|
|
|
my $nodename = PVE::INotify::nodename();
|
|
|
|
sub setup_environment {
|
|
PVE::RPCEnvironment->setup_default_cli_env();
|
|
}
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'path',
|
|
path => 'path',
|
|
method => 'GET',
|
|
description => "Get filesystem path for specified volume",
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
volume => {
|
|
description => "Volume identifier",
|
|
type => 'string', format => 'pve-volume-id',
|
|
completion => \&PVE::Storage::complete_volume,
|
|
},
|
|
},
|
|
},
|
|
returns => { type => 'null' },
|
|
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $cfg = PVE::Storage::config();
|
|
|
|
my $path = PVE::Storage::path ($cfg, $param->{volume});
|
|
|
|
print "$path\n";
|
|
|
|
return undef;
|
|
|
|
}});
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'extractconfig',
|
|
path => 'extractconfig',
|
|
method => 'GET',
|
|
description => "Extract configuration from vzdump backup archive.",
|
|
permissions => {
|
|
description => "The user needs 'VM.Backup' permissions on the backed up guest ID, and 'Datastore.AllocateSpace' on the backup storage.",
|
|
user => 'all',
|
|
},
|
|
protected => 1,
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
volume => {
|
|
description => "Volume identifier",
|
|
type => 'string',
|
|
completion => \&PVE::Storage::complete_volume,
|
|
},
|
|
},
|
|
},
|
|
returns => { type => 'null' },
|
|
code => sub {
|
|
my ($param) = @_;
|
|
my $volume = $param->{volume};
|
|
|
|
my $rpcenv = PVE::RPCEnvironment::get();
|
|
my $authuser = $rpcenv->get_user();
|
|
|
|
my $storage_cfg = PVE::Storage::config();
|
|
PVE::Storage::check_volume_access($rpcenv, $authuser, $storage_cfg, undef, $volume);
|
|
|
|
my $config_raw = PVE::Storage::extract_vzdump_config($storage_cfg, $volume);
|
|
|
|
print "$config_raw\n";
|
|
return;
|
|
}});
|
|
|
|
my $print_content = sub {
|
|
my ($list) = @_;
|
|
|
|
my $maxlenname = 0;
|
|
foreach my $info (@$list) {
|
|
|
|
my $volid = $info->{volid};
|
|
my $sidlen = length ($volid);
|
|
$maxlenname = $sidlen if $sidlen > $maxlenname;
|
|
}
|
|
|
|
foreach my $info (@$list) {
|
|
next if !$info->{vmid};
|
|
my $volid = $info->{volid};
|
|
|
|
printf "%-${maxlenname}s %5s %10d %d\n", $volid,
|
|
$info->{format}, $info->{size}, $info->{vmid};
|
|
}
|
|
|
|
foreach my $info (sort { $a->{format} cmp $b->{format} } @$list) {
|
|
next if $info->{vmid};
|
|
my $volid = $info->{volid};
|
|
|
|
printf "%-${maxlenname}s %5s %10d\n", $volid,
|
|
$info->{format}, $info->{size};
|
|
}
|
|
};
|
|
|
|
my $print_status = sub {
|
|
my $res = shift;
|
|
|
|
my $maxlen = 0;
|
|
foreach my $res (@$res) {
|
|
my $storeid = $res->{storage};
|
|
$maxlen = length ($storeid) if length ($storeid) > $maxlen;
|
|
}
|
|
$maxlen+=1;
|
|
|
|
foreach my $res (sort { $a->{storage} cmp $b->{storage} } @$res) {
|
|
my $storeid = $res->{storage};
|
|
|
|
my $sum = $res->{used} + $res->{avail};
|
|
my $per = $sum ? (0.5 + ($res->{used}*100)/$sum) : 100;
|
|
|
|
printf "%-${maxlen}s %5s %1d %15d %15d %15d %.2f%%\n", $storeid,
|
|
$res->{type}, $res->{active},
|
|
$res->{total}/1024, $res->{used}/1024, $res->{avail}/1024, $per;
|
|
}
|
|
};
|
|
|
|
__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 {
|
|
sysopen($outfh, $filename, O_CREAT|O_WRONLY|O_TRUNC)
|
|
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,
|
|
},
|
|
'delete-snapshot' => {
|
|
description => "A snapshot to delete on success",
|
|
type => 'string',
|
|
pattern => qr/[a-z0-9_\-]{1,80}/,
|
|
maxLength => 80,
|
|
optional => 1,
|
|
},
|
|
},
|
|
},
|
|
returns => { type => 'null' },
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $filename = $param->{filename};
|
|
|
|
my $infh;
|
|
if ($filename eq '-') {
|
|
$infh = \*STDIN;
|
|
} else {
|
|
sysopen($infh, $filename, O_RDONLY)
|
|
or die "open($filename): $!\n";
|
|
}
|
|
|
|
my $cfg = PVE::Storage::config();
|
|
my $volume = $param->{volume};
|
|
my $delete = $param->{'delete-snapshot'};
|
|
PVE::Storage::volume_import($cfg, $infh, $volume, $param->{format},
|
|
$param->{base}, $param->{'with-snapshots'});
|
|
PVE::Storage::volume_snapshot_delete($cfg, $volume, $delete)
|
|
if defined($delete);
|
|
return;
|
|
}
|
|
});
|
|
|
|
our $cmddef = {
|
|
add => [ "PVE::API2::Storage::Config", 'create', ['type', 'storage'] ],
|
|
set => [ "PVE::API2::Storage::Config", 'update', ['storage'] ],
|
|
remove => [ "PVE::API2::Storage::Config", 'delete', ['storage'] ],
|
|
status => [ "PVE::API2::Storage::Status", 'index', [],
|
|
{ node => $nodename }, $print_status ],
|
|
list => [ "PVE::API2::Storage::Content", 'index', ['storage'],
|
|
{ node => $nodename }, $print_content ],
|
|
alloc => [ "PVE::API2::Storage::Content", 'create', ['storage', 'vmid', 'filename', 'size'],
|
|
{ node => $nodename }, sub {
|
|
my $volid = shift;
|
|
print "successfully created '$volid'\n";
|
|
}],
|
|
free => [ "PVE::API2::Storage::Content", 'delete', ['volume'],
|
|
{ node => $nodename } ],
|
|
nfsscan => [ "PVE::API2::Storage::Scan", 'nfsscan', ['server'],
|
|
{ node => $nodename }, sub {
|
|
my $res = shift;
|
|
|
|
my $maxlen = 0;
|
|
foreach my $rec (@$res) {
|
|
my $len = length ($rec->{path});
|
|
$maxlen = $len if $len > $maxlen;
|
|
}
|
|
foreach my $rec (@$res) {
|
|
printf "%-${maxlen}s %s\n", $rec->{path}, $rec->{options};
|
|
}
|
|
}],
|
|
glusterfsscan => [ "PVE::API2::Storage::Scan", 'glusterfsscan', ['server'],
|
|
{ node => $nodename }, sub {
|
|
my $res = shift;
|
|
|
|
foreach my $rec (@$res) {
|
|
printf "%s\n", $rec->{volname};
|
|
}
|
|
}],
|
|
iscsiscan => [ "PVE::API2::Storage::Scan", 'iscsiscan', ['server'],
|
|
{ node => $nodename }, sub {
|
|
my $res = shift;
|
|
|
|
my $maxlen = 0;
|
|
foreach my $rec (@$res) {
|
|
my $len = length ($rec->{target});
|
|
$maxlen = $len if $len > $maxlen;
|
|
}
|
|
foreach my $rec (@$res) {
|
|
printf "%-${maxlen}s %s\n", $rec->{target}, $rec->{portal};
|
|
}
|
|
}],
|
|
lvmscan => [ "PVE::API2::Storage::Scan", 'lvmscan', [],
|
|
{ node => $nodename }, sub {
|
|
my $res = shift;
|
|
foreach my $rec (@$res) {
|
|
printf "$rec->{vg}\n";
|
|
}
|
|
}],
|
|
lvmthinscan => [ "PVE::API2::Storage::Scan", 'lvmthinscan', ['vg'],
|
|
{ node => $nodename }, sub {
|
|
my $res = shift;
|
|
foreach my $rec (@$res) {
|
|
printf "$rec->{lv}\n";
|
|
}
|
|
}],
|
|
zfsscan => [ "PVE::API2::Storage::Scan", 'zfsscan', [],
|
|
{ node => $nodename }, sub {
|
|
my $res = shift;
|
|
|
|
foreach my $rec (@$res) {
|
|
printf "$rec->{pool}\n";
|
|
}
|
|
}],
|
|
path => [ __PACKAGE__, 'path', ['volume']],
|
|
extractconfig => [__PACKAGE__, 'extractconfig', ['volume']],
|
|
export => [ __PACKAGE__, 'export', ['volume', 'format', 'filename']],
|
|
import => [ __PACKAGE__, 'import', ['volume', 'format', 'filename']],
|
|
};
|
|
|
|
1;
|