and add the appropriate api call to set and get the comment we need to bump APIVER for this and can bump APIAGE, since we only use it at this new call that can work with the default implementation Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
533 lines
14 KiB
Perl
533 lines
14 KiB
Perl
package PVE::API2::Storage::Content;
|
|
|
|
use strict;
|
|
use warnings;
|
|
use Data::Dumper;
|
|
|
|
use PVE::SafeSyslog;
|
|
use PVE::Cluster;
|
|
use PVE::Storage;
|
|
use PVE::INotify;
|
|
use PVE::Exception qw(raise_param_exc);
|
|
use PVE::RPCEnvironment;
|
|
use PVE::RESTHandler;
|
|
use PVE::JSONSchema qw(get_standard_option);
|
|
use PVE::SSHInfo;
|
|
|
|
use base qw(PVE::RESTHandler);
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'index',
|
|
path => '',
|
|
method => 'GET',
|
|
description => "List storage content.",
|
|
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', {
|
|
completion => \&PVE::Storage::complete_storage_enabled,
|
|
}),
|
|
content => {
|
|
description => "Only list content of this type.",
|
|
type => 'string', format => 'pve-storage-content',
|
|
optional => 1,
|
|
completion => \&PVE::Storage::complete_content_type,
|
|
},
|
|
vmid => get_standard_option('pve-vmid', {
|
|
description => "Only list images for this VM",
|
|
optional => 1,
|
|
completion => \&PVE::Cluster::complete_vmid,
|
|
}),
|
|
},
|
|
},
|
|
returns => {
|
|
type => 'array',
|
|
items => {
|
|
type => "object",
|
|
properties => {
|
|
volid => {
|
|
description => "Volume identifier.",
|
|
type => 'string',
|
|
},
|
|
vmid => {
|
|
description => "Associated Owner VMID.",
|
|
type => 'integer',
|
|
optional => 1,
|
|
},
|
|
parent => {
|
|
description => "Volume identifier of parent (for linked cloned).",
|
|
type => 'string',
|
|
optional => 1,
|
|
},
|
|
'format' => {
|
|
description => "Format identifier ('raw', 'qcow2', 'subvol', 'iso', 'tgz' ...)",
|
|
type => 'string',
|
|
},
|
|
size => {
|
|
description => "Volume size in bytes.",
|
|
type => 'integer',
|
|
renderer => 'bytes',
|
|
},
|
|
used => {
|
|
description => "Used space. Please note that most storage plugins " .
|
|
"do not report anything useful here.",
|
|
type => 'integer',
|
|
renderer => 'bytes',
|
|
optional => 1,
|
|
},
|
|
ctime => {
|
|
description => "Creation time (seconds since the UNIX Epoch).",
|
|
type => 'integer',
|
|
minimum => 0,
|
|
optional => 1,
|
|
},
|
|
notes => {
|
|
description => "Optional notes. If they contain multiple lines, only the first one is returned here.",
|
|
type => 'string',
|
|
optional => 1,
|
|
},
|
|
verification => {
|
|
description => "Last backup verification result, only useful for PBS storages.",
|
|
type => 'object',
|
|
properties => {
|
|
state => {
|
|
description => "Last backup verification state.",
|
|
type => 'string',
|
|
},
|
|
upid => {
|
|
description => "Last backup verification UPID.",
|
|
type => 'string',
|
|
},
|
|
},
|
|
optional => 1,
|
|
},
|
|
},
|
|
},
|
|
links => [ { rel => 'child', href => "{volid}" } ],
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $rpcenv = PVE::RPCEnvironment::get();
|
|
|
|
my $authuser = $rpcenv->get_user();
|
|
|
|
my $storeid = $param->{storage};
|
|
|
|
my $cfg = PVE::Storage::config();
|
|
|
|
my $vollist = PVE::Storage::volume_list($cfg, $storeid, $param->{vmid}, $param->{content});
|
|
|
|
my $res = [];
|
|
foreach my $item (@$vollist) {
|
|
eval { PVE::Storage::check_volume_access($rpcenv, $authuser, $cfg, undef, $item->{volid}); };
|
|
next if $@;
|
|
$item->{vmid} = int($item->{vmid}) if (defined($item->{vmid}));
|
|
push @$res, $item;
|
|
}
|
|
|
|
return $res;
|
|
}});
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'create',
|
|
path => '',
|
|
method => 'POST',
|
|
description => "Allocate disk images.",
|
|
permissions => {
|
|
check => ['perm', '/storage/{storage}', ['Datastore.AllocateSpace']],
|
|
},
|
|
protected => 1,
|
|
proxyto => 'node',
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
node => get_standard_option('pve-node'),
|
|
storage => get_standard_option('pve-storage-id', {
|
|
completion => \&PVE::Storage::complete_storage_enabled,
|
|
}),
|
|
filename => {
|
|
description => "The name of the file to create.",
|
|
type => 'string',
|
|
},
|
|
vmid => get_standard_option('pve-vmid', {
|
|
description => "Specify owner VM",
|
|
completion => \&PVE::Cluster::complete_vmid,
|
|
}),
|
|
size => {
|
|
description => "Size in kilobyte (1024 bytes). Optional suffixes 'M' (megabyte, 1024K) and 'G' (gigabyte, 1024M)",
|
|
type => 'string',
|
|
pattern => '\d+[MG]?',
|
|
},
|
|
'format' => {
|
|
type => 'string',
|
|
enum => ['raw', 'qcow2', 'subvol'],
|
|
requires => 'size',
|
|
optional => 1,
|
|
},
|
|
},
|
|
},
|
|
returns => {
|
|
description => "Volume identifier",
|
|
type => 'string',
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $storeid = $param->{storage};
|
|
my $name = $param->{filename};
|
|
my $sizestr = $param->{size};
|
|
|
|
my $size;
|
|
if ($sizestr =~ m/^\d+$/) {
|
|
$size = $sizestr;
|
|
} elsif ($sizestr =~ m/^(\d+)M$/) {
|
|
$size = $1 * 1024;
|
|
} elsif ($sizestr =~ m/^(\d+)G$/) {
|
|
$size = $1 * 1024 * 1024;
|
|
} else {
|
|
raise_param_exc({ size => "unable to parse size '$sizestr'" });
|
|
}
|
|
|
|
# extract FORMAT from name
|
|
if ($name =~ m/\.(raw|qcow2|vmdk)$/) {
|
|
my $fmt = $1;
|
|
|
|
raise_param_exc({ format => "different storage formats ($param->{format} != $fmt)" })
|
|
if $param->{format} && $param->{format} ne $fmt;
|
|
|
|
$param->{format} = $fmt;
|
|
}
|
|
|
|
my $cfg = PVE::Storage::config();
|
|
|
|
my $volid = PVE::Storage::vdisk_alloc ($cfg, $storeid, $param->{vmid},
|
|
$param->{format},
|
|
$name, $size);
|
|
|
|
return $volid;
|
|
}});
|
|
|
|
# we allow to pass volume names (without storage prefix) if the storage
|
|
# is specified as separate parameter.
|
|
my $real_volume_id = sub {
|
|
my ($storeid, $volume) = @_;
|
|
|
|
my $volid;
|
|
|
|
if ($volume =~ m/:/) {
|
|
eval {
|
|
my ($sid, $volname) = PVE::Storage::parse_volume_id ($volume);
|
|
die "storage ID missmatch ($sid != $storeid)\n"
|
|
if $storeid && $sid ne $storeid;
|
|
$volid = $volume;
|
|
$storeid = $sid;
|
|
};
|
|
raise_param_exc({ volume => $@ }) if $@;
|
|
|
|
} else {
|
|
raise_param_exc({ volume => "no storage speficied - incomplete volume ID" })
|
|
if !$storeid;
|
|
|
|
$volid = "$storeid:$volume";
|
|
}
|
|
|
|
return wantarray ? ($volid, $storeid) : $volid;
|
|
};
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'info',
|
|
path => '{volume}',
|
|
method => 'GET',
|
|
description => "Get volume attributes",
|
|
permissions => {
|
|
description => "You need read access for the volume.",
|
|
user => 'all',
|
|
},
|
|
protected => 1,
|
|
proxyto => 'node',
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
node => get_standard_option('pve-node'),
|
|
storage => get_standard_option('pve-storage-id', { optional => 1 }),
|
|
volume => {
|
|
description => "Volume identifier",
|
|
type => 'string',
|
|
},
|
|
},
|
|
},
|
|
returns => {
|
|
type => 'object',
|
|
properties => {
|
|
path => {
|
|
description => "The Path",
|
|
type => 'string',
|
|
},
|
|
size => {
|
|
description => "Volume size in bytes.",
|
|
type => 'integer',
|
|
renderer => 'bytes',
|
|
},
|
|
used => {
|
|
description => "Used space. Please note that most storage plugins " .
|
|
"do not report anything useful here.",
|
|
type => 'integer',
|
|
renderer => 'bytes',
|
|
},
|
|
format => {
|
|
description => "Format identifier ('raw', 'qcow2', 'subvol', 'iso', 'tgz' ...)",
|
|
type => 'string',
|
|
},
|
|
notes => {
|
|
description => "Optional notes.",
|
|
optional => 1,
|
|
type => 'string',
|
|
}
|
|
},
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $rpcenv = PVE::RPCEnvironment::get();
|
|
my $authuser = $rpcenv->get_user();
|
|
|
|
my ($volid, $storeid) = &$real_volume_id($param->{storage}, $param->{volume});
|
|
|
|
my $cfg = PVE::Storage::config();
|
|
|
|
PVE::Storage::check_volume_access($rpcenv, $authuser, $cfg, undef, $volid);
|
|
|
|
my $path = PVE::Storage::path($cfg, $volid);
|
|
my ($size, $format, $used, $parent) = PVE::Storage::volume_size_info($cfg, $volid);
|
|
die "volume_size_info on '$volid' failed\n" if !($format && $size);
|
|
|
|
my $entry = {
|
|
path => $path,
|
|
size => $size,
|
|
used => $used,
|
|
format => $format,
|
|
};
|
|
|
|
# not all storages/types support notes, so ignore errors here
|
|
eval {
|
|
my $notes = PVE::Storage::get_volume_notes($cfg, $volid);
|
|
$entry->{notes} = $notes if defined($notes);
|
|
};
|
|
|
|
return $entry;
|
|
}});
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'updateattributes',
|
|
path => '{volume}',
|
|
method => 'PUT',
|
|
description => "Update volume attributes",
|
|
permissions => {
|
|
description => "You need read access for the volume.",
|
|
user => 'all',
|
|
},
|
|
protected => 1,
|
|
proxyto => 'node',
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
node => get_standard_option('pve-node'),
|
|
storage => get_standard_option('pve-storage-id', { optional => 1 }),
|
|
volume => {
|
|
description => "Volume identifier",
|
|
type => 'string',
|
|
},
|
|
notes => {
|
|
description => "The new notes.",
|
|
type => 'string',
|
|
optional => 1,
|
|
},
|
|
},
|
|
},
|
|
returns => { type => 'null' },
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $rpcenv = PVE::RPCEnvironment::get();
|
|
my $authuser = $rpcenv->get_user();
|
|
|
|
my ($volid, $storeid) = &$real_volume_id($param->{storage}, $param->{volume});
|
|
|
|
my $cfg = PVE::Storage::config();
|
|
|
|
PVE::Storage::check_volume_access($rpcenv, $authuser, $cfg, undef, $volid);
|
|
|
|
if (my $notes = $param->{notes}) {
|
|
PVE::Storage::update_volume_notes($cfg, $volid, $notes);
|
|
}
|
|
|
|
return undef;
|
|
}});
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'delete',
|
|
path => '{volume}',
|
|
method => 'DELETE',
|
|
description => "Delete volume",
|
|
permissions => {
|
|
description => "You need 'Datastore.Allocate' privilege on the storage (or 'Datastore.AllocateSpace' for backup volumes if you have VM.Backup privilege on the VM).",
|
|
user => 'all',
|
|
},
|
|
protected => 1,
|
|
proxyto => 'node',
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
node => get_standard_option('pve-node'),
|
|
storage => get_standard_option('pve-storage-id', {
|
|
optional => 1,
|
|
completion => \&PVE::Storage::complete_storage,
|
|
}),
|
|
volume => {
|
|
description => "Volume identifier",
|
|
type => 'string',
|
|
completion => \&PVE::Storage::complete_volume,
|
|
},
|
|
delay => {
|
|
type => 'integer',
|
|
description => "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
|
|
minimum => 1,
|
|
maximum => 30,
|
|
optional => 1,
|
|
},
|
|
},
|
|
},
|
|
returns => { type => 'string', optional => 1, },
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $rpcenv = PVE::RPCEnvironment::get();
|
|
my $authuser = $rpcenv->get_user();
|
|
|
|
my $cfg = PVE::Storage::config();
|
|
|
|
my ($volid, $storeid) = &$real_volume_id($param->{storage}, $param->{volume});
|
|
|
|
my ($path, $ownervm, $vtype) = PVE::Storage::path($cfg, $volid);
|
|
if ($vtype eq 'backup' && $ownervm) {
|
|
$rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
|
|
$rpcenv->check($authuser, "/vms/$ownervm", ['VM.Backup']);
|
|
} else {
|
|
$rpcenv->check($authuser, "/storage/$storeid", ['Datastore.Allocate']);
|
|
}
|
|
|
|
my $worker = sub {
|
|
PVE::Storage::vdisk_free ($cfg, $volid);
|
|
print "Removed volume '$volid'\n";
|
|
if ($vtype eq 'backup'
|
|
&& $path =~ /(.*\/vzdump-\w+-\d+-\d{4}_\d{2}_\d{2}-\d{2}_\d{2}_\d{2})[^\/]+$/) {
|
|
my $logpath = "$1.log";
|
|
# try to cleanup our backup log file too, if still exisiting, #318
|
|
unlink($logpath) if -e $logpath;
|
|
}
|
|
};
|
|
|
|
my $id = (defined $ownervm ? "$ownervm@" : '') . $storeid;
|
|
my $upid = $rpcenv->fork_worker('imgdel', $id, $authuser, $worker);
|
|
my $background_delay = $param->{delay};
|
|
if ($background_delay) {
|
|
my $end_time = time() + $background_delay;
|
|
my $currently_deleting; # not necessarily true, e.g. sequential api call from cli
|
|
do {
|
|
my $task = PVE::Tools::upid_decode($upid);
|
|
$currently_deleting = PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart});
|
|
sleep 1 if $currently_deleting;
|
|
} while (time() < $end_time && $currently_deleting);
|
|
|
|
if (!$currently_deleting) {
|
|
my $status = PVE::Tools::upid_read_status($upid);
|
|
return undef if $status eq 'OK';
|
|
die $status;
|
|
}
|
|
}
|
|
return $upid;
|
|
}});
|
|
|
|
__PACKAGE__->register_method ({
|
|
name => 'copy',
|
|
path => '{volume}',
|
|
method => 'POST',
|
|
description => "Copy a volume. This is experimental code - do not use.",
|
|
protected => 1,
|
|
proxyto => 'node',
|
|
parameters => {
|
|
additionalProperties => 0,
|
|
properties => {
|
|
node => get_standard_option('pve-node'),
|
|
storage => get_standard_option('pve-storage-id', { optional => 1}),
|
|
volume => {
|
|
description => "Source volume identifier",
|
|
type => 'string',
|
|
},
|
|
target => {
|
|
description => "Target volume identifier",
|
|
type => 'string',
|
|
},
|
|
target_node => get_standard_option('pve-node', {
|
|
description => "Target node. Default is local node.",
|
|
optional => 1,
|
|
}),
|
|
},
|
|
},
|
|
returns => {
|
|
type => 'string',
|
|
},
|
|
code => sub {
|
|
my ($param) = @_;
|
|
|
|
my $rpcenv = PVE::RPCEnvironment::get();
|
|
|
|
my $user = $rpcenv->get_user();
|
|
|
|
my $target_node = $param->{target_node} || PVE::INotify::nodename();
|
|
# pvesh examples
|
|
# cd /nodes/localhost/storage/local/content
|
|
# pve:/> create local:103/vm-103-disk-1.raw -target local:103/vm-103-disk-2.raw
|
|
# pve:/> create 103/vm-103-disk-1.raw -target 103/vm-103-disk-3.raw
|
|
|
|
my $src_volid = &$real_volume_id($param->{storage}, $param->{volume});
|
|
my $dst_volid = &$real_volume_id($param->{storage}, $param->{target});
|
|
|
|
print "DEBUG: COPY $src_volid TO $dst_volid\n";
|
|
|
|
my $cfg = PVE::Storage::config();
|
|
|
|
# do all parameter checks first
|
|
|
|
# then do all short running task (to raise errors befor we go to background)
|
|
|
|
# then start the worker task
|
|
my $worker = sub {
|
|
my $upid = shift;
|
|
|
|
print "DEBUG: starting worker $upid\n";
|
|
|
|
my ($target_sid, $target_volname) = PVE::Storage::parse_volume_id($dst_volid);
|
|
#my $target_ip = PVE::Cluster::remote_node_ip($target_node);
|
|
|
|
# you need to get this working (fails currently, because storage_migrate() uses
|
|
# ssh to connect to local host (which is not needed
|
|
my $sshinfo = PVE::SSHInfo::get_ssh_info($target_node);
|
|
PVE::Storage::storage_migrate($cfg, $src_volid, $sshinfo, $target_sid, {'target_volname' => $target_volname});
|
|
|
|
print "DEBUG: end worker $upid\n";
|
|
|
|
};
|
|
|
|
return $rpcenv->fork_worker('imgcopy', undef, $user, $worker);
|
|
}});
|
|
|
|
1;
|