Files
pve-storage/PVE/API2/Storage/Status.pm
Lorenz Stechauner 31bd43648d api: status: fix unlink on file upload
after an error while copying the file to its destination the local
path of the destination was unlinked in every case, even when on the
destination was copied to via scp.

Signed-off-by: Lorenz Stechauner <l.stechauner@proxmox.com>
2021-07-29 17:12:40 +02:00

624 lines
17 KiB
Perl

package PVE::API2::Storage::Status;
use strict;
use warnings;
use File::Basename;
use File::Path;
use PVE::Cluster;
use PVE::Exception qw(raise_param_exc);
use PVE::INotify;
use PVE::JSONSchema qw(get_standard_option);
use PVE::RESTHandler;
use PVE::RPCEnvironment;
use PVE::RRD;
use PVE::Tools;
use PVE::API2::Storage::Content;
use PVE::API2::Storage::FileRestore;
use PVE::API2::Storage::PruneBackups;
use PVE::Storage;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
subclass => "PVE::API2::Storage::PruneBackups",
path => '{storage}/prunebackups',
});
__PACKAGE__->register_method ({
subclass => "PVE::API2::Storage::Content",
# set fragment delimiter (no subdirs) - we need that, because volume
# IDs may contain a slash '/'
fragmentDelimiter => '',
path => '{storage}/content',
});
__PACKAGE__->register_method ({
subclass => "PVE::API2::Storage::FileRestore",
path => '{storage}/file-restore',
});
__PACKAGE__->register_method ({
name => 'index',
path => '',
method => 'GET',
description => "Get status for all datastores.",
permissions => {
description => "Only list entries where you have 'Datastore.Audit' or 'Datastore.AllocateSpace' permissions on '/storage/<storage>'",
user => 'all',
},
protected => 1,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
storage => get_standard_option('pve-storage-id', {
description => "Only list status for specified storage",
optional => 1,
completion => \&PVE::Storage::complete_storage_enabled,
}),
content => {
description => "Only list stores which support this content type.",
type => 'string', format => 'pve-storage-content-list',
optional => 1,
completion => \&PVE::Storage::complete_content_type,
},
enabled => {
description => "Only list stores which are enabled (not disabled in config).",
type => 'boolean',
optional => 1,
default => 0,
},
target => get_standard_option('pve-node', {
description => "If target is different to 'node', we only lists shared storages which " .
"content is accessible on this 'node' and the specified 'target' node.",
optional => 1,
completion => \&PVE::Cluster::get_nodelist,
}),
'format' => {
description => "Include information about formats",
type => 'boolean',
optional => 1,
default => 0,
},
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
storage => get_standard_option('pve-storage-id'),
type => {
description => "Storage type.",
type => 'string',
},
content => {
description => "Allowed storage content types.",
type => 'string', format => 'pve-storage-content-list',
},
enabled => {
description => "Set when storage is enabled (not disabled).",
type => 'boolean',
optional => 1,
},
active => {
description => "Set when storage is accessible.",
type => 'boolean',
optional => 1,
},
shared => {
description => "Shared flag from storage configuration.",
type => 'boolean',
optional => 1,
},
total => {
description => "Total storage space in bytes.",
type => 'integer',
renderer => 'bytes',
optional => 1,
},
used => {
description => "Used storage space in bytes.",
type => 'integer',
renderer => 'bytes',
optional => 1,
},
avail => {
description => "Available storage space in bytes.",
type => 'integer',
renderer => 'bytes',
optional => 1,
},
used_fraction => {
description => "Used fraction (used/total).",
type => 'number',
renderer => 'fraction_as_percentage',
optional => 1,
},
},
},
links => [ { rel => 'child', href => "{storage}" } ],
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $localnode = PVE::INotify::nodename();
my $target = $param->{target};
undef $target if $target && ($target eq $localnode || $target eq 'localhost');
my $cfg = PVE::Storage::config();
my $info = PVE::Storage::storage_info($cfg, $param->{content}, $param->{format});
raise_param_exc({ storage => "No such storage." })
if $param->{storage} && !defined($info->{$param->{storage}});
my $res = {};
my @sids = PVE::Storage::storage_ids($cfg);
foreach my $storeid (@sids) {
my $data = $info->{$storeid};
next if !$data;
my $privs = [ 'Datastore.Audit', 'Datastore.AllocateSpace' ];
next if !$rpcenv->check_any($authuser, "/storage/$storeid", $privs, 1);
next if $param->{storage} && $param->{storage} ne $storeid;
my $scfg = PVE::Storage::storage_config($cfg, $storeid);
next if $param->{enabled} && $scfg->{disable};
if ($target) {
# check if storage content is accessible on local node and specified target node
# we use this on the Clone GUI
next if !$scfg->{shared};
next if !PVE::Storage::storage_check_node($cfg, $storeid, undef, 1);
next if !PVE::Storage::storage_check_node($cfg, $storeid, $target, 1);
}
if ($data->{total}) {
$data->{used_fraction} = ($data->{used} // 0) / $data->{total};
}
$res->{$storeid} = $data;
}
return PVE::RESTHandler::hash_to_array($res, 'storage');
}});
__PACKAGE__->register_method ({
name => 'diridx',
path => '{storage}',
method => 'GET',
description => "",
permissions => {
check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
storage => get_standard_option('pve-storage-id'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
subdir => { type => 'string' },
},
},
links => [ { rel => 'child', href => "{subdir}" } ],
},
code => sub {
my ($param) = @_;
my $res = [
{ subdir => 'content' },
{ subdir => 'download-url' },
{ subdir => 'file-restore' },
{ subdir => 'prunebackups' },
{ subdir => 'rrd' },
{ subdir => 'rrddata' },
{ subdir => 'status' },
{ subdir => 'upload' },
];
return $res;
}});
__PACKAGE__->register_method ({
name => 'read_status',
path => '{storage}/status',
method => 'GET',
description => "Read storage status.",
permissions => {
check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
},
protected => 1,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
storage => get_standard_option('pve-storage-id'),
},
},
returns => {
type => "object",
properties => {},
},
code => sub {
my ($param) = @_;
my $cfg = PVE::Storage::config();
my $info = PVE::Storage::storage_info($cfg, $param->{content});
my $data = $info->{$param->{storage}};
raise_param_exc({ storage => "No such storage." })
if !defined($data);
return $data;
}});
__PACKAGE__->register_method ({
name => 'rrd',
path => '{storage}/rrd',
method => 'GET',
description => "Read storage RRD statistics (returns PNG).",
permissions => {
check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
},
protected => 1,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
storage => get_standard_option('pve-storage-id'),
timeframe => {
description => "Specify the time frame you are interested in.",
type => 'string',
enum => [ 'hour', 'day', 'week', 'month', 'year' ],
},
ds => {
description => "The list of datasources you want to display.",
type => 'string', format => 'pve-configid-list',
},
cf => {
description => "The RRD consolidation function",
type => 'string',
enum => [ 'AVERAGE', 'MAX' ],
optional => 1,
},
},
},
returns => {
type => "object",
properties => {
filename => { type => 'string' },
},
},
code => sub {
my ($param) = @_;
return PVE::RRD::create_rrd_graph(
"pve2-storage/$param->{node}/$param->{storage}",
$param->{timeframe}, $param->{ds}, $param->{cf});
}});
__PACKAGE__->register_method ({
name => 'rrddata',
path => '{storage}/rrddata',
method => 'GET',
description => "Read storage RRD statistics.",
permissions => {
check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
},
protected => 1,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
storage => get_standard_option('pve-storage-id'),
timeframe => {
description => "Specify the time frame you are interested in.",
type => 'string',
enum => [ 'hour', 'day', 'week', 'month', 'year' ],
},
cf => {
description => "The RRD consolidation function",
type => 'string',
enum => [ 'AVERAGE', 'MAX' ],
optional => 1,
},
},
},
returns => {
type => "array",
items => {
type => "object",
properties => {},
},
},
code => sub {
my ($param) = @_;
return PVE::RRD::create_rrd_data(
"pve2-storage/$param->{node}/$param->{storage}",
$param->{timeframe}, $param->{cf});
}});
# makes no sense for big images and backup files (because it
# create a copy of the file).
__PACKAGE__->register_method ({
name => 'upload',
path => '{storage}/upload',
method => 'POST',
description => "Upload templates and ISO images.",
permissions => {
check => ['perm', '/storage/{storage}', ['Datastore.AllocateTemplate']],
},
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
storage => get_standard_option('pve-storage-id'),
content => {
description => "Content type.",
type => 'string', format => 'pve-storage-content',
enum => ['iso', 'vztmpl'],
},
filename => {
description => "The name of the file to create. Caution: This will be normalized!",
maxLength => 255,
type => 'string',
},
tmpfilename => {
description => "The source file name. This parameter is usually set by the REST handler. You can only overwrite it when connecting to the trusted port on localhost.",
type => 'string',
optional => 1,
},
},
},
returns => { type => "string" },
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $cfg = PVE::Storage::config();
my $node = $param->{node};
my $scfg = PVE::Storage::storage_check_enabled($cfg, $param->{storage}, $node);
die "can't upload to storage type '$scfg->{type}'\n"
if !defined($scfg->{path});
my $content = $param->{content};
my $tmpfilename = $param->{tmpfilename};
die "missing temporary file name\n" if !$tmpfilename;
my $size = -s $tmpfilename;
die "temporary file '$tmpfilename' does not exist\n" if !defined($size);
my $filename = PVE::Storage::normalize_content_filename($param->{filename});
my $path;
if ($content eq 'iso') {
if ($filename !~ m![^/]+$PVE::Storage::iso_extension_re$!) {
raise_param_exc({ filename => "wrong file extension" });
}
$path = PVE::Storage::get_iso_dir($cfg, $param->{storage});
} elsif ($content eq 'vztmpl') {
if ($filename !~ m![^/]+$PVE::Storage::vztmpl_extension_re$!) {
raise_param_exc({ filename => "wrong file extension" });
}
$path = PVE::Storage::get_vztmpl_dir($cfg, $param->{storage});
} else {
raise_param_exc({ content => "upload content type '$content' not allowed" });
}
die "storage '$param->{storage}' does not support '$content' content\n"
if !$scfg->{content}->{$content};
my $dest = "$path/$filename";
my $dirname = dirname($dest);
# best effort to match apl_download behaviour
chmod 0644, $tmpfilename;
# we simply overwrite the destination file if it already exists
my $cmd;
my $err_cmd;
if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
my $remip = PVE::Cluster::remote_node_ip($node);
my @ssh_options = ('-o', 'BatchMode=yes');
my @remcmd = ('/usr/bin/ssh', @ssh_options, $remip, '--');
eval {
# activate remote storage
PVE::Tools::run_command([@remcmd, '/usr/sbin/pvesm', 'status',
'--storage', $param->{storage}]);
};
die "can't activate storage '$param->{storage}' on node '$node': $@\n" if $@;
PVE::Tools::run_command([@remcmd, '/bin/mkdir', '-p', '--', PVE::Tools::shell_quote($dirname)],
errmsg => "mkdir failed");
$cmd = ['/usr/bin/scp', @ssh_options, '-p', '--', $tmpfilename, "[$remip]:" . PVE::Tools::shell_quote($dest)];
$err_cmd = [@remcmd, 'unlink', '--', $dest];
} else {
PVE::Storage::activate_storage($cfg, $param->{storage});
File::Path::make_path($dirname);
$cmd = ['cp', '--', $tmpfilename, $dest];
$err_cmd = ['unlink', '--', $dest];
}
my $worker = sub {
my $upid = shift;
print "starting file import from: $tmpfilename\n";
print "target node: $node\n";
print "target file: $dest\n";
print "file size is: $size\n";
print "command: " . join(' ', @$cmd) . "\n";
eval { PVE::Tools::run_command($cmd, errmsg => 'import failed'); };
if (my $err = $@) {
eval { PVE::Tools::run_command($err_cmd); };
die $err;
}
print "finished file import successfully\n";
};
my $upid = $rpcenv->fork_worker('imgcopy', undef, $user, $worker);
# apache removes the temporary file on return, so we need
# to wait here to make sure the worker process starts and
# opens the file before it gets removed.
sleep(1);
return $upid;
}});
__PACKAGE__->register_method({
name => 'download_url',
path => '{storage}/download-url',
method => 'POST',
description => "Download templates and ISO images by using an URL.",
proxyto => 'node',
permissions => {
check => [ 'and',
['perm', '/storage/{storage}', [ 'Datastore.AllocateTemplate' ]],
['perm', '/', [ 'Sys.Audit', 'Sys.Modify' ]],
],
},
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
storage => get_standard_option('pve-storage-id'),
url => {
description => "The URL to download the file from.",
type => 'string',
pattern => 'https?://.*',
},
content => {
description => "Content type.", # TODO: could be optional & detected in most cases
type => 'string', format => 'pve-storage-content',
enum => ['iso', 'vztmpl'],
},
filename => {
description => "The name of the file to create. Caution: This will be normalized!",
maxLength => 255,
type => 'string',
},
checksum => {
description => "The expected checksum of the file.",
type => 'string',
requires => 'checksum-algorithm',
optional => 1,
},
'checksum-algorithm' => {
description => "The algorithm to calculate the checksum of the file.",
type => 'string',
enum => ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'],
requires => 'checksum',
optional => 1,
},
'verify-certificates' => {
description => "If false, no SSL/TLS certificates will be verified.",
type => 'boolean',
optional => 1,
default => 1,
},
},
},
returns => {
type => "string"
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $cfg = PVE::Storage::config();
my ($node, $storage) = $param->@{'node', 'storage'};
my $scfg = PVE::Storage::storage_check_enabled($cfg, $storage, $node);
die "can't upload to storage type '$scfg->{type}', not a file based storage!\n"
if !defined($scfg->{path});
my ($content, $url) = $param->@{'content', 'url'};
die "storage '$storage' is not configured for content-type '$content'\n"
if !$scfg->{content}->{$content};
my $filename = PVE::Storage::normalize_content_filename($param->{filename});
my $path;
if ($content eq 'iso') {
if ($filename !~ m![^/]+$PVE::Storage::iso_extension_re$!) {
raise_param_exc({ filename => "wrong file extension" });
}
$path = PVE::Storage::get_iso_dir($cfg, $storage);
} elsif ($content eq 'vztmpl') {
if ($filename !~ m![^/]+$PVE::Storage::vztmpl_extension_re$!) {
raise_param_exc({ filename => "wrong file extension" });
}
$path = PVE::Storage::get_vztmpl_dir($cfg, $storage);
} else {
raise_param_exc({ content => "upload content-type '$content' is not allowed" });
}
PVE::Storage::activate_storage($cfg, $storage);
File::Path::make_path($path);
my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
my $opts = {
hash_required => 0,
verify_certificates => $param->{'verify-certificates'} // 1,
http_proxy => $dccfg->{http_proxy},
};
my ($checksum, $checksum_algorithm) = $param->@{'checksum', 'checksum-algorithm'};
if ($checksum) {
$opts->{"${checksum_algorithm}sum"} = $checksum;
$opts->{hash_required} = 1;
}
my $worker = sub {
PVE::Tools::download_file_from_url("$path/$filename", $url, $opts);
};
my $worker_id = PVE::Tools::encode_text($filename); # must not pass : or the like as w-ID
return $rpcenv->fork_worker('download', $worker_id, $user, $worker);
}});
1;