separate packaging and source build system

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
Thomas Lamprecht
2023-05-24 16:20:27 +02:00
parent f5e87de606
commit a2242b41fc
213 changed files with 66 additions and 47 deletions

318
src/PVE/API2/Disks.pm Normal file
View 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;

View 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
View 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;

View 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;

View 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
View 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
View 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
View 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;

View 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;

View 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;

View 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

View 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;

View 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;

View 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;