Files
pve-storage/PVE/CLI/pvesm.pm
Wolfgang Bumiller 3cc29a0487 bump storage API: update import/export methods
Bumps APIVER to 9 and resets APIAGE to zero.

The import methods (volume_import, volume_import_formats):

These additionally get the '$snapshot' parameter which is
already present on the export side as an informational piece
to know which of the snapshots is the *current* one.
This parameter is inserted *in the middle* of the current
parameters, so the import & export format methods now have
the same signatures.
The current "disk" state will be set to this snapshot.
This, too, is required for our btrfs implementation.
  `volume_import_formats` can obviously not make much
*use* of this parameter, but it'll still be useful to know
that the information is actually available in the import
call, so its presence will be checked in the btrfs
implementation.

Currently this is intended to be used for btrfs send/recv
support, which in theory could also get additional metadata
similar to how we do the "tar+size" format, however, we
currently only really use this within this repository in
storage_migrate() which has this information readily
available anyway.

On the export side (volume_export, volume_export_formats):

The `$with_snapshots` option is now "defined" to be an
ordered array of snapshots to include, as a hint for
storages which need this. (As of the next commit this is
only btrfs, and only when also specifying a base snapshot,
which is a case we can currently not run into except on the
command line interface.)
  The current providers of the `with_snapshot` option will
still treat it as a boolean (since eg. for ZFS you cannot
really "skip" snapshots AFAIK).
  This is mainly intended for storages which do not have a
strong association between snapshots and the originals, or
an ordering (eg. btrfs and lvm-thin allow creating
arbitrary snapshot trees, and with btrfs you can even
create a "circular" connection between subvolumes, also we
could consider reflink based copies snapshots on xfs in
the future maybe?)

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2021-06-23 20:20:31 +02:00

