separate packaging and source build system
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
318
src/PVE/API2/Disks.pm
Normal file
318
src/PVE/API2/Disks.pm
Normal file
@ -0,0 +1,318 @@
|
||||
package PVE::API2::Disks;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use File::Basename;
|
||||
use HTTP::Status qw(:constants);
|
||||
|
||||
use PVE::Diskmanage;
|
||||
use PVE::JSONSchema qw(get_standard_option);
|
||||
use PVE::SafeSyslog;
|
||||
use PVE::Tools qw(run_command);
|
||||
|
||||
use PVE::API2::Disks::Directory;
|
||||
use PVE::API2::Disks::LVM;
|
||||
use PVE::API2::Disks::LVMThin;
|
||||
use PVE::API2::Disks::ZFS;
|
||||
|
||||
use PVE::RESTHandler;
|
||||
use base qw(PVE::RESTHandler);
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
subclass => "PVE::API2::Disks::LVM",
|
||||
path => 'lvm',
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
subclass => "PVE::API2::Disks::LVMThin",
|
||||
path => 'lvmthin',
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
subclass => "PVE::API2::Disks::Directory",
|
||||
path => 'directory',
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
subclass => "PVE::API2::Disks::ZFS",
|
||||
path => 'zfs',
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'index',
|
||||
path => '',
|
||||
method => 'GET',
|
||||
proxyto => 'node',
|
||||
permissions => { user => 'all' },
|
||||
description => "Node index.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {},
|
||||
},
|
||||
links => [ { rel => 'child', href => "{name}" } ],
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $result = [
|
||||
{ name => 'list' },
|
||||
{ name => 'initgpt' },
|
||||
{ name => 'smart' },
|
||||
{ name => 'lvm' },
|
||||
{ name => 'lvmthin' },
|
||||
{ name => 'directory' },
|
||||
{ name => 'wipedisk' },
|
||||
{ name => 'zfs' },
|
||||
];
|
||||
|
||||
return $result;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'list',
|
||||
path => 'list',
|
||||
method => 'GET',
|
||||
description => "List local disks.",
|
||||
protected => 1,
|
||||
proxyto => 'node',
|
||||
permissions => {
|
||||
check => ['or',
|
||||
['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
|
||||
['perm', '/nodes/{node}', ['Sys.Audit', 'Datastore.Audit'], any => 1],
|
||||
],
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
'include-partitions' => {
|
||||
description => "Also include partitions.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
skipsmart => {
|
||||
description => "Skip smart checks.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
type => {
|
||||
description => "Only list specific types of disks.",
|
||||
type => 'string',
|
||||
enum => ['unused', 'journal_disks'],
|
||||
optional => 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => 'object',
|
||||
properties => {
|
||||
devpath => {
|
||||
type => 'string',
|
||||
description => 'The device path',
|
||||
},
|
||||
used => { type => 'string', optional => 1 },
|
||||
gpt => { type => 'boolean' },
|
||||
mounted => { type => 'boolean' },
|
||||
size => { type => 'integer'},
|
||||
osdid => { type => 'integer'},
|
||||
vendor => { type => 'string', optional => 1 },
|
||||
model => { type => 'string', optional => 1 },
|
||||
serial => { type => 'string', optional => 1 },
|
||||
wwn => { type => 'string', optional => 1},
|
||||
health => { type => 'string', optional => 1},
|
||||
parent => {
|
||||
type => 'string',
|
||||
description => 'For partitions only. The device path of ' .
|
||||
'the disk the partition resides on.',
|
||||
optional => 1
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $skipsmart = $param->{skipsmart} // 0;
|
||||
my $include_partitions = $param->{'include-partitions'} // 0;
|
||||
|
||||
my $disks = PVE::Diskmanage::get_disks(
|
||||
undef,
|
||||
$skipsmart,
|
||||
$include_partitions
|
||||
);
|
||||
|
||||
my $type = $param->{type} // '';
|
||||
my $result = [];
|
||||
|
||||
foreach my $disk (sort keys %$disks) {
|
||||
my $entry = $disks->{$disk};
|
||||
if ($type eq 'journal_disks') {
|
||||
next if $entry->{osdid} >= 0;
|
||||
if (my $usage = $entry->{used}) {
|
||||
next if !($usage eq 'partitions' && $entry->{gpt}
|
||||
|| $usage eq 'LVM');
|
||||
}
|
||||
} elsif ($type eq 'unused') {
|
||||
next if $entry->{used};
|
||||
} elsif ($type ne '') {
|
||||
die "internal error"; # should not happen
|
||||
}
|
||||
push @$result, $entry;
|
||||
}
|
||||
return $result;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'smart',
|
||||
path => 'smart',
|
||||
method => 'GET',
|
||||
description => "Get SMART Health of a disk.",
|
||||
protected => 1,
|
||||
proxyto => "node",
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
disk => {
|
||||
type => 'string',
|
||||
pattern => '^/dev/[a-zA-Z0-9\/]+$',
|
||||
description => "Block device name",
|
||||
},
|
||||
healthonly => {
|
||||
type => 'boolean',
|
||||
description => "If true returns only the health status",
|
||||
optional => 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'object',
|
||||
properties => {
|
||||
health => { type => 'string' },
|
||||
type => { type => 'string', optional => 1 },
|
||||
attributes => { type => 'array', optional => 1},
|
||||
text => { type => 'string', optional => 1 },
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $disk = PVE::Diskmanage::verify_blockdev_path($param->{disk});
|
||||
|
||||
my $result = PVE::Diskmanage::get_smart_data($disk, $param->{healthonly});
|
||||
|
||||
$result->{health} = 'UNKNOWN' if !defined $result->{health};
|
||||
$result = { health => $result->{health} } if $param->{healthonly};
|
||||
|
||||
return $result;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'initgpt',
|
||||
path => 'initgpt',
|
||||
method => 'POST',
|
||||
description => "Initialize Disk with GPT",
|
||||
protected => 1,
|
||||
proxyto => "node",
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Modify']],
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
disk => {
|
||||
type => 'string',
|
||||
description => "Block device name",
|
||||
pattern => '^/dev/[a-zA-Z0-9\/]+$',
|
||||
},
|
||||
uuid => {
|
||||
type => 'string',
|
||||
description => 'UUID for the GPT table',
|
||||
pattern => '[a-fA-F0-9\-]+',
|
||||
maxLength => 36,
|
||||
optional => 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => { type => 'string' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $disk = PVE::Diskmanage::verify_blockdev_path($param->{disk});
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
|
||||
my $authuser = $rpcenv->get_user();
|
||||
|
||||
die "$disk is a partition\n" if PVE::Diskmanage::is_partition($disk);
|
||||
die "disk $disk already in use\n" if PVE::Diskmanage::disk_is_used($disk);
|
||||
my $worker = sub {
|
||||
PVE::Diskmanage::init_disk($disk, $param->{uuid});
|
||||
};
|
||||
|
||||
my $diskid = $disk;
|
||||
$diskid =~ s|^.*/||; # remove all up to the last slash
|
||||
return $rpcenv->fork_worker('diskinit', $diskid, $authuser, $worker);
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'wipe_disk',
|
||||
path => 'wipedisk',
|
||||
method => 'PUT',
|
||||
description => "Wipe a disk or partition.",
|
||||
proxyto => 'node',
|
||||
protected => 1,
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
disk => {
|
||||
type => 'string',
|
||||
description => "Block device name",
|
||||
pattern => '^/dev/[a-zA-Z0-9\/]+$',
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => { type => 'string' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $disk = PVE::Diskmanage::verify_blockdev_path($param->{disk});
|
||||
|
||||
my $mounted = PVE::Diskmanage::is_mounted($disk);
|
||||
die "disk/partition '${mounted}' is mounted\n" if $mounted;
|
||||
|
||||
my $held = PVE::Diskmanage::has_holder($disk);
|
||||
die "disk/partition '${held}' has a holder\n" if $held;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
|
||||
my $worker = sub {
|
||||
PVE::Diskmanage::wipe_blockdev($disk);
|
||||
PVE::Diskmanage::udevadm_trigger($disk);
|
||||
};
|
||||
|
||||
my $basename = basename($disk); # avoid '/' in the ID
|
||||
|
||||
return $rpcenv->fork_worker('wipedisk', $basename, $authuser, $worker);
|
||||
}});
|
||||
|
||||
1;
|
||||
409
src/PVE/API2/Disks/Directory.pm
Normal file
409
src/PVE/API2/Disks/Directory.pm
Normal file
@ -0,0 +1,409 @@
|
||||
package PVE::API2::Disks::Directory;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use POSIX;
|
||||
|
||||
use PVE::Diskmanage;
|
||||
use PVE::JSONSchema qw(get_standard_option);
|
||||
use PVE::RESTHandler;
|
||||
use PVE::RPCEnvironment;
|
||||
use PVE::Systemd;
|
||||
use PVE::Tools qw(run_command trim file_set_contents file_get_contents dir_glob_foreach lock_file);
|
||||
|
||||
use PVE::API2::Storage::Config;
|
||||
|
||||
use base qw(PVE::RESTHandler);
|
||||
|
||||
my $SGDISK = '/sbin/sgdisk';
|
||||
my $MKFS = '/sbin/mkfs';
|
||||
my $BLKID = '/sbin/blkid';
|
||||
|
||||
my $read_ini = sub {
|
||||
my ($filename) = @_;
|
||||
|
||||
my $content = file_get_contents($filename);
|
||||
my @lines = split /\n/, $content;
|
||||
|
||||
my $result = {};
|
||||
my $section;
|
||||
|
||||
foreach my $line (@lines) {
|
||||
$line = trim($line);
|
||||
if ($line =~ m/^\[([^\]]+)\]/) {
|
||||
$section = $1;
|
||||
if (!defined($result->{$section})) {
|
||||
$result->{$section} = {};
|
||||
}
|
||||
} elsif ($line =~ m/^(.*?)=(.*)$/) {
|
||||
my ($key, $val) = ($1, $2);
|
||||
if (!$section) {
|
||||
warn "key value pair found without section, skipping\n";
|
||||
next;
|
||||
}
|
||||
|
||||
if ($result->{$section}->{$key}) {
|
||||
# make duplicate properties to arrays to keep the order
|
||||
my $prop = $result->{$section}->{$key};
|
||||
if (ref($prop) eq 'ARRAY') {
|
||||
push @$prop, $val;
|
||||
} else {
|
||||
$result->{$section}->{$key} = [$prop, $val];
|
||||
}
|
||||
} else {
|
||||
$result->{$section}->{$key} = $val;
|
||||
}
|
||||
}
|
||||
# ignore everything else
|
||||
}
|
||||
|
||||
return $result;
|
||||
};
|
||||
|
||||
my $write_ini = sub {
|
||||
my ($ini, $filename) = @_;
|
||||
|
||||
my $content = "";
|
||||
|
||||
foreach my $sname (sort keys %$ini) {
|
||||
my $section = $ini->{$sname};
|
||||
|
||||
$content .= "[$sname]\n";
|
||||
|
||||
foreach my $pname (sort keys %$section) {
|
||||
my $prop = $section->{$pname};
|
||||
|
||||
if (!ref($prop)) {
|
||||
$content .= "$pname=$prop\n";
|
||||
} elsif (ref($prop) eq 'ARRAY') {
|
||||
foreach my $val (@$prop) {
|
||||
$content .= "$pname=$val\n";
|
||||
}
|
||||
} else {
|
||||
die "invalid property '$pname'\n";
|
||||
}
|
||||
}
|
||||
$content .= "\n";
|
||||
}
|
||||
|
||||
file_set_contents($filename, $content);
|
||||
};
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'index',
|
||||
path => '',
|
||||
method => 'GET',
|
||||
proxyto => 'node',
|
||||
protected => 1,
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
|
||||
},
|
||||
description => "PVE Managed Directory storages.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => 'object',
|
||||
properties => {
|
||||
unitfile => {
|
||||
type => 'string',
|
||||
description => 'The path of the mount unit.',
|
||||
},
|
||||
path => {
|
||||
type => 'string',
|
||||
description => 'The mount path.',
|
||||
},
|
||||
device => {
|
||||
type => 'string',
|
||||
description => 'The mounted device.',
|
||||
},
|
||||
type => {
|
||||
type => 'string',
|
||||
description => 'The filesystem type.',
|
||||
},
|
||||
options => {
|
||||
type => 'string',
|
||||
description => 'The mount options.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $result = [];
|
||||
|
||||
dir_glob_foreach('/etc/systemd/system', '^mnt-pve-(.+)\.mount$', sub {
|
||||
my ($filename, $storid) = @_;
|
||||
$storid = PVE::Systemd::unescape_unit($storid);
|
||||
|
||||
my $unitfile = "/etc/systemd/system/$filename";
|
||||
my $unit = $read_ini->($unitfile);
|
||||
|
||||
push @$result, {
|
||||
unitfile => $unitfile,
|
||||
path => "/mnt/pve/$storid",
|
||||
device => $unit->{'Mount'}->{'What'},
|
||||
type => $unit->{'Mount'}->{'Type'},
|
||||
options => $unit->{'Mount'}->{'Options'},
|
||||
};
|
||||
});
|
||||
|
||||
return $result;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'create',
|
||||
path => '',
|
||||
method => 'POST',
|
||||
proxyto => 'node',
|
||||
protected => 1,
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
|
||||
},
|
||||
description => "Create a Filesystem on an unused disk. Will be mounted under '/mnt/pve/NAME'.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
name => get_standard_option('pve-storage-id'),
|
||||
device => {
|
||||
type => 'string',
|
||||
description => 'The block device you want to create the filesystem on.',
|
||||
},
|
||||
add_storage => {
|
||||
description => "Configure storage using the directory.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
filesystem => {
|
||||
description => "The desired filesystem.",
|
||||
type => 'string',
|
||||
enum => ['ext4', 'xfs'],
|
||||
optional => 1,
|
||||
default => 'ext4',
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => { type => 'string' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $user = $rpcenv->get_user();
|
||||
|
||||
my $name = $param->{name};
|
||||
my $dev = $param->{device};
|
||||
my $node = $param->{node};
|
||||
my $type = $param->{filesystem} // 'ext4';
|
||||
my $path = "/mnt/pve/$name";
|
||||
my $mountunitname = PVE::Systemd::escape_unit($path, 1) . ".mount";
|
||||
my $mountunitpath = "/etc/systemd/system/$mountunitname";
|
||||
|
||||
$dev = PVE::Diskmanage::verify_blockdev_path($dev);
|
||||
PVE::Diskmanage::assert_disk_unused($dev);
|
||||
|
||||
my $storage_params = {
|
||||
type => 'dir',
|
||||
storage => $name,
|
||||
content => 'rootdir,images,iso,backup,vztmpl,snippets',
|
||||
is_mountpoint => 1,
|
||||
path => $path,
|
||||
nodes => $node,
|
||||
};
|
||||
my $verify_params = [qw(path)];
|
||||
|
||||
if ($param->{add_storage}) {
|
||||
PVE::API2::Storage::Config->create_or_update(
|
||||
$name,
|
||||
$node,
|
||||
$storage_params,
|
||||
$verify_params,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
my $mounted = PVE::Diskmanage::mounted_paths();
|
||||
die "the path for '${name}' is already mounted: ${path} ($mounted->{$path})\n"
|
||||
if $mounted->{$path};
|
||||
die "a systemd mount unit already exists: ${mountunitpath}\n" if -e $mountunitpath;
|
||||
|
||||
my $worker = sub {
|
||||
PVE::Diskmanage::locked_disk_action(sub {
|
||||
PVE::Diskmanage::assert_disk_unused($dev);
|
||||
|
||||
my $part = $dev;
|
||||
|
||||
if (PVE::Diskmanage::is_partition($dev)) {
|
||||
eval { PVE::Diskmanage::change_parttype($dev, '8300'); };
|
||||
warn $@ if $@;
|
||||
} else {
|
||||
# create partition
|
||||
my $cmd = [$SGDISK, '-n1', '-t1:8300', $dev];
|
||||
print "# ", join(' ', @$cmd), "\n";
|
||||
run_command($cmd);
|
||||
|
||||
my ($devname) = $dev =~ m|^/dev/(.*)$|;
|
||||
$part = "/dev/";
|
||||
dir_glob_foreach("/sys/block/$devname", qr/\Q$devname\E.+/, sub {
|
||||
my ($partition) = @_;
|
||||
$part .= $partition;
|
||||
});
|
||||
}
|
||||
|
||||
# create filesystem
|
||||
my $cmd = [$MKFS, '-t', $type, $part];
|
||||
print "# ", join(' ', @$cmd), "\n";
|
||||
run_command($cmd);
|
||||
|
||||
# create systemd mount unit and enable & start it
|
||||
my $ini = {
|
||||
'Unit' => {
|
||||
'Description' => "Mount storage '$name' under /mnt/pve",
|
||||
},
|
||||
'Install' => {
|
||||
'WantedBy' => 'multi-user.target',
|
||||
},
|
||||
};
|
||||
|
||||
my $uuid_path;
|
||||
my $uuid;
|
||||
|
||||
$cmd = [$BLKID, $part, '-o', 'export'];
|
||||
print "# ", join(' ', @$cmd), "\n";
|
||||
run_command($cmd, outfunc => sub {
|
||||
my ($line) = @_;
|
||||
|
||||
if ($line =~ m/^UUID=(.*)$/) {
|
||||
$uuid = $1;
|
||||
$uuid_path = "/dev/disk/by-uuid/$uuid";
|
||||
}
|
||||
});
|
||||
|
||||
die "could not get UUID of device '$part'\n" if !$uuid;
|
||||
|
||||
$ini->{'Mount'} = {
|
||||
'What' => $uuid_path,
|
||||
'Where' => $path,
|
||||
'Type' => $type,
|
||||
'Options' => 'defaults',
|
||||
};
|
||||
|
||||
$write_ini->($ini, $mountunitpath);
|
||||
|
||||
PVE::Diskmanage::udevadm_trigger($part);
|
||||
|
||||
run_command(['systemctl', 'daemon-reload']);
|
||||
run_command(['systemctl', 'enable', $mountunitname]);
|
||||
run_command(['systemctl', 'start', $mountunitname]);
|
||||
|
||||
if ($param->{add_storage}) {
|
||||
PVE::API2::Storage::Config->create_or_update(
|
||||
$name,
|
||||
$node,
|
||||
$storage_params,
|
||||
$verify_params,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return $rpcenv->fork_worker('dircreate', $name, $user, $worker);
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'delete',
|
||||
path => '{name}',
|
||||
method => 'DELETE',
|
||||
proxyto => 'node',
|
||||
protected => 1,
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
|
||||
},
|
||||
description => "Unmounts the storage and removes the mount unit.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
name => get_standard_option('pve-storage-id'),
|
||||
'cleanup-config' => {
|
||||
description => "Marks associated storage(s) as not available on this node anymore ".
|
||||
"or removes them from the configuration (if configured for this node only).",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
'cleanup-disks' => {
|
||||
description => "Also wipe disk so it can be repurposed afterwards.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => { type => 'string' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $user = $rpcenv->get_user();
|
||||
|
||||
my $name = $param->{name};
|
||||
my $node = $param->{node};
|
||||
|
||||
my $worker = sub {
|
||||
my $path = "/mnt/pve/$name";
|
||||
my $mountunitname = PVE::Systemd::escape_unit($path, 1) . ".mount";
|
||||
my $mountunitpath = "/etc/systemd/system/$mountunitname";
|
||||
|
||||
PVE::Diskmanage::locked_disk_action(sub {
|
||||
my $to_wipe;
|
||||
if ($param->{'cleanup-disks'}) {
|
||||
my $unit = $read_ini->($mountunitpath);
|
||||
|
||||
my $dev = PVE::Diskmanage::verify_blockdev_path($unit->{'Mount'}->{'What'});
|
||||
$to_wipe = $dev;
|
||||
|
||||
# clean up whole device if this is the only partition
|
||||
$dev =~ s|^/dev/||;
|
||||
my $info = PVE::Diskmanage::get_disks($dev, 1, 1);
|
||||
die "unable to obtain information for disk '$dev'\n" if !$info->{$dev};
|
||||
$to_wipe = $info->{$dev}->{parent}
|
||||
if $info->{$dev}->{parent} && scalar(keys $info->%*) == 2;
|
||||
}
|
||||
|
||||
run_command(['systemctl', 'stop', $mountunitname]);
|
||||
run_command(['systemctl', 'disable', $mountunitname]);
|
||||
|
||||
unlink $mountunitpath or $! == ENOENT or die "cannot remove $mountunitpath - $!\n";
|
||||
|
||||
my $config_err;
|
||||
if ($param->{'cleanup-config'}) {
|
||||
my $match = sub {
|
||||
my ($scfg) = @_;
|
||||
return $scfg->{type} eq 'dir' && $scfg->{path} eq $path;
|
||||
};
|
||||
eval { PVE::API2::Storage::Config->cleanup_storages_for_node($match, $node); };
|
||||
warn $config_err = $@ if $@;
|
||||
}
|
||||
|
||||
if ($to_wipe) {
|
||||
PVE::Diskmanage::wipe_blockdev($to_wipe);
|
||||
PVE::Diskmanage::udevadm_trigger($to_wipe);
|
||||
}
|
||||
|
||||
die "config cleanup failed - $config_err" if $config_err;
|
||||
});
|
||||
};
|
||||
|
||||
return $rpcenv->fork_worker('dirremove', $name, $user, $worker);
|
||||
}});
|
||||
|
||||
1;
|
||||
281
src/PVE/API2/Disks/LVM.pm
Normal file
281
src/PVE/API2/Disks/LVM.pm
Normal file
@ -0,0 +1,281 @@
|
||||
package PVE::API2::Disks::LVM;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use PVE::Storage::LVMPlugin;
|
||||
use PVE::Diskmanage;
|
||||
use PVE::JSONSchema qw(get_standard_option);
|
||||
use PVE::API2::Storage::Config;
|
||||
use PVE::Tools qw(lock_file run_command);
|
||||
|
||||
use PVE::RPCEnvironment;
|
||||
use PVE::RESTHandler;
|
||||
|
||||
use base qw(PVE::RESTHandler);
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'index',
|
||||
path => '',
|
||||
method => 'GET',
|
||||
proxyto => 'node',
|
||||
protected => 1,
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
|
||||
},
|
||||
description => "List LVM Volume Groups",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'object',
|
||||
properties => {
|
||||
leaf => {
|
||||
type => 'boolean',
|
||||
},
|
||||
children => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
leaf => {
|
||||
type => 'boolean',
|
||||
},
|
||||
name => {
|
||||
type => 'string',
|
||||
description => 'The name of the volume group',
|
||||
},
|
||||
size => {
|
||||
type => 'integer',
|
||||
description => 'The size of the volume group in bytes',
|
||||
},
|
||||
free => {
|
||||
type => 'integer',
|
||||
description => 'The free bytes in the volume group',
|
||||
},
|
||||
children => {
|
||||
optional => 1,
|
||||
type => 'array',
|
||||
description => 'The underlying physical volumes',
|
||||
items => {
|
||||
type => 'object',
|
||||
properties => {
|
||||
leaf => {
|
||||
type => 'boolean',
|
||||
},
|
||||
name => {
|
||||
type => 'string',
|
||||
description => 'The name of the physical volume',
|
||||
},
|
||||
size => {
|
||||
type => 'integer',
|
||||
description => 'The size of the physical volume in bytes',
|
||||
},
|
||||
free => {
|
||||
type => 'integer',
|
||||
description => 'The free bytes in the physical volume',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $result = [];
|
||||
|
||||
my $vgs = PVE::Storage::LVMPlugin::lvm_vgs(1);
|
||||
|
||||
foreach my $vg_name (sort keys %$vgs) {
|
||||
my $vg = $vgs->{$vg_name};
|
||||
$vg->{name} = $vg_name;
|
||||
$vg->{leaf} = 0;
|
||||
foreach my $pv (@{$vg->{pvs}}) {
|
||||
$pv->{leaf} = 1;
|
||||
}
|
||||
$vg->{children} = delete $vg->{pvs};
|
||||
push @$result, $vg;
|
||||
}
|
||||
|
||||
return {
|
||||
leaf => 0,
|
||||
children => $result,
|
||||
};
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'create',
|
||||
path => '',
|
||||
method => 'POST',
|
||||
proxyto => 'node',
|
||||
protected => 1,
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
|
||||
},
|
||||
description => "Create an LVM Volume Group",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
name => get_standard_option('pve-storage-id'),
|
||||
device => {
|
||||
type => 'string',
|
||||
description => 'The block device you want to create the volume group on',
|
||||
},
|
||||
add_storage => {
|
||||
description => "Configure storage using the Volume Group",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => { type => 'string' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $user = $rpcenv->get_user();
|
||||
|
||||
my $name = $param->{name};
|
||||
my $dev = $param->{device};
|
||||
my $node = $param->{node};
|
||||
|
||||
$dev = PVE::Diskmanage::verify_blockdev_path($dev);
|
||||
PVE::Diskmanage::assert_disk_unused($dev);
|
||||
|
||||
my $storage_params = {
|
||||
type => 'lvm',
|
||||
vgname => $name,
|
||||
storage => $name,
|
||||
content => 'rootdir,images',
|
||||
shared => 0,
|
||||
nodes => $node,
|
||||
};
|
||||
my $verify_params = [qw(vgname)];
|
||||
|
||||
if ($param->{add_storage}) {
|
||||
PVE::API2::Storage::Config->create_or_update(
|
||||
$name,
|
||||
$node,
|
||||
$storage_params,
|
||||
$verify_params,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
my $worker = sub {
|
||||
PVE::Diskmanage::locked_disk_action(sub {
|
||||
PVE::Diskmanage::assert_disk_unused($dev);
|
||||
die "volume group with name '${name}' already exists on node '${node}'\n"
|
||||
if PVE::Storage::LVMPlugin::lvm_vgs()->{$name};
|
||||
|
||||
if (PVE::Diskmanage::is_partition($dev)) {
|
||||
eval { PVE::Diskmanage::change_parttype($dev, '8E00'); };
|
||||
warn $@ if $@;
|
||||
}
|
||||
|
||||
PVE::Storage::LVMPlugin::lvm_create_volume_group($dev, $name);
|
||||
|
||||
PVE::Diskmanage::udevadm_trigger($dev);
|
||||
|
||||
if ($param->{add_storage}) {
|
||||
PVE::API2::Storage::Config->create_or_update(
|
||||
$name,
|
||||
$node,
|
||||
$storage_params,
|
||||
$verify_params,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return $rpcenv->fork_worker('lvmcreate', $name, $user, $worker);
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'delete',
|
||||
path => '{name}',
|
||||
method => 'DELETE',
|
||||
proxyto => 'node',
|
||||
protected => 1,
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
|
||||
},
|
||||
description => "Remove an LVM Volume Group.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
name => get_standard_option('pve-storage-id'),
|
||||
'cleanup-config' => {
|
||||
description => "Marks associated storage(s) as not available on this node anymore ".
|
||||
"or removes them from the configuration (if configured for this node only).",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
'cleanup-disks' => {
|
||||
description => "Also wipe disks so they can be repurposed afterwards.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => { type => 'string' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $user = $rpcenv->get_user();
|
||||
|
||||
my $name = $param->{name};
|
||||
my $node = $param->{node};
|
||||
|
||||
my $worker = sub {
|
||||
PVE::Diskmanage::locked_disk_action(sub {
|
||||
my $vgs = PVE::Storage::LVMPlugin::lvm_vgs(1);
|
||||
die "no such volume group '$name'\n" if !$vgs->{$name};
|
||||
|
||||
PVE::Storage::LVMPlugin::lvm_destroy_volume_group($name);
|
||||
|
||||
my $config_err;
|
||||
if ($param->{'cleanup-config'}) {
|
||||
my $match = sub {
|
||||
my ($scfg) = @_;
|
||||
return $scfg->{type} eq 'lvm' && $scfg->{vgname} eq $name;
|
||||
};
|
||||
eval { PVE::API2::Storage::Config->cleanup_storages_for_node($match, $node); };
|
||||
warn $config_err = $@ if $@;
|
||||
}
|
||||
|
||||
if ($param->{'cleanup-disks'}) {
|
||||
my $wiped = [];
|
||||
eval {
|
||||
for my $pv ($vgs->{$name}->{pvs}->@*) {
|
||||
my $dev = PVE::Diskmanage::verify_blockdev_path($pv->{name});
|
||||
PVE::Diskmanage::wipe_blockdev($dev);
|
||||
push $wiped->@*, $dev;
|
||||
}
|
||||
};
|
||||
my $err = $@;
|
||||
PVE::Diskmanage::udevadm_trigger($wiped->@*);
|
||||
die "cleanup failed - $err" if $err;
|
||||
}
|
||||
|
||||
die "config cleanup failed - $config_err" if $config_err;
|
||||
});
|
||||
};
|
||||
|
||||
return $rpcenv->fork_worker('lvmremove', $name, $user, $worker);
|
||||
}});
|
||||
|
||||
1;
|
||||
271
src/PVE/API2/Disks/LVMThin.pm
Normal file
271
src/PVE/API2/Disks/LVMThin.pm
Normal file
@ -0,0 +1,271 @@
|
||||
package PVE::API2::Disks::LVMThin;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use PVE::Storage::LvmThinPlugin;
|
||||
use PVE::Diskmanage;
|
||||
use PVE::JSONSchema qw(get_standard_option);
|
||||
use PVE::API2::Storage::Config;
|
||||
use PVE::Storage;
|
||||
use PVE::Tools qw(run_command lock_file);
|
||||
|
||||
use PVE::RPCEnvironment;
|
||||
use PVE::RESTHandler;
|
||||
|
||||
use base qw(PVE::RESTHandler);
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'index',
|
||||
path => '',
|
||||
method => 'GET',
|
||||
proxyto => 'node',
|
||||
protected => 1,
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
|
||||
},
|
||||
description => "List LVM thinpools",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => 'object',
|
||||
properties => {
|
||||
lv => {
|
||||
type => 'string',
|
||||
description => 'The name of the thinpool.',
|
||||
},
|
||||
vg => {
|
||||
type => 'string',
|
||||
description => 'The associated volume group.',
|
||||
},
|
||||
lv_size => {
|
||||
type => 'integer',
|
||||
description => 'The size of the thinpool in bytes.',
|
||||
},
|
||||
used => {
|
||||
type => 'integer',
|
||||
description => 'The used bytes of the thinpool.',
|
||||
},
|
||||
metadata_size => {
|
||||
type => 'integer',
|
||||
description => 'The size of the metadata lv in bytes.',
|
||||
},
|
||||
metadata_used => {
|
||||
type => 'integer',
|
||||
description => 'The used bytes of the metadata lv.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
return PVE::Storage::LvmThinPlugin::list_thinpools(undef);
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'create',
|
||||
path => '',
|
||||
method => 'POST',
|
||||
proxyto => 'node',
|
||||
protected => 1,
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
|
||||
},
|
||||
description => "Create an LVM thinpool",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
name => get_standard_option('pve-storage-id'),
|
||||
device => {
|
||||
type => 'string',
|
||||
description => 'The block device you want to create the thinpool on.',
|
||||
},
|
||||
add_storage => {
|
||||
description => "Configure storage using the thinpool.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => { type => 'string' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $user = $rpcenv->get_user();
|
||||
|
||||
my $name = $param->{name};
|
||||
my $dev = $param->{device};
|
||||
my $node = $param->{node};
|
||||
|
||||
$dev = PVE::Diskmanage::verify_blockdev_path($dev);
|
||||
PVE::Diskmanage::assert_disk_unused($dev);
|
||||
|
||||
my $storage_params = {
|
||||
type => 'lvmthin',
|
||||
vgname => $name,
|
||||
thinpool => $name,
|
||||
storage => $name,
|
||||
content => 'rootdir,images',
|
||||
nodes => $node,
|
||||
};
|
||||
my $verify_params = [qw(vgname thinpool)];
|
||||
|
||||
if ($param->{add_storage}) {
|
||||
PVE::API2::Storage::Config->create_or_update(
|
||||
$name,
|
||||
$node,
|
||||
$storage_params,
|
||||
$verify_params,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
my $worker = sub {
|
||||
PVE::Diskmanage::locked_disk_action(sub {
|
||||
PVE::Diskmanage::assert_disk_unused($dev);
|
||||
|
||||
die "volume group with name '${name}' already exists on node '${node}'\n"
|
||||
if PVE::Storage::LVMPlugin::lvm_vgs()->{$name};
|
||||
|
||||
if (PVE::Diskmanage::is_partition($dev)) {
|
||||
eval { PVE::Diskmanage::change_parttype($dev, '8E00'); };
|
||||
warn $@ if $@;
|
||||
}
|
||||
|
||||
PVE::Storage::LVMPlugin::lvm_create_volume_group($dev, $name);
|
||||
my $pv = PVE::Storage::LVMPlugin::lvm_pv_info($dev);
|
||||
# keep some free space just in case
|
||||
my $datasize = $pv->{size} - 128*1024;
|
||||
# default to 1% for metadata
|
||||
my $metadatasize = $datasize/100;
|
||||
# but at least 1G, as recommended in lvmthin man
|
||||
$metadatasize = 1024*1024 if $metadatasize < 1024*1024;
|
||||
# but at most 16G, which is the current lvm max
|
||||
$metadatasize = 16*1024*1024 if $metadatasize > 16*1024*1024;
|
||||
# shrink data by needed amount for metadata
|
||||
$datasize -= 2*$metadatasize;
|
||||
|
||||
run_command([
|
||||
'/sbin/lvcreate',
|
||||
'--type', 'thin-pool',
|
||||
"-L${datasize}K",
|
||||
'--poolmetadatasize', "${metadatasize}K",
|
||||
'-n', $name,
|
||||
$name
|
||||
]);
|
||||
|
||||
PVE::Diskmanage::udevadm_trigger($dev);
|
||||
|
||||
if ($param->{add_storage}) {
|
||||
PVE::API2::Storage::Config->create_or_update(
|
||||
$name,
|
||||
$node,
|
||||
$storage_params,
|
||||
$verify_params,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return $rpcenv->fork_worker('lvmthincreate', $name, $user, $worker);
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'delete',
|
||||
path => '{name}',
|
||||
method => 'DELETE',
|
||||
proxyto => 'node',
|
||||
protected => 1,
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
|
||||
},
|
||||
description => "Remove an LVM thin pool.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
name => get_standard_option('pve-storage-id'),
|
||||
'volume-group' => get_standard_option('pve-storage-id'),
|
||||
'cleanup-config' => {
|
||||
description => "Marks associated storage(s) as not available on this node anymore ".
|
||||
"or removes them from the configuration (if configured for this node only).",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
'cleanup-disks' => {
|
||||
description => "Also wipe disks so they can be repurposed afterwards.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => { type => 'string' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $user = $rpcenv->get_user();
|
||||
|
||||
my $vg = $param->{'volume-group'};
|
||||
my $lv = $param->{name};
|
||||
my $node = $param->{node};
|
||||
|
||||
my $worker = sub {
|
||||
PVE::Diskmanage::locked_disk_action(sub {
|
||||
my $thinpools = PVE::Storage::LvmThinPlugin::list_thinpools();
|
||||
|
||||
die "no such thin pool ${vg}/${lv}\n"
|
||||
if !grep { $_->{lv} eq $lv && $_->{vg} eq $vg } $thinpools->@*;
|
||||
|
||||
run_command(['lvremove', '-y', "${vg}/${lv}"]);
|
||||
|
||||
my $config_err;
|
||||
if ($param->{'cleanup-config'}) {
|
||||
my $match = sub {
|
||||
my ($scfg) = @_;
|
||||
return $scfg->{type} eq 'lvmthin'
|
||||
&& $scfg->{vgname} eq $vg
|
||||
&& $scfg->{thinpool} eq $lv;
|
||||
};
|
||||
eval { PVE::API2::Storage::Config->cleanup_storages_for_node($match, $node); };
|
||||
warn $config_err = $@ if $@;
|
||||
}
|
||||
|
||||
if ($param->{'cleanup-disks'}) {
|
||||
my $vgs = PVE::Storage::LVMPlugin::lvm_vgs(1);
|
||||
|
||||
die "no such volume group '$vg'\n" if !$vgs->{$vg};
|
||||
die "volume group '$vg' still in use\n" if $vgs->{$vg}->{lvcount} > 0;
|
||||
|
||||
my $wiped = [];
|
||||
eval {
|
||||
for my $pv ($vgs->{$vg}->{pvs}->@*) {
|
||||
my $dev = PVE::Diskmanage::verify_blockdev_path($pv->{name});
|
||||
PVE::Diskmanage::wipe_blockdev($dev);
|
||||
push $wiped->@*, $dev;
|
||||
}
|
||||
};
|
||||
my $err = $@;
|
||||
PVE::Diskmanage::udevadm_trigger($wiped->@*);
|
||||
die "cleanup failed - $err" if $err;
|
||||
}
|
||||
|
||||
die "config cleanup failed - $config_err" if $config_err;
|
||||
});
|
||||
};
|
||||
|
||||
return $rpcenv->fork_worker('lvmthinremove', "${vg}-${lv}", $user, $worker);
|
||||
}});
|
||||
|
||||
1;
|
||||
9
src/PVE/API2/Disks/Makefile
Normal file
9
src/PVE/API2/Disks/Makefile
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
SOURCES= LVM.pm\
|
||||
LVMThin.pm\
|
||||
ZFS.pm\
|
||||
Directory.pm
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/API2/Disks/$$i; done
|
||||
612
src/PVE/API2/Disks/ZFS.pm
Normal file
612
src/PVE/API2/Disks/ZFS.pm
Normal file
@ -0,0 +1,612 @@
|
||||
package PVE::API2::Disks::ZFS;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use PVE::Diskmanage;
|
||||
use PVE::JSONSchema qw(get_standard_option parse_property_string);
|
||||
use PVE::Systemd;
|
||||
use PVE::API2::Storage::Config;
|
||||
use PVE::Storage;
|
||||
use PVE::Tools qw(run_command lock_file trim);
|
||||
|
||||
use PVE::RPCEnvironment;
|
||||
use PVE::RESTHandler;
|
||||
|
||||
use base qw(PVE::RESTHandler);
|
||||
|
||||
my $ZPOOL = '/sbin/zpool';
|
||||
my $ZFS = '/sbin/zfs';
|
||||
|
||||
sub get_pool_data {
|
||||
die "zfsutils-linux not installed\n" if ! -f $ZPOOL;
|
||||
|
||||
my $propnames = [qw(name size alloc free frag dedup health)];
|
||||
my $numbers = {
|
||||
size => 1,
|
||||
alloc => 1,
|
||||
free => 1,
|
||||
frag => 1,
|
||||
dedup => 1,
|
||||
};
|
||||
|
||||
my $pools = [];
|
||||
run_command([$ZPOOL, 'list', '-HpPLo', join(',', @$propnames)], outfunc => sub {
|
||||
my ($line) = @_;
|
||||
|
||||
my @props = split('\s+', trim($line));
|
||||
my $pool = {};
|
||||
for (my $i = 0; $i < scalar(@$propnames); $i++) {
|
||||
if ($numbers->{$propnames->[$i]}) {
|
||||
$pool->{$propnames->[$i]} = $props[$i] + 0;
|
||||
} else {
|
||||
$pool->{$propnames->[$i]} = $props[$i];
|
||||
}
|
||||
}
|
||||
|
||||
push @$pools, $pool;
|
||||
});
|
||||
|
||||
return $pools;
|
||||
}
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'index',
|
||||
path => '',
|
||||
method => 'GET',
|
||||
proxyto => 'node',
|
||||
protected => 1,
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
|
||||
},
|
||||
description => "List Zpools.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => 'object',
|
||||
properties => {
|
||||
name => {
|
||||
type => 'string',
|
||||
description => "",
|
||||
},
|
||||
size => {
|
||||
type => 'integer',
|
||||
description => "",
|
||||
},
|
||||
alloc => {
|
||||
type => 'integer',
|
||||
description => "",
|
||||
},
|
||||
free => {
|
||||
type => 'integer',
|
||||
description => "",
|
||||
},
|
||||
frag => {
|
||||
type => 'integer',
|
||||
description => "",
|
||||
},
|
||||
dedup => {
|
||||
type => 'number',
|
||||
description => "",
|
||||
},
|
||||
health => {
|
||||
type => 'string',
|
||||
description => "",
|
||||
},
|
||||
},
|
||||
},
|
||||
links => [ { rel => 'child', href => "{name}" } ],
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
return get_pool_data();
|
||||
}});
|
||||
|
||||
sub preparetree {
|
||||
my ($el) = @_;
|
||||
delete $el->{lvl};
|
||||
if ($el->{children} && scalar(@{$el->{children}})) {
|
||||
$el->{leaf} = 0;
|
||||
foreach my $child (@{$el->{children}}) {
|
||||
preparetree($child);
|
||||
}
|
||||
} else {
|
||||
$el->{leaf} = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'detail',
|
||||
path => '{name}',
|
||||
method => 'GET',
|
||||
proxyto => 'node',
|
||||
protected => 1,
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
|
||||
},
|
||||
description => "Get details about a zpool.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
name => get_standard_option('pve-storage-id'),
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'object',
|
||||
properties => {
|
||||
name => {
|
||||
type => 'string',
|
||||
description => 'The name of the zpool.',
|
||||
},
|
||||
state => {
|
||||
type => 'string',
|
||||
description => 'The state of the zpool.',
|
||||
},
|
||||
status => {
|
||||
optional => 1,
|
||||
type => 'string',
|
||||
description => 'Information about the state of the zpool.',
|
||||
},
|
||||
action => {
|
||||
optional => 1,
|
||||
type => 'string',
|
||||
description => 'Information about the recommended action to fix the state.',
|
||||
},
|
||||
scan => {
|
||||
optional => 1,
|
||||
type => 'string',
|
||||
description => 'Information about the last/current scrub.',
|
||||
},
|
||||
errors => {
|
||||
type => 'string',
|
||||
description => 'Information about the errors on the zpool.',
|
||||
},
|
||||
children => {
|
||||
type => 'array',
|
||||
description => "The pool configuration information, including the vdevs for each section (e.g. spares, cache), may be nested.",
|
||||
items => {
|
||||
type => 'object',
|
||||
properties => {
|
||||
name => {
|
||||
type => 'string',
|
||||
description => 'The name of the vdev or section.',
|
||||
},
|
||||
state => {
|
||||
optional => 1,
|
||||
type => 'string',
|
||||
description => 'The state of the vdev.',
|
||||
},
|
||||
read => {
|
||||
optional => 1,
|
||||
type => 'number',
|
||||
},
|
||||
write => {
|
||||
optional => 1,
|
||||
type => 'number',
|
||||
},
|
||||
cksum => {
|
||||
optional => 1,
|
||||
type => 'number',
|
||||
},
|
||||
msg => {
|
||||
type => 'string',
|
||||
description => 'An optional message about the vdev.'
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
if (!-f $ZPOOL) {
|
||||
die "zfsutils-linux not installed\n";
|
||||
}
|
||||
|
||||
my $cmd = [$ZPOOL, 'status', '-P', $param->{name}];
|
||||
|
||||
my $pool = {
|
||||
lvl => 0,
|
||||
};
|
||||
|
||||
my $curfield;
|
||||
my $config = 0;
|
||||
|
||||
my $stack = [$pool];
|
||||
my $curlvl = 0;
|
||||
|
||||
run_command($cmd, outfunc => sub {
|
||||
my ($line) = @_;
|
||||
|
||||
if ($line =~ m/^\s*(\S+): (\S+.*)$/) {
|
||||
$curfield = $1;
|
||||
$pool->{$curfield} = $2;
|
||||
|
||||
$config = 0 if $curfield eq 'errors';
|
||||
} elsif (!$config && $line =~ m/^\s+(\S+.*)$/) {
|
||||
$pool->{$curfield} .= " " . $1;
|
||||
} elsif (!$config && $line =~ m/^\s*config:/) {
|
||||
$config = 1;
|
||||
} elsif ($config && $line =~ m/^(\s+)(\S+)\s*(\S+)?(?:\s+(\S+)\s+(\S+)\s+(\S+))?\s*(.*)$/) {
|
||||
my ($space, $name, $state, $read, $write, $cksum, $msg) = ($1, $2, $3, $4, $5, $6, $7);
|
||||
if ($name ne "NAME") {
|
||||
my $lvl = int(length($space) / 2) + 1; # two spaces per level
|
||||
my $vdev = {
|
||||
name => $name,
|
||||
msg => $msg,
|
||||
lvl => $lvl,
|
||||
};
|
||||
|
||||
$vdev->{state} = $state if defined($state);
|
||||
$vdev->{read} = $read + 0 if defined($read);
|
||||
$vdev->{write} = $write + 0 if defined($write);
|
||||
$vdev->{cksum} = $cksum + 0 if defined($cksum);
|
||||
|
||||
my $cur = pop @$stack;
|
||||
|
||||
if ($lvl > $curlvl) {
|
||||
$cur->{children} = [ $vdev ];
|
||||
} elsif ($lvl == $curlvl) {
|
||||
$cur = pop @$stack;
|
||||
push @{$cur->{children}}, $vdev;
|
||||
} else {
|
||||
while ($lvl <= $cur->{lvl} && $cur->{lvl} != 0) {
|
||||
$cur = pop @$stack;
|
||||
}
|
||||
push @{$cur->{children}}, $vdev;
|
||||
}
|
||||
|
||||
push @$stack, $cur;
|
||||
push @$stack, $vdev;
|
||||
$curlvl = $lvl;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
# change treenodes for extjs tree
|
||||
$pool->{name} = delete $pool->{pool};
|
||||
preparetree($pool);
|
||||
|
||||
return $pool;
|
||||
}});
|
||||
|
||||
my $draid_config_format = {
|
||||
spares => {
|
||||
type => 'integer',
|
||||
minimum => 0,
|
||||
description => 'Number of dRAID spares.',
|
||||
},
|
||||
data => {
|
||||
type => 'integer',
|
||||
minimum => 1,
|
||||
description => 'The number of data devices per redundancy group. (dRAID)',
|
||||
},
|
||||
};
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'create',
|
||||
path => '',
|
||||
method => 'POST',
|
||||
proxyto => 'node',
|
||||
protected => 1,
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
|
||||
},
|
||||
description => "Create a ZFS pool.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
name => get_standard_option('pve-storage-id'),
|
||||
raidlevel => {
|
||||
type => 'string',
|
||||
description => 'The RAID level to use.',
|
||||
enum => [
|
||||
'single', 'mirror',
|
||||
'raid10', 'raidz', 'raidz2', 'raidz3',
|
||||
'draid', 'draid2', 'draid3',
|
||||
],
|
||||
},
|
||||
devices => {
|
||||
type => 'string', format => 'string-list',
|
||||
description => 'The block devices you want to create the zpool on.',
|
||||
},
|
||||
'draid-config' => {
|
||||
type => 'string',
|
||||
format => $draid_config_format,
|
||||
optional => 1,
|
||||
},
|
||||
ashift => {
|
||||
type => 'integer',
|
||||
minimum => 9,
|
||||
maximum => 16,
|
||||
optional => 1,
|
||||
default => 12,
|
||||
description => 'Pool sector size exponent.',
|
||||
},
|
||||
compression => {
|
||||
type => 'string',
|
||||
description => 'The compression algorithm to use.',
|
||||
enum => ['on', 'off', 'gzip', 'lz4', 'lzjb', 'zle', 'zstd'],
|
||||
optional => 1,
|
||||
default => 'on',
|
||||
},
|
||||
add_storage => {
|
||||
description => "Configure storage using the zpool.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => { type => 'string' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $user = $rpcenv->get_user();
|
||||
|
||||
my $name = $param->{name};
|
||||
my $node = $param->{node};
|
||||
my $devs = [PVE::Tools::split_list($param->{devices})];
|
||||
my $raidlevel = $param->{raidlevel};
|
||||
my $compression = $param->{compression} // 'on';
|
||||
|
||||
my $draid_config;
|
||||
if (exists $param->{'draid-config'}) {
|
||||
die "draid-config set without using dRAID level\n" if $raidlevel !~ m/^draid/;
|
||||
$draid_config = parse_property_string($draid_config_format, $param->{'draid-config'});
|
||||
}
|
||||
|
||||
for my $dev (@$devs) {
|
||||
$dev = PVE::Diskmanage::verify_blockdev_path($dev);
|
||||
PVE::Diskmanage::assert_disk_unused($dev);
|
||||
|
||||
}
|
||||
my $storage_params = {
|
||||
type => 'zfspool',
|
||||
pool => $name,
|
||||
storage => $name,
|
||||
content => 'rootdir,images',
|
||||
nodes => $node,
|
||||
};
|
||||
my $verify_params = [qw(pool)];
|
||||
|
||||
if ($param->{add_storage}) {
|
||||
PVE::API2::Storage::Config->create_or_update(
|
||||
$name,
|
||||
$node,
|
||||
$storage_params,
|
||||
$verify_params,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
my $pools = get_pool_data();
|
||||
die "pool '${name}' already exists on node '${node}'\n"
|
||||
if grep { $_->{name} eq $name } @{$pools};
|
||||
|
||||
my $numdisks = scalar(@$devs);
|
||||
my $mindisks = {
|
||||
single => 1,
|
||||
mirror => 2,
|
||||
raid10 => 4,
|
||||
raidz => 3,
|
||||
raidz2 => 4,
|
||||
raidz3 => 5,
|
||||
draid => 3,
|
||||
draid2 => 4,
|
||||
draid3 => 5,
|
||||
};
|
||||
|
||||
# sanity checks
|
||||
die "raid10 needs an even number of disks\n"
|
||||
if $raidlevel eq 'raid10' && $numdisks % 2 != 0;
|
||||
|
||||
die "please give only one disk for single disk mode\n"
|
||||
if $raidlevel eq 'single' && $numdisks > 1;
|
||||
|
||||
die "$raidlevel needs at least $mindisks->{$raidlevel} disks\n"
|
||||
if $numdisks < $mindisks->{$raidlevel};
|
||||
|
||||
# draid checks
|
||||
if ($raidlevel =~ m/^draid/) {
|
||||
# bare minimum would be two drives: one for parity & one for data, but forbid that
|
||||
# because it makes no sense in practice, at least one spare disk should be used
|
||||
my $draid_min = $mindisks->{$raidlevel} - 2;
|
||||
if ($draid_config) {
|
||||
$draid_min += $draid_config->{data} || 0;
|
||||
$draid_min += $draid_config->{spares} || 0;
|
||||
}
|
||||
die "At least $draid_min disks needed for current dRAID config\n"
|
||||
if $numdisks < $draid_min;
|
||||
}
|
||||
|
||||
my $code = sub {
|
||||
for my $dev (@$devs) {
|
||||
PVE::Diskmanage::assert_disk_unused($dev);
|
||||
|
||||
my $is_partition = PVE::Diskmanage::is_partition($dev);
|
||||
|
||||
if ($is_partition) {
|
||||
eval {
|
||||
PVE::Diskmanage::change_parttype($dev, '6a898cc3-1dd2-11b2-99a6-080020736631');
|
||||
};
|
||||
warn $@ if $@;
|
||||
}
|
||||
|
||||
my $sysfsdev = $is_partition ? PVE::Diskmanage::get_blockdev($dev) : $dev;
|
||||
|
||||
$sysfsdev =~ s!^/dev/!/sys/block/!;
|
||||
if ($is_partition) {
|
||||
my $part = $dev =~ s!^/dev/!!r;
|
||||
$sysfsdev .= "/${part}";
|
||||
}
|
||||
|
||||
my $udevinfo = PVE::Diskmanage::get_udev_info($sysfsdev);
|
||||
$dev = $udevinfo->{by_id_link} if defined($udevinfo->{by_id_link});
|
||||
}
|
||||
|
||||
# create zpool with desired raidlevel
|
||||
my $ashift = $param->{ashift} // 12;
|
||||
|
||||
my $cmd = [$ZPOOL, 'create', '-o', "ashift=$ashift", $name];
|
||||
|
||||
if ($raidlevel eq 'raid10') {
|
||||
for (my $i = 0; $i < @$devs; $i+=2) {
|
||||
push @$cmd, 'mirror', $devs->[$i], $devs->[$i+1];
|
||||
}
|
||||
} elsif ($raidlevel eq 'single') {
|
||||
push @$cmd, $devs->[0];
|
||||
} elsif ($raidlevel =~ m/^draid/) {
|
||||
my $draid_cmd = $raidlevel;
|
||||
$draid_cmd .= ":$draid_config->{data}d" if $draid_config->{data};
|
||||
$draid_cmd .= ":$draid_config->{spares}s" if $draid_config->{spares};
|
||||
push @$cmd, $draid_cmd, @$devs;
|
||||
} else {
|
||||
push @$cmd, $raidlevel, @$devs;
|
||||
}
|
||||
|
||||
print "# ", join(' ', @$cmd), "\n";
|
||||
run_command($cmd);
|
||||
|
||||
$cmd = [$ZFS, 'set', "compression=$compression", $name];
|
||||
print "# ", join(' ', @$cmd), "\n";
|
||||
run_command($cmd);
|
||||
|
||||
if (-e '/lib/systemd/system/zfs-import@.service') {
|
||||
my $importunit = 'zfs-import@'. PVE::Systemd::escape_unit($name, undef) . '.service';
|
||||
$cmd = ['systemctl', 'enable', $importunit];
|
||||
print "# ", join(' ', @$cmd), "\n";
|
||||
run_command($cmd);
|
||||
}
|
||||
|
||||
PVE::Diskmanage::udevadm_trigger($devs->@*);
|
||||
|
||||
if ($param->{add_storage}) {
|
||||
PVE::API2::Storage::Config->create_or_update(
|
||||
$name,
|
||||
$node,
|
||||
$storage_params,
|
||||
$verify_params,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return $rpcenv->fork_worker('zfscreate', $name, $user, sub {
|
||||
PVE::Diskmanage::locked_disk_action($code);
|
||||
});
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'delete',
|
||||
path => '{name}',
|
||||
method => 'DELETE',
|
||||
proxyto => 'node',
|
||||
protected => 1,
|
||||
permissions => {
|
||||
check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
|
||||
},
|
||||
description => "Destroy a ZFS pool.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
name => get_standard_option('pve-storage-id'),
|
||||
'cleanup-config' => {
|
||||
description => "Marks associated storage(s) as not available on this node anymore ".
|
||||
"or removes them from the configuration (if configured for this node only).",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
'cleanup-disks' => {
|
||||
description => "Also wipe disks so they can be repurposed afterwards.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
default => 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => { type => 'string' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $user = $rpcenv->get_user();
|
||||
|
||||
my $name = $param->{name};
|
||||
my $node = $param->{node};
|
||||
|
||||
my $worker = sub {
|
||||
PVE::Diskmanage::locked_disk_action(sub {
|
||||
my $to_wipe = [];
|
||||
if ($param->{'cleanup-disks'}) {
|
||||
# Using -o name does not only output the name in combination with -v.
|
||||
run_command(['zpool', 'list', '-vHPL', $name], outfunc => sub {
|
||||
my ($line) = @_;
|
||||
|
||||
my ($name) = PVE::Tools::split_list($line);
|
||||
return if $name !~ m|^/dev/.+|;
|
||||
|
||||
my $dev = PVE::Diskmanage::verify_blockdev_path($name);
|
||||
my $wipe = $dev;
|
||||
|
||||
$dev =~ s|^/dev/||;
|
||||
my $info = PVE::Diskmanage::get_disks($dev, 1, 1);
|
||||
die "unable to obtain information for disk '$dev'\n" if !$info->{$dev};
|
||||
|
||||
# Wipe whole disk if usual ZFS layout with partition 9 as ZFS reserved.
|
||||
my $parent = $info->{$dev}->{parent};
|
||||
if ($parent && scalar(keys $info->%*) == 3) {
|
||||
$parent =~ s|^/dev/||;
|
||||
my $info9 = $info->{"${parent}9"};
|
||||
|
||||
$wipe = $info->{$dev}->{parent} # need leading /dev/
|
||||
if $info9 && $info9->{used} && $info9->{used} =~ m/^ZFS reserved/;
|
||||
}
|
||||
|
||||
push $to_wipe->@*, $wipe;
|
||||
});
|
||||
}
|
||||
|
||||
if (-e '/lib/systemd/system/zfs-import@.service') {
|
||||
my $importunit = 'zfs-import@' . PVE::Systemd::escape_unit($name) . '.service';
|
||||
run_command(['systemctl', 'disable', $importunit]);
|
||||
}
|
||||
|
||||
run_command(['zpool', 'destroy', $name]);
|
||||
|
||||
my $config_err;
|
||||
if ($param->{'cleanup-config'}) {
|
||||
my $match = sub {
|
||||
my ($scfg) = @_;
|
||||
return $scfg->{type} eq 'zfspool' && $scfg->{pool} eq $name;
|
||||
};
|
||||
eval { PVE::API2::Storage::Config->cleanup_storages_for_node($match, $node); };
|
||||
warn $config_err = $@ if $@;
|
||||
}
|
||||
|
||||
eval { PVE::Diskmanage::wipe_blockdev($_) for $to_wipe->@*; };
|
||||
my $err = $@;
|
||||
PVE::Diskmanage::udevadm_trigger($to_wipe->@*);
|
||||
die "cleanup failed - $err" if $err;
|
||||
|
||||
die "config cleanup failed - $config_err" if $config_err;
|
||||
});
|
||||
};
|
||||
|
||||
return $rpcenv->fork_worker('zfsremove', $name, $user, $worker);
|
||||
}});
|
||||
|
||||
1;
|
||||
7
src/PVE/API2/Makefile
Normal file
7
src/PVE/API2/Makefile
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
install -D -m 0644 Disks.pm ${DESTDIR}${PERLDIR}/PVE/API2/Disks.pm
|
||||
make -C Storage install
|
||||
make -C Disks install
|
||||
424
src/PVE/API2/Storage/Config.pm
Executable file
424
src/PVE/API2/Storage/Config.pm
Executable file
@ -0,0 +1,424 @@
|
||||
package PVE::API2::Storage::Config;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use PVE::SafeSyslog;
|
||||
use PVE::Tools qw(extract_param extract_sensitive_params);
|
||||
use PVE::Cluster qw(cfs_read_file cfs_write_file);
|
||||
use PVE::Storage;
|
||||
use PVE::Storage::Plugin;
|
||||
use PVE::Storage::LVMPlugin;
|
||||
use PVE::Storage::CIFSPlugin;
|
||||
use HTTP::Status qw(:constants);
|
||||
use Storable qw(dclone);
|
||||
use PVE::JSONSchema qw(get_standard_option);
|
||||
use PVE::RPCEnvironment;
|
||||
|
||||
use PVE::RESTHandler;
|
||||
|
||||
use base qw(PVE::RESTHandler);
|
||||
|
||||
my @ctypes = qw(images vztmpl iso backup);
|
||||
|
||||
my $storage_type_enum = PVE::Storage::Plugin->lookup_types();
|
||||
|
||||
my $api_storage_config = sub {
|
||||
my ($cfg, $storeid) = @_;
|
||||
|
||||
my $scfg = dclone(PVE::Storage::storage_config($cfg, $storeid));
|
||||
$scfg->{storage} = $storeid;
|
||||
$scfg->{digest} = $cfg->{digest};
|
||||
$scfg->{content} = PVE::Storage::Plugin->encode_value($scfg->{type}, 'content', $scfg->{content});
|
||||
|
||||
if ($scfg->{nodes}) {
|
||||
$scfg->{nodes} = PVE::Storage::Plugin->encode_value($scfg->{type}, 'nodes', $scfg->{nodes});
|
||||
}
|
||||
|
||||
return $scfg;
|
||||
};
|
||||
|
||||
# For storages that $match->($scfg), update node restrictions to not include $node anymore and
|
||||
# in case no node remains, remove the storage altogether.
|
||||
sub cleanup_storages_for_node {
|
||||
my ($self, $match, $node) = @_;
|
||||
|
||||
my $config = PVE::Storage::config();
|
||||
my $cluster_nodes = PVE::Cluster::get_nodelist();
|
||||
|
||||
for my $storeid (keys $config->{ids}->%*) {
|
||||
my $scfg = PVE::Storage::storage_config($config, $storeid);
|
||||
next if !$match->($scfg);
|
||||
|
||||
my $nodes = $scfg->{nodes} || { map { $_ => 1 } $cluster_nodes->@* };
|
||||
next if !$nodes->{$node}; # not configured on $node, so nothing to do
|
||||
delete $nodes->{$node};
|
||||
|
||||
if (scalar(keys $nodes->%*) > 0) {
|
||||
$self->update({
|
||||
nodes => join(',', sort keys $nodes->%*),
|
||||
storage => $storeid,
|
||||
});
|
||||
} else {
|
||||
$self->delete({storage => $storeid});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Decides if a storage needs to be created or updated. An update is needed, if
|
||||
# the storage has a node list configured, then the current node will be added.
|
||||
# The verify_params parameter is an array of parameter names that need to match
|
||||
# if there already is a storage config of the same name present. This is
|
||||
# mainly intended for local storage types as certain parameters need to be the
|
||||
# same. For exmaple 'pool' for ZFS, 'vg_name' for LVM, ...
|
||||
# Set the dryrun parameter, to only verify the parameters without updating or
|
||||
# creating the storage.
|
||||
sub create_or_update {
|
||||
my ($self, $sid, $node, $storage_params, $verify_params, $dryrun) = @_;
|
||||
|
||||
my $cfg = PVE::Storage::config();
|
||||
my $scfg = PVE::Storage::storage_config($cfg, $sid, 1);
|
||||
|
||||
if ($scfg) {
|
||||
die "storage config for '${sid}' exists but no parameters to verify were provided\n"
|
||||
if !$verify_params;
|
||||
|
||||
$node = PVE::INotify::nodename() if !$node || ($node eq 'localhost');
|
||||
die "Storage ID '${sid}' already exists on node ${node}\n"
|
||||
if !defined($scfg->{nodes}) || $scfg->{nodes}->{$node};
|
||||
|
||||
push @$verify_params, 'type';
|
||||
for my $key (@$verify_params) {
|
||||
if (!defined($scfg->{$key})) {
|
||||
die "Option '${key}' is not configured for storage '$sid', "
|
||||
."expected it to be '$storage_params->{$key}'";
|
||||
}
|
||||
if ($storage_params->{$key} ne $scfg->{$key}) {
|
||||
die "Option '${key}' ($storage_params->{$key}) does not match "
|
||||
."existing storage configuration '$scfg->{$key}'\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$dryrun) {
|
||||
if ($scfg) {
|
||||
if ($scfg->{nodes}) {
|
||||
$scfg->{nodes}->{$node} = 1;
|
||||
$self->update({
|
||||
nodes => join(',', sort keys $scfg->{nodes}->%*),
|
||||
storage => $sid,
|
||||
});
|
||||
print "Added '${node}' to nodes for storage '${sid}'\n";
|
||||
}
|
||||
} else {
|
||||
$self->create($storage_params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'index',
|
||||
path => '',
|
||||
method => 'GET',
|
||||
description => "Storage index.",
|
||||
permissions => {
|
||||
description => "Only list entries where you have 'Datastore.Audit' or 'Datastore.AllocateSpace' permissions on '/storage/<storage>'",
|
||||
user => 'all',
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
type => {
|
||||
description => "Only list storage of specific type",
|
||||
type => 'string',
|
||||
enum => $storage_type_enum,
|
||||
optional => 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => { storage => { type => 'string'} },
|
||||
},
|
||||
links => [ { rel => 'child', href => "{storage}" } ],
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
|
||||
my $cfg = PVE::Storage::config();
|
||||
|
||||
my @sids = PVE::Storage::storage_ids($cfg);
|
||||
|
||||
my $res = [];
|
||||
foreach my $storeid (@sids) {
|
||||
my $privs = [ 'Datastore.Audit', 'Datastore.AllocateSpace' ];
|
||||
next if !$rpcenv->check_any($authuser, "/storage/$storeid", $privs, 1);
|
||||
|
||||
my $scfg = &$api_storage_config($cfg, $storeid);
|
||||
next if $param->{type} && $param->{type} ne $scfg->{type};
|
||||
push @$res, $scfg;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'read',
|
||||
path => '{storage}',
|
||||
method => 'GET',
|
||||
description => "Read storage configuration.",
|
||||
permissions => {
|
||||
check => ['perm', '/storage/{storage}', ['Datastore.Allocate']],
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
storage => get_standard_option('pve-storage-id'),
|
||||
},
|
||||
},
|
||||
returns => { type => 'object' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $cfg = PVE::Storage::config();
|
||||
|
||||
return &$api_storage_config($cfg, $param->{storage});
|
||||
}});
|
||||
|
||||
my $sensitive_params = [qw(password encryption-key master-pubkey keyring)];
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'create',
|
||||
protected => 1,
|
||||
path => '',
|
||||
method => 'POST',
|
||||
description => "Create a new storage.",
|
||||
permissions => {
|
||||
check => ['perm', '/storage', ['Datastore.Allocate']],
|
||||
},
|
||||
parameters => PVE::Storage::Plugin->createSchema(),
|
||||
returns => {
|
||||
type => 'object',
|
||||
properties => {
|
||||
storage => {
|
||||
description => "The ID of the created storage.",
|
||||
type => 'string',
|
||||
},
|
||||
type => {
|
||||
description => "The type of the created storage.",
|
||||
type => 'string',
|
||||
enum => $storage_type_enum,
|
||||
},
|
||||
config => {
|
||||
description => "Partial, possible server generated, configuration properties.",
|
||||
type => 'object',
|
||||
optional => 1,
|
||||
additionalProperties => 1,
|
||||
properties => {
|
||||
'encryption-key' => {
|
||||
description => "The, possible auto-generated, encryption-key.",
|
||||
optional => 1,
|
||||
type => 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $type = extract_param($param, 'type');
|
||||
my $storeid = extract_param($param, 'storage');
|
||||
|
||||
# revent an empty nodelist.
|
||||
# fix me in section config create never need an empty entity.
|
||||
delete $param->{nodes} if !$param->{nodes};
|
||||
|
||||
my $sensitive = extract_sensitive_params($param, $sensitive_params, []);
|
||||
|
||||
my $plugin = PVE::Storage::Plugin->lookup($type);
|
||||
my $opts = $plugin->check_config($storeid, $param, 1, 1);
|
||||
|
||||
my $returned_config;
|
||||
PVE::Storage::lock_storage_config(sub {
|
||||
my $cfg = PVE::Storage::config();
|
||||
|
||||
if (my $scfg = PVE::Storage::storage_config($cfg, $storeid, 1)) {
|
||||
die "storage ID '$storeid' already defined\n";
|
||||
}
|
||||
|
||||
$cfg->{ids}->{$storeid} = $opts;
|
||||
|
||||
$returned_config = $plugin->on_add_hook($storeid, $opts, %$sensitive);
|
||||
|
||||
eval {
|
||||
# try to activate if enabled on local node,
|
||||
# we only do this to detect errors/problems sooner
|
||||
if (PVE::Storage::storage_check_enabled($cfg, $storeid, undef, 1)) {
|
||||
PVE::Storage::activate_storage($cfg, $storeid);
|
||||
}
|
||||
};
|
||||
if (my $err = $@) {
|
||||
eval { $plugin->on_delete_hook($storeid, $opts) };
|
||||
warn "$@\n" if $@;
|
||||
die $err;
|
||||
}
|
||||
|
||||
PVE::Storage::write_config($cfg);
|
||||
|
||||
}, "create storage failed");
|
||||
|
||||
my $res = {
|
||||
storage => $storeid,
|
||||
type => $type,
|
||||
};
|
||||
$res->{config} = $returned_config if $returned_config;
|
||||
return $res;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'update',
|
||||
protected => 1,
|
||||
path => '{storage}',
|
||||
method => 'PUT',
|
||||
description => "Update storage configuration.",
|
||||
permissions => {
|
||||
check => ['perm', '/storage', ['Datastore.Allocate']],
|
||||
},
|
||||
parameters => PVE::Storage::Plugin->updateSchema(),
|
||||
returns => {
|
||||
type => 'object',
|
||||
properties => {
|
||||
storage => {
|
||||
description => "The ID of the created storage.",
|
||||
type => 'string',
|
||||
},
|
||||
type => {
|
||||
description => "The type of the created storage.",
|
||||
type => 'string',
|
||||
enum => $storage_type_enum,
|
||||
},
|
||||
config => {
|
||||
description => "Partial, possible server generated, configuration properties.",
|
||||
type => 'object',
|
||||
optional => 1,
|
||||
additionalProperties => 1,
|
||||
properties => {
|
||||
'encryption-key' => {
|
||||
description => "The, possible auto-generated, encryption-key.",
|
||||
optional => 1,
|
||||
type => 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $storeid = extract_param($param, 'storage');
|
||||
my $digest = extract_param($param, 'digest');
|
||||
my $delete = extract_param($param, 'delete');
|
||||
my $type;
|
||||
|
||||
if ($delete) {
|
||||
$delete = [ PVE::Tools::split_list($delete) ];
|
||||
}
|
||||
|
||||
my $returned_config;
|
||||
PVE::Storage::lock_storage_config(sub {
|
||||
my $cfg = PVE::Storage::config();
|
||||
|
||||
PVE::SectionConfig::assert_if_modified($cfg, $digest);
|
||||
|
||||
my $scfg = PVE::Storage::storage_config($cfg, $storeid);
|
||||
$type = $scfg->{type};
|
||||
|
||||
my $sensitive = extract_sensitive_params($param, $sensitive_params, $delete);
|
||||
|
||||
my $plugin = PVE::Storage::Plugin->lookup($type);
|
||||
my $opts = $plugin->check_config($storeid, $param, 0, 1);
|
||||
|
||||
if ($delete) {
|
||||
my $options = $plugin->private()->{options}->{$type};
|
||||
foreach my $k (@$delete) {
|
||||
my $d = $options->{$k} || die "no such option '$k'\n";
|
||||
die "unable to delete required option '$k'\n" if !$d->{optional};
|
||||
die "unable to delete fixed option '$k'\n" if $d->{fixed};
|
||||
die "cannot set and delete property '$k' at the same time!\n"
|
||||
if defined($opts->{$k});
|
||||
|
||||
delete $scfg->{$k};
|
||||
}
|
||||
}
|
||||
|
||||
$returned_config = $plugin->on_update_hook($storeid, $opts, %$sensitive);
|
||||
|
||||
for my $k (keys %$opts) {
|
||||
$scfg->{$k} = $opts->{$k};
|
||||
}
|
||||
|
||||
PVE::Storage::write_config($cfg);
|
||||
|
||||
}, "update storage failed");
|
||||
|
||||
my $res = {
|
||||
storage => $storeid,
|
||||
type => $type,
|
||||
};
|
||||
$res->{config} = $returned_config if $returned_config;
|
||||
return $res;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'delete',
|
||||
protected => 1,
|
||||
path => '{storage}', # /storage/config/{storage}
|
||||
method => 'DELETE',
|
||||
description => "Delete storage configuration.",
|
||||
permissions => {
|
||||
check => ['perm', '/storage', ['Datastore.Allocate']],
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
storage => get_standard_option('pve-storage-id', {
|
||||
completion => \&PVE::Storage::complete_storage,
|
||||
}),
|
||||
},
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $storeid = extract_param($param, 'storage');
|
||||
|
||||
PVE::Storage::lock_storage_config(sub {
|
||||
my $cfg = PVE::Storage::config();
|
||||
|
||||
my $scfg = PVE::Storage::storage_config($cfg, $storeid);
|
||||
|
||||
die "can't remove storage - storage is used as base of another storage\n"
|
||||
if PVE::Storage::storage_is_used($cfg, $storeid);
|
||||
|
||||
my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
|
||||
|
||||
$plugin->on_delete_hook($storeid, $scfg);
|
||||
|
||||
delete $cfg->{ids}->{$storeid};
|
||||
|
||||
PVE::Storage::write_config($cfg);
|
||||
|
||||
}, "delete storage failed");
|
||||
|
||||
PVE::AccessControl::remove_storage_access($storeid);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
|
||||
1;
|
||||
560
src/PVE/API2/Storage/Content.pm
Normal file
560
src/PVE/API2/Storage/Content.pm
Normal file
@ -0,0 +1,560 @@
|
||||
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,
|
||||
},
|
||||
encrypted => {
|
||||
description => "If whole backup is encrypted, value is the fingerprint or '1' "
|
||||
." if encrypted. Only useful for the Proxmox Backup Server storage type.",
|
||||
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,
|
||||
},
|
||||
protected => {
|
||||
description => "Protection status. Currently only supported for backups.",
|
||||
type => 'boolean',
|
||||
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});
|
||||
$item->{size} = int($item->{size}) if defined($item->{size});
|
||||
$item->{used} = int($item->{used}) if defined($item->{used});
|
||||
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 mismatch ($sid != $storeid)\n"
|
||||
if $storeid && $sid ne $storeid;
|
||||
$volid = $volume;
|
||||
$storeid = $sid;
|
||||
};
|
||||
raise_param_exc({ volume => $@ }) if $@;
|
||||
|
||||
} else {
|
||||
raise_param_exc({ volume => "no storage specified - 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',
|
||||
},
|
||||
protected => {
|
||||
description => "Protection status. Currently only supported for backups.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
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 => int($size), # cast to integer in case it was changed to a string previously
|
||||
used => int($used),
|
||||
format => $format,
|
||||
};
|
||||
|
||||
for my $attribute (qw(notes protected)) {
|
||||
# keep going if fetching an optional attribute fails
|
||||
eval {
|
||||
my $value = PVE::Storage::get_volume_attribute($cfg, $volid, $attribute);
|
||||
$entry->{$attribute} = $value if defined($value);
|
||||
};
|
||||
warn $@ if $@;
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
protected => {
|
||||
description => "Protection status. Currently only supported for backups.",
|
||||
type => 'boolean',
|
||||
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);
|
||||
|
||||
for my $attr (qw(notes protected)) {
|
||||
if (exists $param->{$attr}) {
|
||||
PVE::Storage::update_volume_attribute($cfg, $volid, $attr, $param->{$attr});
|
||||
}
|
||||
}
|
||||
|
||||
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})[^\/]+$/) {
|
||||
# Remove log file #318 and notes file #3972 if they still exist
|
||||
PVE::Storage::archive_auxiliaries_remove($path);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
chomp $status;
|
||||
return undef if !PVE::Tools::upid_status_is_error($status);
|
||||
die "$status\n";
|
||||
}
|
||||
}
|
||||
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 before 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;
|
||||
215
src/PVE/API2/Storage/FileRestore.pm
Normal file
215
src/PVE/API2/Storage/FileRestore.pm
Normal file
@ -0,0 +1,215 @@
|
||||
package PVE::API2::Storage::FileRestore;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use MIME::Base64;
|
||||
use PVE::Exception qw(raise_param_exc);
|
||||
use PVE::JSONSchema qw(get_standard_option);
|
||||
use PVE::PBSClient;
|
||||
use PVE::Storage;
|
||||
use PVE::Tools qw(extract_param);
|
||||
|
||||
use PVE::RESTHandler;
|
||||
use base qw(PVE::RESTHandler);
|
||||
|
||||
my $parse_volname_or_id = sub {
|
||||
my ($storeid, $volume) = @_;
|
||||
|
||||
my $volid;
|
||||
my ($sid, $volname) = PVE::Storage::parse_volume_id($volume, 1);
|
||||
|
||||
if (defined($sid)) {
|
||||
raise_param_exc({ volume => "storage ID mismatch ($sid != $storeid)." })
|
||||
if $sid ne $storeid;
|
||||
|
||||
$volid = $volume;
|
||||
} elsif ($volume =~ m/^backup\//) {
|
||||
$volid = "$storeid:$volume";
|
||||
} else {
|
||||
$volid = "$storeid:backup/$volume";
|
||||
}
|
||||
|
||||
return $volid;
|
||||
};
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'list',
|
||||
path => 'list',
|
||||
method => 'GET',
|
||||
proxyto => 'node',
|
||||
permissions => {
|
||||
description => "You need read access for the volume.",
|
||||
user => 'all',
|
||||
},
|
||||
description => "List files and directories for single file restore under the given path.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
storage => get_standard_option('pve-storage-id', {
|
||||
completion => \&PVE::Storage::complete_storage_enabled,
|
||||
}),
|
||||
volume => {
|
||||
description => "Backup volume ID or name. Currently only PBS snapshots are supported.",
|
||||
type => 'string',
|
||||
completion => \&PVE::Storage::complete_volume,
|
||||
},
|
||||
filepath => {
|
||||
description => 'base64-path to the directory or file being listed, or "/".',
|
||||
type => 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
filepath => {
|
||||
description => "base64 path of the current entry",
|
||||
type => 'string',
|
||||
},
|
||||
type => {
|
||||
description => "Entry type.",
|
||||
type => 'string',
|
||||
},
|
||||
text => {
|
||||
description => "Entry display text.",
|
||||
type => 'string',
|
||||
},
|
||||
leaf => {
|
||||
description => "If this entry is a leaf in the directory graph.",
|
||||
type => 'boolean',
|
||||
},
|
||||
size => {
|
||||
description => "Entry file size.",
|
||||
type => 'integer',
|
||||
optional => 1,
|
||||
},
|
||||
mtime => {
|
||||
description => "Entry last-modified time (unix timestamp).",
|
||||
type => 'integer',
|
||||
optional => 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
protected => 1,
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $user = $rpcenv->get_user();
|
||||
|
||||
my $path = extract_param($param, 'filepath') || "/";
|
||||
my $base64 = $path ne "/";
|
||||
|
||||
my $storeid = extract_param($param, 'storage');
|
||||
|
||||
my $volid = $parse_volname_or_id->($storeid, $param->{volume});
|
||||
my $cfg = PVE::Storage::config();
|
||||
my $scfg = PVE::Storage::storage_config($cfg, $storeid);
|
||||
|
||||
PVE::Storage::check_volume_access($rpcenv, $user, $cfg, undef, $volid, 'backup');
|
||||
|
||||
raise_param_exc({'storage' => "Only PBS storages supported for file-restore."})
|
||||
if $scfg->{type} ne 'pbs';
|
||||
|
||||
my (undef, $snap) = PVE::Storage::parse_volname($cfg, $volid);
|
||||
|
||||
my $client = PVE::PBSClient->new($scfg, $storeid);
|
||||
my $ret = $client->file_restore_list($snap, $path, $base64, { timeout => 25 });
|
||||
|
||||
if (ref($ret) eq "HASH") {
|
||||
my $msg = $ret->{message};
|
||||
if (my $code = $ret->{code}) {
|
||||
die PVE::Exception->new("$msg\n", code => $code);
|
||||
} else {
|
||||
die "$msg\n";
|
||||
}
|
||||
} elsif (ref($ret) eq "ARRAY") {
|
||||
# 'leaf' is a proper JSON boolean, map to perl-y bool
|
||||
# TODO: make PBSClient decode all bools always as 1/0?
|
||||
foreach my $item (@$ret) {
|
||||
$item->{leaf} = $item->{leaf} ? 1 : 0;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
die "invalid proxmox-file-restore output";
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'download',
|
||||
path => 'download',
|
||||
method => 'GET',
|
||||
proxyto => 'node',
|
||||
permissions => {
|
||||
description => "You need read access for the volume.",
|
||||
user => 'all',
|
||||
},
|
||||
description => "Extract a file or directory (as zip archive) from a PBS backup.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
storage => get_standard_option('pve-storage-id', {
|
||||
completion => \&PVE::Storage::complete_storage_enabled,
|
||||
}),
|
||||
volume => {
|
||||
description => "Backup volume ID or name. Currently only PBS snapshots are supported.",
|
||||
type => 'string',
|
||||
completion => \&PVE::Storage::complete_volume,
|
||||
},
|
||||
filepath => {
|
||||
description => 'base64-path to the directory or file to download.',
|
||||
type => 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'any', # download
|
||||
},
|
||||
protected => 1,
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $user = $rpcenv->get_user();
|
||||
|
||||
my $path = extract_param($param, 'filepath');
|
||||
my $storeid = extract_param($param, 'storage');
|
||||
my $volid = $parse_volname_or_id->($storeid, $param->{volume});
|
||||
|
||||
my $cfg = PVE::Storage::config();
|
||||
my $scfg = PVE::Storage::storage_config($cfg, $storeid);
|
||||
|
||||
PVE::Storage::check_volume_access($rpcenv, $user, $cfg, undef, $volid, 'backup');
|
||||
|
||||
raise_param_exc({'storage' => "Only PBS storages supported for file-restore."})
|
||||
if $scfg->{type} ne 'pbs';
|
||||
|
||||
my (undef, $snap) = PVE::Storage::parse_volname($cfg, $volid);
|
||||
|
||||
my $client = PVE::PBSClient->new($scfg, $storeid);
|
||||
my $fifo = $client->file_restore_extract_prepare();
|
||||
|
||||
$rpcenv->fork_worker('pbs-download', undef, $user, sub {
|
||||
my $name = decode_base64($path);
|
||||
print "Starting download of file: $name\n";
|
||||
$client->file_restore_extract($fifo, $snap, $path, 1);
|
||||
});
|
||||
|
||||
my $ret = {
|
||||
download => {
|
||||
path => $fifo,
|
||||
stream => 1,
|
||||
'content-type' => 'application/octet-stream',
|
||||
},
|
||||
};
|
||||
return $ret;
|
||||
}});
|
||||
|
||||
1;
|
||||
6
src/PVE/API2/Storage/Makefile
Normal file
6
src/PVE/API2/Storage/Makefile
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
SOURCES= Content.pm Status.pm Config.pm PruneBackups.pm Scan.pm FileRestore.pm
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/API2/Storage/$$i; done
|
||||
164
src/PVE/API2/Storage/PruneBackups.pm
Normal file
164
src/PVE/API2/Storage/PruneBackups.pm
Normal file
@ -0,0 +1,164 @@
|
||||
package PVE::API2::Storage::PruneBackups;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use PVE::Cluster;
|
||||
use PVE::JSONSchema qw(get_standard_option);
|
||||
use PVE::RESTHandler;
|
||||
use PVE::RPCEnvironment;
|
||||
use PVE::Storage;
|
||||
use PVE::Tools qw(extract_param);
|
||||
|
||||
use base qw(PVE::RESTHandler);
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'dryrun',
|
||||
path => '',
|
||||
method => 'GET',
|
||||
description => "Get prune information for backups. NOTE: this is only a preview and might not be " .
|
||||
"what a subsequent prune call does if backups are removed/added in the meantime.",
|
||||
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,
|
||||
}),
|
||||
'prune-backups' => get_standard_option('prune-backups', {
|
||||
description => "Use these retention options instead of those from the storage configuration.",
|
||||
optional => 1,
|
||||
}),
|
||||
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 => '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. Backups that are" .
|
||||
" protected or don't use the standard naming scheme are not removed.",
|
||||
type => 'string',
|
||||
enum => ['keep', 'remove', 'protected', 'renamed'],
|
||||
},
|
||||
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 $cfg = PVE::Storage::config();
|
||||
|
||||
my $vmid = extract_param($param, 'vmid');
|
||||
my $type = extract_param($param, 'type');
|
||||
my $storeid = extract_param($param, 'storage');
|
||||
|
||||
my $prune_backups = extract_param($param, 'prune-backups');
|
||||
$prune_backups = PVE::JSONSchema::parse_property_string('prune-backups', $prune_backups)
|
||||
if defined($prune_backups);
|
||||
|
||||
return PVE::Storage::prune_backups($cfg, $storeid, $prune_backups, $vmid, $type, 1);
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'delete',
|
||||
path => '',
|
||||
method => 'DELETE',
|
||||
description => "Prune backups. Only those using the standard naming scheme are considered.",
|
||||
permissions => {
|
||||
description => "You need the 'Datastore.Allocate' privilege on the storage " .
|
||||
"(or if a VM ID is specified, 'Datastore.AllocateSpace' and 'VM.Backup' for 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', {
|
||||
completion => \&PVE::Storage::complete_storage,
|
||||
}),
|
||||
'prune-backups' => get_standard_option('prune-backups', {
|
||||
description => "Use these retention options instead of those from the storage configuration.",
|
||||
}),
|
||||
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 prune backups for this VM.",
|
||||
completion => \&PVE::Cluster::complete_vmid,
|
||||
optional => 1,
|
||||
}),
|
||||
},
|
||||
},
|
||||
returns => { type => 'string' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
my $authuser = $rpcenv->get_user();
|
||||
|
||||
my $cfg = PVE::Storage::config();
|
||||
|
||||
my $vmid = extract_param($param, 'vmid');
|
||||
my $type = extract_param($param, 'type');
|
||||
my $storeid = extract_param($param, 'storage');
|
||||
|
||||
my $prune_backups = extract_param($param, 'prune-backups');
|
||||
$prune_backups = PVE::JSONSchema::parse_property_string('prune-backups', $prune_backups)
|
||||
if defined($prune_backups);
|
||||
|
||||
if (defined($vmid)) {
|
||||
$rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
|
||||
$rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup']);
|
||||
} else {
|
||||
$rpcenv->check($authuser, "/storage/$storeid", ['Datastore.Allocate']);
|
||||
}
|
||||
|
||||
my $id = (defined($vmid) ? "$vmid@" : '') . $storeid;
|
||||
my $worker = sub {
|
||||
PVE::Storage::prune_backups($cfg, $storeid, $prune_backups, $vmid, $type, 0);
|
||||
};
|
||||
|
||||
return $rpcenv->fork_worker('prunebackups', $id, $authuser, $worker);
|
||||
}});
|
||||
|
||||
1;
|
||||
449
src/PVE/API2/Storage/Scan.pm
Normal file
449
src/PVE/API2/Storage/Scan.pm
Normal file
@ -0,0 +1,449 @@
|
||||
package PVE::API2::Storage::Scan;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
# NOTE: This API endpoints are mounted by pve-manager's API2::Node module and pvesm CLI
|
||||
|
||||
use PVE::JSONSchema qw(get_standard_option);
|
||||
use PVE::RESTHandler;
|
||||
use PVE::SafeSyslog;
|
||||
use PVE::Storage::LVMPlugin;
|
||||
use PVE::Storage;
|
||||
use PVE::SysFSTools;
|
||||
|
||||
use base qw(PVE::RESTHandler);
|
||||
|
||||
__PACKAGE__->register_method({
|
||||
name => 'index',
|
||||
path => '',
|
||||
method => 'GET',
|
||||
description => "Index of available scan methods",
|
||||
permissions => {
|
||||
user => 'all',
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
method => { type => 'string'},
|
||||
},
|
||||
},
|
||||
links => [ { rel => 'child', href => "{method}" } ],
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $res = [
|
||||
{ method => 'cifs' },
|
||||
{ method => 'glusterfs' },
|
||||
{ method => 'iscsi' },
|
||||
{ method => 'lvm' },
|
||||
{ method => 'nfs' },
|
||||
{ method => 'pbs' },
|
||||
{ method => 'zfs' },
|
||||
];
|
||||
|
||||
return $res;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method({
|
||||
name => 'nfsscan',
|
||||
path => 'nfs',
|
||||
method => 'GET',
|
||||
description => "Scan remote NFS server.",
|
||||
protected => 1,
|
||||
proxyto => "node",
|
||||
permissions => {
|
||||
check => ['perm', '/storage', ['Datastore.Allocate']],
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
server => {
|
||||
description => "The server address (name or IP).",
|
||||
type => 'string', format => 'pve-storage-server',
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
path => {
|
||||
description => "The exported path.",
|
||||
type => 'string',
|
||||
},
|
||||
options => {
|
||||
description => "NFS export options.",
|
||||
type => 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $server = $param->{server};
|
||||
my $res = PVE::Storage::scan_nfs($server);
|
||||
|
||||
my $data = [];
|
||||
foreach my $k (sort keys %$res) {
|
||||
push @$data, { path => $k, options => $res->{$k} };
|
||||
}
|
||||
return $data;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method({
|
||||
name => 'cifsscan',
|
||||
path => 'cifs',
|
||||
method => 'GET',
|
||||
description => "Scan remote CIFS server.",
|
||||
protected => 1,
|
||||
proxyto => "node",
|
||||
permissions => {
|
||||
check => ['perm', '/storage', ['Datastore.Allocate']],
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
server => {
|
||||
description => "The server address (name or IP).",
|
||||
type => 'string', format => 'pve-storage-server',
|
||||
},
|
||||
username => {
|
||||
description => "User name.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
password => {
|
||||
description => "User password.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
domain => {
|
||||
description => "SMB domain (Workgroup).",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
share => {
|
||||
description => "The cifs share name.",
|
||||
type => 'string',
|
||||
},
|
||||
description => {
|
||||
description => "Descriptive text from server.",
|
||||
type => 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $server = $param->{server};
|
||||
|
||||
my $username = $param->{username};
|
||||
my $password = $param->{password};
|
||||
my $domain = $param->{domain};
|
||||
|
||||
my $res = PVE::Storage::scan_cifs($server, $username, $password, $domain);
|
||||
|
||||
my $data = [];
|
||||
foreach my $k (sort keys %$res) {
|
||||
next if $k =~ m/NT_STATUS_/;
|
||||
push @$data, { share => $k, description => $res->{$k} };
|
||||
}
|
||||
|
||||
return $data;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method({
|
||||
name => 'pbsscan',
|
||||
path => 'pbs',
|
||||
method => 'GET',
|
||||
description => "Scan remote Proxmox Backup Server.",
|
||||
protected => 1,
|
||||
proxyto => "node",
|
||||
permissions => {
|
||||
check => ['perm', '/storage', ['Datastore.Allocate']],
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
server => {
|
||||
description => "The server address (name or IP).",
|
||||
type => 'string', format => 'pve-storage-server',
|
||||
},
|
||||
username => {
|
||||
description => "User-name or API token-ID.",
|
||||
type => 'string',
|
||||
},
|
||||
password => {
|
||||
description => "User password or API token secret.",
|
||||
type => 'string',
|
||||
},
|
||||
fingerprint => get_standard_option('fingerprint-sha256', {
|
||||
optional => 1,
|
||||
}),
|
||||
port => {
|
||||
description => "Optional port.",
|
||||
type => 'integer',
|
||||
minimum => 1,
|
||||
maximum => 65535,
|
||||
default => 8007,
|
||||
optional => 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
store => {
|
||||
description => "The datastore name.",
|
||||
type => 'string',
|
||||
},
|
||||
comment => {
|
||||
description => "Comment from server.",
|
||||
type => 'string',
|
||||
optional => 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $password = delete $param->{password};
|
||||
|
||||
return PVE::Storage::PBSPlugin::scan_datastores($param, $password);
|
||||
}
|
||||
});
|
||||
|
||||
# Note: GlusterFS currently does not have an equivalent of showmount.
|
||||
# As workaround, we simply use nfs showmount.
|
||||
# see http://www.gluster.org/category/volumes/
|
||||
__PACKAGE__->register_method({
|
||||
name => 'glusterfsscan',
|
||||
path => 'glusterfs',
|
||||
method => 'GET',
|
||||
description => "Scan remote GlusterFS server.",
|
||||
protected => 1,
|
||||
proxyto => "node",
|
||||
permissions => {
|
||||
check => ['perm', '/storage', ['Datastore.Allocate']],
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
server => {
|
||||
description => "The server address (name or IP).",
|
||||
type => 'string', format => 'pve-storage-server',
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
volname => {
|
||||
description => "The volume name.",
|
||||
type => 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $server = $param->{server};
|
||||
my $res = PVE::Storage::scan_nfs($server);
|
||||
|
||||
my $data = [];
|
||||
foreach my $path (sort keys %$res) {
|
||||
if ($path =~ m!^/([^\s/]+)$!) {
|
||||
push @$data, { volname => $1 };
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method({
|
||||
name => 'iscsiscan',
|
||||
path => 'iscsi',
|
||||
method => 'GET',
|
||||
description => "Scan remote iSCSI server.",
|
||||
protected => 1,
|
||||
proxyto => "node",
|
||||
permissions => {
|
||||
check => ['perm', '/storage', ['Datastore.Allocate']],
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
portal => {
|
||||
description => "The iSCSI portal (IP or DNS name with optional port).",
|
||||
type => 'string', format => 'pve-storage-portal-dns',
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
target => {
|
||||
description => "The iSCSI target name.",
|
||||
type => 'string',
|
||||
},
|
||||
portal => {
|
||||
description => "The iSCSI portal name.",
|
||||
type => 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $res = PVE::Storage::scan_iscsi($param->{portal});
|
||||
|
||||
my $data = [];
|
||||
foreach my $k (sort keys %$res) {
|
||||
push @$data, { target => $k, portal => join(',', @{$res->{$k}}) };
|
||||
}
|
||||
|
||||
return $data;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method({
|
||||
name => 'lvmscan',
|
||||
path => 'lvm',
|
||||
method => 'GET',
|
||||
description => "List local LVM volume groups.",
|
||||
protected => 1,
|
||||
proxyto => "node",
|
||||
permissions => {
|
||||
check => ['perm', '/storage', ['Datastore.Allocate']],
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
vg => {
|
||||
description => "The LVM logical volume group name.",
|
||||
type => 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $res = PVE::Storage::LVMPlugin::lvm_vgs();
|
||||
return PVE::RESTHandler::hash_to_array($res, 'vg');
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method({
|
||||
name => 'lvmthinscan',
|
||||
path => 'lvmthin',
|
||||
method => 'GET',
|
||||
description => "List local LVM Thin Pools.",
|
||||
protected => 1,
|
||||
proxyto => "node",
|
||||
permissions => {
|
||||
check => ['perm', '/storage', ['Datastore.Allocate']],
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
vg => {
|
||||
type => 'string',
|
||||
pattern => '[a-zA-Z0-9\.\+\_][a-zA-Z0-9\.\+\_\-]+', # see lvm(8) manpage
|
||||
maxLength => 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
lv => {
|
||||
description => "The LVM Thin Pool name (LVM logical volume).",
|
||||
type => 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
return PVE::Storage::LvmThinPlugin::list_thinpools($param->{vg});
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method({
|
||||
name => 'zfsscan',
|
||||
path => 'zfs',
|
||||
method => 'GET',
|
||||
description => "Scan zfs pool list on local node.",
|
||||
protected => 1,
|
||||
proxyto => "node",
|
||||
permissions => {
|
||||
check => ['perm', '/storage', ['Datastore.Allocate']],
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
node => get_standard_option('pve-node'),
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
pool => {
|
||||
description => "ZFS pool name.",
|
||||
type => 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
return PVE::Storage::scan_zfs();
|
||||
}});
|
||||
|
||||
1;
|
||||
660
src/PVE/API2/Storage/Status.pm
Normal file
660
src/PVE/API2/Storage/Status.pm
Normal file
@ -0,0 +1,660 @@
|
||||
package PVE::API2::Storage::Status;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use File::Basename;
|
||||
use File::Path;
|
||||
use POSIX qw(ENOENT);
|
||||
|
||||
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 qw(run_command);
|
||||
|
||||
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',
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
pattern => '/var/tmp/pveupload-[0-9a-f]+',
|
||||
},
|
||||
},
|
||||
},
|
||||
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_EXT_RE_0$!) {
|
||||
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_EXT_RE_1$!) {
|
||||
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;
|
||||
|
||||
my $err_cleanup = sub { unlink $dest; die "cleanup failed: $!\n" if $! && $! != ENOENT };
|
||||
|
||||
my $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
|
||||
run_command([@remcmd, '/usr/sbin/pvesm', 'status', '--storage', $param->{storage}]);
|
||||
};
|
||||
die "can't activate storage '$param->{storage}' on node '$node': $@\n" if $@;
|
||||
|
||||
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_cleanup = sub { run_command([@remcmd, 'rm', '-f', '--', $dest]) };
|
||||
} else {
|
||||
PVE::Storage::activate_storage($cfg, $param->{storage});
|
||||
File::Path::make_path($dirname);
|
||||
$cmd = ['cp', '--', $tmpfilename, $dest];
|
||||
}
|
||||
|
||||
# NOTE: we simply overwrite the destination file if it already exists
|
||||
my $worker = sub {
|
||||
my $upid = shift;
|
||||
|
||||
print "starting file import from: $tmpfilename\n";
|
||||
|
||||
eval {
|
||||
my ($checksum, $checksum_algorithm) = $param->@{'checksum', 'checksum-algorithm'};
|
||||
if ($checksum_algorithm) {
|
||||
print "calculating checksum...";
|
||||
|
||||
my $checksum_got = PVE::Tools::get_file_hash($checksum_algorithm, $tmpfilename);
|
||||
|
||||
if (lc($checksum_got) eq lc($checksum)) {
|
||||
print "OK, checksum verified\n";
|
||||
} else {
|
||||
print "\n"; # the front end expects the error to reside at the last line without any noise
|
||||
die "checksum mismatch: got '$checksum_got' != expect '$checksum'\n";
|
||||
}
|
||||
}
|
||||
};
|
||||
if (my $err = $@) {
|
||||
# unlinks only the temporary file from the http server
|
||||
unlink $tmpfilename;
|
||||
warn "unable to clean up temporory file '$tmpfilename' - $!\n"
|
||||
if $! && $! != ENOENT;
|
||||
die $err;
|
||||
}
|
||||
|
||||
print "target node: $node\n";
|
||||
print "target file: $dest\n";
|
||||
print "file size is: $size\n";
|
||||
print "command: " . join(' ', @$cmd) . "\n";
|
||||
|
||||
eval { run_command($cmd, errmsg => 'import failed'); };
|
||||
|
||||
unlink $tmpfilename; # the temporary file got only uploaded locally, no need to rm remote
|
||||
warn "unable to clean up temporary file '$tmpfilename' - $!\n" if $! && $! != ENOENT;
|
||||
|
||||
if (my $err = $@) {
|
||||
eval { $err_cleanup->() };
|
||||
warn "$@" if $@;
|
||||
die $err;
|
||||
}
|
||||
print "finished file import successfully\n";
|
||||
};
|
||||
|
||||
return $rpcenv->fork_worker('imgcopy', undef, $user, $worker);
|
||||
}});
|
||||
|
||||
__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_EXT_RE_0$!) {
|
||||
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_EXT_RE_1$!) {
|
||||
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;
|
||||
Reference in New Issue
Block a user