714 lines
19 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 MIME::Base64 qw(encode_base64);
use IO::Socket::IP;
use IO::Socket::UNIX;
use Socket qw(SOCK_STREAM);
use PVE::SafeSyslog;
use PVE::Cluster;
use PVE::INotify;
use PVE::RPCEnvironment;
use PVE::Storage;
use PVE::Tools qw(extract_param);
use PVE::API2::Storage::Config;
use PVE::API2::Storage::Content;
use PVE::API2::Storage::PruneBackups;
use PVE::API2::Storage::Scan;
use PVE::API2::Storage::Status;
use PVE::JSONSchema qw(get_standard_option);
use PVE::PTY;
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 param_mapping {
my ($name) = @_;
my $password_map = PVE::CLIHandler::get_standard_mapping('pve-password', {
func => sub {
my ($value) = @_;
return $value if $value;
return PVE::PTY::read_password("Enter Password: ");
},
});
my $enc_key_map = {
name => 'encryption-key',
desc => 'a file containing an encryption key, or the special value "autogen"',
func => sub {
my ($value) = @_;
return $value if $value eq 'autogen';
return PVE::Tools::file_get_contents($value);
}
};
my $master_key_map = {
name => 'master-pubkey',
desc => 'a file containing a PEM-formatted master public key',
func => sub {
my ($value) = @_;
return encode_base64(PVE::Tools::file_get_contents($value), '');
}
};
my $mapping = {
'cifsscan' => [ $password_map ],
'cifs' => [ $password_map ],
'pbs' => [ $password_map ],
'create' => [ $password_map, $enc_key_map, $master_key_map ],
'update' => [ $password_map, $enc_key_map, $master_key_map ],
};
return $mapping->{$name};
}
sub setup_environment {
PVE::RPCEnvironment->setup_default_cli_env();
}
__PACKAGE__->register_method ({
name => 'apiinfo',
path => 'apiinfo',
method => 'GET',
description => "Returns APIVER and APIAGE.",
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'object',
properties => {
apiver => { type => 'integer' },
apiage => { type => 'integer' },
},
},
code => sub {
return {
apiver => PVE::Storage::APIVER,
apiage => PVE::Storage::APIAGE,
};
}
});
__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, $maxsize) = (0, 0);
foreach my $info (@$list) {
my $volid = $info->{volid};
my $sidlen = length ($volid);
$maxlenname = $sidlen if $sidlen > $maxlenname;
$maxsize = $info->{size} if ($info->{size} // 0) > $maxsize;
}
my $sizemaxdigits = length($maxsize);
my $basefmt = "%-${maxlenname}s %-7s %-9s %${sizemaxdigits}s";
printf "$basefmt %s\n", "Volid", "Format", "Type", "Size", "VMID";
foreach my $info (@$list) {
next if !$info->{vmid};
my $volid = $info->{volid};
printf "$basefmt %d\n", $volid, $info->{format}, $info->{content}, $info->{size}, $info->{vmid};
}
foreach my $info (sort { $a->{format} cmp $b->{format} } @$list) {
next if $info->{vmid};
my $volid = $info->{volid};
printf "$basefmt\n", $volid, $info->{format}, $info->{content}, $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;
printf "%-${maxlen}s %10s %10s %15s %15s %15s %8s\n", 'Name', 'Type',
'Status', 'Total', 'Used', 'Available', '%';
foreach my $res (sort { $a->{storage} cmp $b->{storage} } @$res) {
my $storeid = $res->{storage};
my $active = $res->{active} ? 'active' : 'inactive';
my ($per, $per_fmt) = (0, '% 7.2f%%');
$per = ($res->{used}*100)/$res->{total} if $res->{total} > 0;
if (!$res->{enabled}) {
$per = 'N/A';
$per_fmt = '% 8s';
$active = 'disabled';
}
printf "%-${maxlen}s %10s %10s %15d %15d %15d $per_fmt\n", $storeid,
$res->{type}, $active, $res->{total}/1024, $res->{used}/1024,
$res->{avail}/1024, $per;
}
};
__PACKAGE__->register_method ({
name => 'export',
path => 'export',
method => 'GET',
description => "Used internally to 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}/i,
maxLength => 40,
optional => 1,
},
snapshot => {
description => "Snapshot to export",
type => 'string',
pattern => qr/[a-z0-9_\-]{1,40}/i,
maxLength => 40,
optional => 1,
},
'with-snapshots' => {
description =>
"Whether to include intermediate snapshots in the stream",
type => 'boolean',
optional => 1,
default => 0,
},
'snapshot-list' => {
description => "Ordered list of snapshots to transfer",
type => 'string',
format => 'string-list',
optional => 1,
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my $with_snapshots = $param->{'with-snapshots'};
if (defined(my $list = $param->{'snapshot-list'})) {
$with_snapshots = PVE::Tools::split_list($list);
}
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}, $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 => "Used internally to 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. For '-' stdin is used, the " .
"tcp://<IP-or-CIDR> format allows to use a TCP connection, " .
"the unix://PATH-TO-SOCKET format a UNIX socket as input." .
"Else, the file is treated as common file.",
type => 'string',
},
base => {
description => "Base snapshot of an incremental stream",
type => 'string',
pattern => qr/[a-z0-9_\-]{1,40}/i,
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}/i,
maxLength => 80,
optional => 1,
},
'allow-rename' => {
description => "Choose a new volume ID if the requested " .
"volume ID already exists, instead of throwing an error.",
type => 'boolean',
optional => 1,
default => 0,
},
snapshot => {
description => "The current-state snapshot if the stream contains snapshots",
type => 'string',
pattern => qr/[a-z0-9_\-]{1,40}/i,
maxLength => 40,
optional => 1,
},
},
},
returns => { type => 'string' },
code => sub {
my ($param) = @_;
my $filename = $param->{filename};
my $infh;
if ($filename eq '-') {
$infh = \*STDIN;
} elsif ($filename =~ m!^tcp://(([^/]+)(/\d+)?)$!) {
my ($cidr, $ip, $subnet) = ($1, $2, $3);
if ($subnet) { # got real CIDR notation, not just IP
my $ips = PVE::Network::get_local_ip_from_cidr($cidr);
die "Unable to get any local IP address in network '$cidr'\n"
if scalar(@$ips) < 1;
die "Got multiple local IP address in network '$cidr'\n"
if scalar(@$ips) > 1;
$ip = $ips->[0];
}
my $family = PVE::Tools::get_host_address_family($ip);
my $port = PVE::Tools::next_migrate_port($family, $ip);
my $sock_params = {
Listen => 1,
ReuseAddr => 1,
Proto => &Socket::IPPROTO_TCP,
GetAddrInfoFlags => 0,
LocalAddr => $ip,
LocalPort => $port,
};
my $socket = IO::Socket::IP->new(%$sock_params)
or die "failed to open socket: $!\n";
print "$ip\n$port\n"; # tell remote where to connect
*STDOUT->flush();
my $prev_alarm = alarm 0;
local $SIG{ALRM} = sub { die "timed out waiting for client\n" };
alarm 30;
my $client = $socket->accept; # Wait for a client
alarm $prev_alarm;
close($socket);
$infh = \*$client;
} elsif ($filename =~ m!^unix://(.*)$!) {
my $socket_path = $1;
my $socket = IO::Socket::UNIX->new(
Type => SOCK_STREAM(),
Local => $socket_path,
Listen => 1,
) or die "failed to open socket: $!\n";
print "ready\n";
*STDOUT->flush();
my $prev_alarm = alarm 0;
local $SIG{ALRM} = sub { die "timed out waiting for client\n" };
alarm 30;
my $client = $socket->accept; # Wait for a client
alarm $prev_alarm;
close($socket);
$infh = \*$client;
} 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'};
my $imported_volid = PVE::Storage::volume_import($cfg, $infh, $volume, $param->{format},
$param->{snapshot}, $param->{base}, $param->{'with-snapshots'},
$param->{'allow-rename'});
PVE::Storage::volume_snapshot_delete($cfg, $imported_volid, $delete)
if defined($delete);
return $imported_volid;
}
});
__PACKAGE__->register_method ({
name => 'prunebackups',
path => 'prunebackups',
method => 'GET',
description => "Prune backups. Only those using the standard naming scheme are considered. " .
"If no keep options are specified, those from the storage configuration are used.",
protected => 1,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
'dry-run' => {
description => "Only show what would be pruned, don't delete anything.",
type => 'boolean',
optional => 1,
},
node => get_standard_option('pve-node'),
storage => get_standard_option('pve-storage-id', {
completion => \&PVE::Storage::complete_storage_enabled,
}),
%{$PVE::Storage::Plugin::prune_backups_format},
type => {
description => "Either 'qemu' or 'lxc'. Only consider backups for guests of this type.",
type => 'string',
optional => 1,
enum => ['qemu', 'lxc'],
},
vmid => get_standard_option('pve-vmid', {
description => "Only consider backups for this guest.",
optional => 1,
completion => \&PVE::Cluster::complete_vmid,
}),
},
},
returns => {
type => 'object',
properties => {
dryrun => {
description => 'If it was a dry run or not. The list will only be defined in that case.',
type => 'boolean',
},
list => {
type => 'array',
items => {
type => 'object',
properties => {
volid => {
description => "Backup volume ID.",
type => 'string',
},
'ctime' => {
description => "Creation time of the backup (seconds since the UNIX epoch).",
type => 'integer',
},
'mark' => {
description => "Whether the backup would be kept or removed. For backups that don't " .
"use the standard naming scheme, it's 'protected'.",
type => 'string',
},
type => {
description => "One of 'qemu', 'lxc', 'openvz' or 'unknown'.",
type => 'string',
},
'vmid' => {
description => "The VM the backup belongs to.",
type => 'integer',
optional => 1,
},
},
},
},
},
},
code => sub {
my ($param) = @_;
my $dryrun = extract_param($param, 'dry-run') ? 1 : 0;
my $keep_opts;
foreach my $keep (keys %{$PVE::Storage::Plugin::prune_backups_format}) {
$keep_opts->{$keep} = extract_param($param, $keep) if defined($param->{$keep});
}
$param->{'prune-backups'} = PVE::JSONSchema::print_property_string(
$keep_opts, $PVE::Storage::Plugin::prune_backups_format) if $keep_opts;
my $list = [];
if ($dryrun) {
$list = PVE::API2::Storage::PruneBackups->dryrun($param);
} else {
PVE::API2::Storage::PruneBackups->delete($param);
}
return {
dryrun => $dryrun,
list => $list,
};
}});
my $print_api_result = sub {
my ($data, $schema, $options) = @_;
PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
};
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 } ],
scan => {
nfs => [ "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};
}
}],
cifs => [ "PVE::API2::Storage::Scan", 'cifsscan', ['server'], { node => $nodename }, sub {
my $res = shift;
my $maxlen = 0;
foreach my $rec (@$res) {
my $len = length ($rec->{share});
$maxlen = $len if $len > $maxlen;
}
foreach my $rec (@$res) {
printf "%-${maxlen}s %s\n", $rec->{share}, $rec->{description};
}
}],
glusterfs => [ "PVE::API2::Storage::Scan", 'glusterfsscan', ['server'], { node => $nodename }, sub {
my $res = shift;
foreach my $rec (@$res) {
printf "%s\n", $rec->{volname};
}
}],
iscsi => [ "PVE::API2::Storage::Scan", 'iscsiscan', ['portal'], { 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};
}
}],
lvm => [ "PVE::API2::Storage::Scan", 'lvmscan', [], { node => $nodename }, sub {
my $res = shift;
foreach my $rec (@$res) {
printf "$rec->{vg}\n";
}
}],
lvmthin => [ "PVE::API2::Storage::Scan", 'lvmthinscan', ['vg'], { node => $nodename }, sub {
my $res = shift;
foreach my $rec (@$res) {
printf "$rec->{lv}\n";
}
}],
pbs => [
"PVE::API2::Storage::Scan",
'pbsscan',
['server', 'username'],
{ node => $nodename },
$print_api_result,
$PVE::RESTHandler::standard_output_options,
],
zfs => [ "PVE::API2::Storage::Scan", 'zfsscan', [], { node => $nodename }, sub {
my $res = shift;
foreach my $rec (@$res) {
printf "$rec->{pool}\n";
}
}],
},
nfsscan => { alias => 'scan nfs' },
cifsscan => { alias => 'scan cifs' },
glusterfsscan => { alias => 'scan glusterfs' },
iscsiscan => { alias => 'scan iscsi' },
lvmscan => { alias => 'scan lvm' },
lvmthinscan => { alias => 'scan lvmthin' },
zfsscan => { alias => 'scan zfs' },
path => [ __PACKAGE__, 'path', ['volume']],
extractconfig => [__PACKAGE__, 'extractconfig', ['volume']],
export => [ __PACKAGE__, 'export', ['volume', 'format', 'filename']],
import => [ __PACKAGE__, 'import', ['volume', 'format', 'filename'], {}, sub {
my $volid = shift;
print PVE::Storage::volume_imported_message($volid);
}],
apiinfo => [ __PACKAGE__, 'apiinfo', [], {}, sub {
my $res = shift;
print "APIVER $res->{apiver}\n";
print "APIAGE $res->{apiage}\n";
}],
'prune-backups' => [ __PACKAGE__, 'prunebackups', ['storage'], { node => $nodename }, sub {
my $res = shift;
my ($dryrun, $list) = ($res->{dryrun}, $res->{list});
return if !$dryrun;
if (!scalar(@{$list})) {
print "No backups found\n";
return;
}
print "NOTE: this is only a preview and might not be what a subsequent\n" .
"prune call does if backups are removed/added in the meantime.\n\n";
my @sorted = sort {
my $vmcmp = PVE::Tools::safe_compare($a->{vmid}, $b->{vmid}, sub { $_[0] <=> $_[1] });
return $vmcmp if $vmcmp ne 0;
return $a->{ctime} <=> $b->{ctime};
} @{$list};
my $maxlen = 0;
foreach my $backup (@sorted) {
my $volid = $backup->{volid};
$maxlen = length($volid) if length($volid) > $maxlen;
}
$maxlen+=1;
printf("%-${maxlen}s %15s %10s\n", 'Backup', 'Backup-ID', 'Prune-Mark');
foreach my $backup (@sorted) {
my $type = $backup->{type};
my $vmid = $backup->{vmid};
my $backup_id = defined($vmid) ? "$type/$vmid" : "$type";
printf("%-${maxlen}s %15s %10s\n", $backup->{volid}, $backup_id, $backup->{mark});
}
}],
};
1;