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

View File

@ -0,0 +1,933 @@
package PVE::Storage::BTRFSPlugin;
use strict;
use warnings;
use base qw(PVE::Storage::Plugin);
use Fcntl qw(S_ISDIR O_WRONLY O_CREAT O_EXCL);
use File::Basename qw(basename dirname);
use File::Path qw(mkpath);
use IO::Dir;
use POSIX qw(EEXIST);
use PVE::Tools qw(run_command dir_glob_foreach);
use PVE::Storage::DirPlugin;
use constant {
BTRFS_FIRST_FREE_OBJECTID => 256,
FS_NOCOW_FL => 0x00800000,
FS_IOC_GETFLAGS => 0x40086602,
FS_IOC_SETFLAGS => 0x80086601,
BTRFS_MAGIC => 0x9123683e,
};
# Configuration (similar to DirPlugin)
sub type {
return 'btrfs';
}
sub plugindata {
return {
content => [
{
images => 1,
rootdir => 1,
vztmpl => 1,
iso => 1,
backup => 1,
snippets => 1,
none => 1,
},
{ images => 1, rootdir => 1 },
],
format => [ { raw => 1, subvol => 1 }, 'raw', ],
};
}
sub properties {
return {
nocow => {
description => "Set the NOCOW flag on files."
. " Disables data checksumming and causes data errors to be unrecoverable from"
. " while allowing direct I/O. Only use this if data does not need to be any more"
. " safe than on a single ext4 formatted disk with no underlying raid system.",
type => 'boolean',
default => 0,
},
};
}
sub options {
return {
path => { fixed => 1 },
nodes => { optional => 1 },
shared => { optional => 1 },
disable => { optional => 1 },
maxfiles => { optional => 1 },
'prune-backups' => { optional => 1 },
'max-protected-backups' => { optional => 1 },
content => { optional => 1 },
format => { optional => 1 },
is_mountpoint => { optional => 1 },
nocow => { optional => 1 },
mkdir => { optional => 1 },
preallocation => { optional => 1 },
# TODO: The new variant of mkdir with `populate` vs `create`...
};
}
# Storage implementation
#
# We use the same volume names are directory plugins, but map *raw* disk image file names into a
# subdirectory.
#
# `vm-VMID-disk-ID.raw`
# -> `images/VMID/vm-VMID-disk-ID/disk.raw`
# where the `vm-VMID-disk-ID/` subdirectory is a btrfs subvolume
# Reuse `DirPlugin`'s `check_config`. This simply checks for invalid paths.
sub check_config {
my ($self, $sectionId, $config, $create, $skipSchemaCheck) = @_;
return PVE::Storage::DirPlugin::check_config($self, $sectionId, $config, $create, $skipSchemaCheck);
}
my sub getfsmagic($) {
my ($path) = @_;
# The field type sizes in `struct statfs` are defined in a rather annoying way, and we only
# need the first field, which is a `long` for our supported platforms.
# Should be moved to pve-rs, so this can be the problem of the `libc` crate ;-)
# Just round up and extract what we need:
my $buf = pack('x160');
if (0 != syscall(&PVE::Syscall::SYS_statfs, $path, $buf)) {
die "statfs on '$path' failed - $!\n";
}
return unpack('L!', $buf);
}
my sub assert_btrfs($) {
my ($path) = @_;
die "'$path' is not a btrfs file system\n"
if getfsmagic($path) != BTRFS_MAGIC;
}
sub activate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
my $path = $scfg->{path};
if (!defined($scfg->{mkdir}) || $scfg->{mkdir}) {
mkpath $path;
}
my $mp = PVE::Storage::DirPlugin::parse_is_mountpoint($scfg);
if (defined($mp) && !PVE::Storage::DirPlugin::path_is_mounted($mp, $cache->{mountdata})) {
die "unable to activate storage '$storeid' - directory is expected to be a mount point but"
." is not mounted: '$mp'\n";
}
assert_btrfs($path); # only assert this stuff now, ensures $path is there and better UX
$class->SUPER::activate_storage($storeid, $scfg, $cache);
}
sub status {
my ($class, $storeid, $scfg, $cache) = @_;
return PVE::Storage::DirPlugin::status($class, $storeid, $scfg, $cache);
}
sub get_volume_attribute {
my ($class, $scfg, $storeid, $volname, $attribute) = @_;
return PVE::Storage::DirPlugin::get_volume_attribute($class, $scfg, $storeid, $volname, $attribute);
}
sub update_volume_attribute {
my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_;
return PVE::Storage::DirPlugin::update_volume_attribute(
$class,
$scfg,
$storeid,
$volname,
$attribute,
$value,
);
}
# croak would not include the caller from within this module
sub __error {
my ($msg) = @_;
my (undef, $f, $n) = caller(1);
die "$msg at $f: $n\n";
}
# Given a name (eg. `vm-VMID-disk-ID.raw`), take the part up to the format suffix as the name of
# the subdirectory (subvolume).
sub raw_name_to_dir($) {
my ($raw) = @_;
# For the subvolume directory Strip the `.<format>` suffix:
if ($raw =~ /^(.*)\.raw$/) {
return $1;
}
__error "internal error: bad disk name: $raw";
}
sub raw_file_to_subvol($) {
my ($file) = @_;
if ($file =~ m|^(.*)/disk\.raw$|) {
return "$1";
}
__error "internal error: bad raw path: $file";
}
sub filesystem_path {
my ($class, $scfg, $volname, $snapname) = @_;
my ($vtype, $name, $vmid, undef, undef, $isBase, $format) =
$class->parse_volname($volname);
my $path = $class->get_subdir($scfg, $vtype);
$path .= "/$vmid" if $vtype eq 'images';
if (defined($format) && $format eq 'raw') {
my $dir = raw_name_to_dir($name);
if ($snapname) {
$dir .= "\@$snapname";
}
$path .= "/$dir/disk.raw";
} elsif (defined($format) && $format eq 'subvol') {
$path .= "/$name";
if ($snapname) {
$path .= "\@$snapname";
}
} else {
$path .= "/$name";
}
return wantarray ? ($path, $vmid, $vtype) : $path;
}
sub btrfs_cmd {
my ($class, $cmd, $outfunc) = @_;
my $msg = '';
my $func;
if (defined($outfunc)) {
$func = sub {
my $part = &$outfunc(@_);
$msg .= $part if defined($part);
};
} else {
$func = sub { $msg .= "$_[0]\n" };
}
run_command(['btrfs', '-q', @$cmd], errmsg => 'btrfs error', outfunc => $func);
return $msg;
}
sub btrfs_get_subvol_id {
my ($class, $path) = @_;
my $info = $class->btrfs_cmd(['subvolume', 'show', '--', $path]);
if ($info !~ /^\s*(?:Object|Subvolume) ID:\s*(\d+)$/m) {
die "failed to get btrfs subvolume ID from: $info\n";
}
return $1;
}
my sub chattr : prototype($$$) {
my ($fh, $mask, $xor) = @_;
my $flags = pack('L!', 0);
ioctl($fh, FS_IOC_GETFLAGS, $flags) or die "FS_IOC_GETFLAGS failed - $!\n";
$flags = pack('L!', (unpack('L!', $flags) & $mask) ^ $xor);
ioctl($fh, FS_IOC_SETFLAGS, $flags) or die "FS_IOC_SETFLAGS failed - $!\n";
return 1;
}
sub create_base {
my ($class, $storeid, $scfg, $volname) = @_;
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) =
$class->parse_volname($volname);
my $newname = $name;
$newname =~ s/^vm-/base-/;
# If we're not working with a 'raw' file, which is the only thing that's "different" for btrfs,
# or a subvolume, we forward to the DirPlugin
if ($format ne 'raw' && $format ne 'subvol') {
return PVE::Storage::DirPlugin::create_base(@_);
}
my $path = $class->filesystem_path($scfg, $volname);
my $newvolname = $basename ? "$basevmid/$basename/$vmid/$newname" : "$vmid/$newname";
my $newpath = $class->filesystem_path($scfg, $newvolname);
my $subvol = $path;
my $newsubvol = $newpath;
if ($format eq 'raw') {
$subvol = raw_file_to_subvol($subvol);
$newsubvol = raw_file_to_subvol($newsubvol);
}
rename($subvol, $newsubvol)
|| die "rename '$subvol' to '$newsubvol' failed - $!\n";
eval { $class->btrfs_cmd(['property', 'set', $newsubvol, 'ro', 'true']) };
warn $@ if $@;
return $newvolname;
}
sub clone_image {
my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
my ($vtype, $basename, $basevmid, undef, undef, $isBase, $format) =
$class->parse_volname($volname);
# If we're not working with a 'raw' file, which is the only thing that's "different" for btrfs,
# or a subvolume, we forward to the DirPlugin
if ($format ne 'raw' && $format ne 'subvol') {
return PVE::Storage::DirPlugin::clone_image(@_);
}
my $imagedir = $class->get_subdir($scfg, 'images');
$imagedir .= "/$vmid";
mkpath $imagedir;
my $path = $class->filesystem_path($scfg, $volname);
my $newname = $class->find_free_diskname($storeid, $scfg, $vmid, $format, 1);
# For btrfs subvolumes we don't actually need the "link":
#my $newvolname = "$basevmid/$basename/$vmid/$newname";
my $newvolname = "$vmid/$newname";
my $newpath = $class->filesystem_path($scfg, $newvolname);
my $subvol = $path;
my $newsubvol = $newpath;
if ($format eq 'raw') {
$subvol = raw_file_to_subvol($subvol);
$newsubvol = raw_file_to_subvol($newsubvol);
}
$class->btrfs_cmd(['subvolume', 'snapshot', '--', $subvol, $newsubvol]);
return $newvolname;
}
sub alloc_image {
my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
if ($fmt ne 'raw' && $fmt ne 'subvol') {
return $class->SUPER::alloc_image($storeid, $scfg, $vmid, $fmt, $name, $size);
}
# From Plugin.pm:
my $imagedir = $class->get_subdir($scfg, 'images') . "/$vmid";
mkpath $imagedir;
$name = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt, 1) if !$name;
my (undef, $tmpfmt) = PVE::Storage::Plugin::parse_name_dir($name);
die "illegal name '$name' - wrong extension for format ('$tmpfmt != '$fmt')\n"
if $tmpfmt ne $fmt;
# End copy from Plugin.pm
my $subvol = "$imagedir/$name";
# .raw is not part of the directory name
$subvol =~ s/\.raw$//;
die "disk image '$subvol' already exists\n" if -e $subvol;
my $path;
if ($fmt eq 'raw') {
$path = "$subvol/disk.raw";
}
if ($fmt eq 'subvol' && !!$size) {
# NOTE: `btrfs send/recv` actually drops quota information so supporting subvolumes with
# quotas doesn't play nice with send/recv.
die "btrfs quotas are currently not supported, use an unsized subvolume or a raw file\n";
}
$class->btrfs_cmd(['subvolume', 'create', '--', $subvol]);
eval {
if ($fmt eq 'subvol') {
# Nothing to do for now...
# This is how we *would* do it:
# # Use the subvol's default 0/$id qgroup
# eval {
# # This call should happen at storage creation instead and therefore governed by a
# # configuration option!
# # $class->btrfs_cmd(['quota', 'enable', $subvol]);
# my $id = $class->btrfs_get_subvol_id($subvol);
# $class->btrfs_cmd(['qgroup', 'limit', "${size}k", "0/$id", $subvol]);
# };
} elsif ($fmt eq 'raw') {
sysopen my $fh, $path, O_WRONLY | O_CREAT | O_EXCL
or die "failed to create raw file '$path' - $!\n";
chattr($fh, ~FS_NOCOW_FL, FS_NOCOW_FL) if $scfg->{nocow};
truncate($fh, $size * 1024)
or die "failed to set file size for '$path' - $!\n";
close($fh);
} else {
die "internal format error (format = $fmt)\n";
}
};
if (my $err = $@) {
eval { $class->btrfs_cmd(['subvolume', 'delete', '--', $subvol]); };
warn $@ if $@;
die $err;
}
return "$vmid/$name";
}
# Same as btrfsprogs does:
my sub path_is_subvolume : prototype($) {
my ($path) = @_;
my @stat = stat($path)
or die "stat failed on '$path' - $!\n";
my ($ino, $mode) = @stat[1, 2];
return S_ISDIR($mode) && $ino == BTRFS_FIRST_FREE_OBJECTID;
}
my $BTRFS_VOL_REGEX = qr/((?:vm|base|subvol)-\d+-disk-\d+(?:\.subvol)?)(?:\@(\S+))$/;
# Calls `$code->($volume, $name, $snapshot)` for each subvol in a directory matching our volume
# regex.
my sub foreach_subvol : prototype($$) {
my ($dir, $code) = @_;
dir_glob_foreach($dir, $BTRFS_VOL_REGEX, sub {
my ($volume, $name, $snapshot) = ($1, $2, $3);
return if !path_is_subvolume("$dir/$volume");
$code->($volume, $name, $snapshot);
})
}
sub free_image {
my ($class, $storeid, $scfg, $volname, $isBase, $_format) = @_;
my (undef, undef, $vmid, undef, undef, undef, $format) =
$class->parse_volname($volname);
if (!defined($format) || ($format ne 'subvol' && $format ne 'raw')) {
return $class->SUPER::free_image($storeid, $scfg, $volname, $isBase, $_format);
}
my $path = $class->filesystem_path($scfg, $volname);
my $subvol = $path;
if ($format eq 'raw') {
$subvol = raw_file_to_subvol($path);
}
my $dir = dirname($subvol);
my $basename = basename($subvol);
my @snapshot_vols;
foreach_subvol($dir, sub {
my ($volume, $name, $snapshot) = @_;
return if $name ne $basename;
return if !defined $snapshot;
push @snapshot_vols, "$dir/$volume";
});
$class->btrfs_cmd(['subvolume', 'delete', '--', @snapshot_vols, $subvol]);
# try to cleanup directory to not clutter storage with empty $vmid dirs if
# all images from a guest got deleted
rmdir($dir);
return undef;
}
# Currently not used because quotas clash with send/recv.
# my sub btrfs_subvol_quota {
# my ($class, $path) = @_;
# my $id = '0/' . $class->btrfs_get_subvol_id($path);
# my $search = qr/^\Q$id\E\s+(\d)+\s+\d+\s+(\d+)\s*$/;
# my ($used, $size);
# $class->btrfs_cmd(['qgroup', 'show', '--raw', '-rf', '--', $path], sub {
# return if defined($size);
# if ($_[0] =~ $search) {
# ($used, $size) = ($1, $2);
# }
# });
# if (!defined($size)) {
# # syslog should include more information:
# syslog('err', "failed to get subvolume size for: $path (id $id)");
# # UI should only see the last path component:
# $path =~ s|^.*/||;
# die "failed to get subvolume size for $path\n";
# }
# return wantarray ? ($used, $size) : $size;
# }
sub volume_size_info {
my ($class, $scfg, $storeid, $volname, $timeout) = @_;
my $path = $class->filesystem_path($scfg, $volname);
my $format = ($class->parse_volname($volname))[6];
if (defined($format) && $format eq 'subvol') {
my $ctime = (stat($path))[10];
my ($used, $size) = (0, 0);
#my ($used, $size) = btrfs_subvol_quota($class, $path); # uses wantarray
return wantarray ? ($size, 'subvol', $used, undef, $ctime) : 1;
}
return PVE::Storage::Plugin::file_size_info($path, $timeout);
}
sub volume_resize {
my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
my $format = ($class->parse_volname($volname))[6];
if ($format eq 'subvol') {
my $path = $class->filesystem_path($scfg, $volname);
my $id = '0/' . $class->btrfs_get_subvol_id($path);
$class->btrfs_cmd(['qgroup', 'limit', '--', "${size}k", "0/$id", $path]);
return undef;
}
return PVE::Storage::Plugin::volume_resize(@_);
}
sub volume_snapshot {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
my ($name, $vmid, $format) = ($class->parse_volname($volname))[1,2,6];
if ($format ne 'subvol' && $format ne 'raw') {
return PVE::Storage::Plugin::volume_snapshot(@_);
}
my $path = $class->filesystem_path($scfg, $volname);
my $snap_path = $class->filesystem_path($scfg, $volname, $snap);
if ($format eq 'raw') {
$path = raw_file_to_subvol($path);
$snap_path = raw_file_to_subvol($snap_path);
}
my $snapshot_dir = $class->get_subdir($scfg, 'images') . "/$vmid";
mkpath $snapshot_dir;
$class->btrfs_cmd(['subvolume', 'snapshot', '-r', '--', $path, $snap_path]);
return undef;
}
sub volume_rollback_is_possible {
my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_;
return 1;
}
sub volume_snapshot_rollback {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
my ($name, $format) = ($class->parse_volname($volname))[1,6];
if ($format ne 'subvol' && $format ne 'raw') {
return PVE::Storage::Plugin::volume_snapshot_rollback(@_);
}
my $path = $class->filesystem_path($scfg, $volname);
my $snap_path = $class->filesystem_path($scfg, $volname, $snap);
if ($format eq 'raw') {
$path = raw_file_to_subvol($path);
$snap_path = raw_file_to_subvol($snap_path);
}
# Simple version would be:
# rename old to temp
# create new
# on error rename temp back
# But for atomicity in case the rename after create-failure *also* fails, we create the new
# subvol first, then use RENAME_EXCHANGE,
my $tmp_path = "$path.tmp.$$";
$class->btrfs_cmd(['subvolume', 'snapshot', '--', $snap_path, $tmp_path]);
# The paths are absolute, so pass -1 as file descriptors.
my $ok = PVE::Tools::renameat2(-1, $tmp_path, -1, $path, &PVE::Tools::RENAME_EXCHANGE);
eval { $class->btrfs_cmd(['subvolume', 'delete', '--', $tmp_path]) };
warn "failed to remove '$tmp_path' subvolume: $@" if $@;
if (!$ok) {
die "failed to rotate '$tmp_path' into place at '$path' - $!\n";
}
return undef;
}
sub volume_snapshot_delete {
my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
my ($name, $vmid, $format) = ($class->parse_volname($volname))[1,2,6];
if ($format ne 'subvol' && $format ne 'raw') {
return PVE::Storage::Plugin::volume_snapshot_delete(@_);
}
my $path = $class->filesystem_path($scfg, $volname, $snap);
if ($format eq 'raw') {
$path = raw_file_to_subvol($path);
}
$class->btrfs_cmd(['subvolume', 'delete', '--', $path]);
return undef;
}
sub volume_has_feature {
my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
my $features = {
snapshot => {
current => { qcow2 => 1, raw => 1, subvol => 1 },
snap => { qcow2 => 1, raw => 1, subvol => 1 }
},
clone => {
base => { qcow2 => 1, raw => 1, subvol => 1, vmdk => 1 },
current => { raw => 1 },
snap => { raw => 1 },
},
template => {
current => { qcow2 => 1, raw => 1, vmdk => 1, subvol => 1 },
},
copy => {
base => { qcow2 => 1, raw => 1, subvol => 1, vmdk => 1 },
current => { qcow2 => 1, raw => 1, subvol => 1, vmdk => 1 },
snap => { qcow2 => 1, raw => 1, subvol => 1 },
},
sparseinit => {
base => { qcow2 => 1, raw => 1, vmdk => 1 },
current => { qcow2 => 1, raw => 1, vmdk => 1 },
},
};
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = $class->parse_volname($volname);
my $key = undef;
if ($snapname) {
$key = 'snap';
} else {
$key = $isBase ? 'base' : 'current';
}
return 1 if defined($features->{$feature}->{$key}->{$format});
return undef;
}
sub list_images {
my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
my $imagedir = $class->get_subdir($scfg, 'images');
my $res = [];
# Copied from Plugin.pm, with file_size_info calls adapted:
foreach my $fn (<$imagedir/[0-9][0-9]*/*>) {
# different to in Plugin.pm the regex below also excludes '@' as valid file name
next if $fn !~ m@^(/.+/(\d+)/([^/\@.]+(?:\.(qcow2|vmdk|subvol))?))$@;
$fn = $1; # untaint
my $owner = $2;
my $name = $3;
my $ext = $4;
next if !$vollist && defined($vmid) && ($owner ne $vmid);
my $volid = "$storeid:$owner/$name";
my ($size, $format, $used, $parent, $ctime);
if (!$ext) { # raw
$volid .= '.raw';
($size, $format, $used, $parent, $ctime) = PVE::Storage::Plugin::file_size_info("$fn/disk.raw");
} elsif ($ext eq 'subvol') {
($used, $size) = (0, 0);
#($used, $size) = btrfs_subvol_quota($class, $fn);
$format = 'subvol';
} else {
($size, $format, $used, $parent, $ctime) = PVE::Storage::Plugin::file_size_info($fn);
}
next if !($format && defined($size));
if ($vollist) {
next if ! grep { $_ eq $volid } @$vollist;
}
my $info = {
volid => $volid, format => $format,
size => $size, vmid => $owner, used => $used, parent => $parent,
};
$info->{ctime} = $ctime if $ctime;
push @$res, $info;
}
return $res;
}
sub volume_export_formats {
my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
# We can do whatever `DirPlugin` can do.
my @result = PVE::Storage::Plugin::volume_export_formats(@_);
# `btrfs send` only works on snapshots:
return @result if !defined $snapshot;
# Incremental stream with snapshots is only supported if the snapshots are listed (new api):
return @result if defined($base_snapshot) && $with_snapshots && ref($with_snapshots) ne 'ARRAY';
# Otherwise we do also support `with_snapshots`.
# Finally, `btrfs send` only works on formats where we actually use btrfs subvolumes:
my $format = ($class->parse_volname($volname))[6];
return @result if $format ne 'raw' && $format ne 'subvol';
return ('btrfs', @result);
}
sub volume_import_formats {
my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
# Same as export-formats, beware the parameter order:
return volume_export_formats(
$class,
$scfg,
$storeid,
$volname,
$snapshot,
$base_snapshot,
$with_snapshots,
);
}
sub volume_export {
my (
$class,
$scfg,
$storeid,
$fh,
$volname,
$format,
$snapshot,
$base_snapshot,
$with_snapshots,
) = @_;
if ($format ne 'btrfs') {
return PVE::Storage::Plugin::volume_export(@_);
}
die "format 'btrfs' only works on snapshots\n"
if !defined $snapshot;
die "'btrfs' format in incremental mode requires snapshots to be listed explicitly\n"
if defined($base_snapshot) && $with_snapshots && ref($with_snapshots) ne 'ARRAY';
my $volume_format = ($class->parse_volname($volname))[6];
die "btrfs-sending volumes of type $volume_format ('$volname') is not supported\n"
if $volume_format ne 'raw' && $volume_format ne 'subvol';
my $path = $class->path($scfg, $volname, $storeid);
if ($volume_format eq 'raw') {
$path = raw_file_to_subvol($path);
}
my $cmd = ['btrfs', '-q', 'send', '-e'];
if ($base_snapshot) {
my $base = $class->path($scfg, $volname, $storeid, $base_snapshot);
if ($volume_format eq 'raw') {
$base = raw_file_to_subvol($base);
}
push @$cmd, '-p', $base;
}
push @$cmd, '--';
if (ref($with_snapshots) eq 'ARRAY') {
push @$cmd, (map { "$path\@$_" } ($with_snapshots // [])->@*), $path;
} else {
dir_glob_foreach(dirname($path), $BTRFS_VOL_REGEX, sub {
push @$cmd, "$path\@$_[2]" if !(defined($snapshot) && $_[2] eq $snapshot);
});
}
$path .= "\@$snapshot" if defined($snapshot);
push @$cmd, $path;
run_command($cmd, output => '>&'.fileno($fh));
return;
}
sub volume_import {
my (
$class,
$scfg,
$storeid,
$fh,
$volname,
$format,
$snapshot,
$base_snapshot,
$with_snapshots,
$allow_rename,
) = @_;
if ($format ne 'btrfs') {
return PVE::Storage::Plugin::volume_import(@_);
}
die "format 'btrfs' only works on snapshots\n"
if !defined $snapshot;
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $volume_format) =
$class->parse_volname($volname);
die "btrfs-receiving volumes of type $volume_format ('$volname') is not supported\n"
if $volume_format ne 'raw' && $volume_format ne 'subvol';
if (defined($base_snapshot)) {
my $path = $class->path($scfg, $volname, $storeid, $base_snapshot);
die "base snapshot '$base_snapshot' not found - no such directory '$path'\n"
if !path_is_subvolume($path);
}
my $destination = $class->filesystem_path($scfg, $volname);
if ($volume_format eq 'raw') {
$destination = raw_file_to_subvol($destination);
}
if (!defined($base_snapshot) && -e $destination) {
die "volume $volname already exists\n" if !$allow_rename;
$volname = $class->find_free_diskname($storeid, $scfg, $vmid, $volume_format, 1);
}
my $imagedir = $class->get_subdir($scfg, $vtype);
$imagedir .= "/$vmid" if $vtype eq 'images';
my $tmppath = "$imagedir/recv.$vmid.tmp";
mkdir($imagedir); # FIXME: if $scfg->{mkdir};
if (!mkdir($tmppath)) {
die "temp receive directory already exists at '$tmppath', incomplete concurrent import?\n"
if $! == EEXIST;
die "failed to create temporary receive directory at '$tmppath' - $!\n";
}
my $dh = IO::Dir->new($tmppath)
or die "failed to open temporary receive directory '$tmppath' - $!\n";
eval {
run_command(['btrfs', '-q', 'receive', '-e', '--', $tmppath], input => '<&'.fileno($fh));
# Analyze the received subvolumes;
my ($diskname, $found_snapshot, @snapshots);
$dh->rewind;
while (defined(my $entry = $dh->read)) {
next if $entry eq '.' || $entry eq '..';
next if $entry !~ /^$BTRFS_VOL_REGEX$/;
my ($cur_diskname, $cur_snapshot) = ($1, $2);
die "send stream included a non-snapshot subvolume\n"
if !defined($cur_snapshot);
if (!defined($diskname)) {
$diskname = $cur_diskname;
} else {
die "multiple disks contained in stream ('$diskname' vs '$cur_diskname')\n"
if $diskname ne $cur_diskname;
}
if ($cur_snapshot eq $snapshot) {
$found_snapshot = 1;
} else {
push @snapshots, $cur_snapshot;
}
}
die "send stream did not contain the expected current snapshot '$snapshot'\n"
if !$found_snapshot;
# Rotate the disk into place, first the current state:
# Note that read-only subvolumes cannot be moved into different directories, but for the
# "current" state we also want a writable copy, so start with that:
$class->btrfs_cmd(['property', 'set', "$tmppath/$diskname\@$snapshot", 'ro', 'false']);
PVE::Tools::renameat2(
-1,
"$tmppath/$diskname\@$snapshot",
-1,
$destination,
&PVE::Tools::RENAME_NOREPLACE,
) or die "failed to move received snapshot '$tmppath/$diskname\@$snapshot'"
. " into place at '$destination' - $!\n";
# Now recreate the actual snapshot:
$class->btrfs_cmd([
'subvolume',
'snapshot',
'-r',
'--',
$destination,
"$destination\@$snapshot",
]);
# Now go through the remaining snapshots (if any)
foreach my $snap (@snapshots) {
$class->btrfs_cmd(['property', 'set', "$tmppath/$diskname\@$snap", 'ro', 'false']);
PVE::Tools::renameat2(
-1,
"$tmppath/$diskname\@$snap",
-1,
"$destination\@$snap",
&PVE::Tools::RENAME_NOREPLACE,
) or die "failed to move received snapshot '$tmppath/$diskname\@$snap'"
. " into place at '$destination\@$snap' - $!\n";
eval { $class->btrfs_cmd(['property', 'set', "$destination\@$snap", 'ro', 'true']) };
warn "failed to make $destination\@$snap read-only - $!\n" if $@;
}
};
my $err = $@;
eval {
# Cleanup all the received snapshots we did not move into place, so we can remove the temp
# directory.
if ($dh) {
$dh->rewind;
while (defined(my $entry = $dh->read)) {
next if $entry eq '.' || $entry eq '..';
eval { $class->btrfs_cmd(['subvolume', 'delete', '--', "$tmppath/$entry"]) };
warn $@ if $@;
}
$dh->close; undef $dh;
}
if (!rmdir($tmppath)) {
warn "failed to remove temporary directory '$tmppath' - $!\n"
}
};
warn $@ if $@;
if ($err) {
# clean up if the directory ended up being empty after an error
rmdir($tmppath);
die $err;
}
return "$storeid:$volname";
}
1

View File

@ -0,0 +1,313 @@
package PVE::Storage::CIFSPlugin;
use strict;
use warnings;
use Net::IP;
use PVE::Tools qw(run_command);
use PVE::ProcFSTools;
use File::Path;
use PVE::Storage::Plugin;
use PVE::JSONSchema qw(get_standard_option);
use base qw(PVE::Storage::Plugin);
# CIFS helper functions
sub cifs_is_mounted : prototype($$) {
my ($scfg, $mountdata) = @_;
my ($mountpoint, $server, $share) = $scfg->@{'path', 'server', 'share'};
my $subdir = $scfg->{subdir} // '';
$server = "[$server]" if Net::IP::ip_is_ipv6($server);
my $source = "//${server}/$share$subdir";
$mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata;
return $mountpoint if grep {
$_->[2] =~ /^cifs/ &&
$_->[0] =~ m|^\Q$source\E/?$| &&
$_->[1] eq $mountpoint
} @$mountdata;
return undef;
}
sub cifs_cred_file_name {
my ($storeid) = @_;
return "/etc/pve/priv/storage/${storeid}.pw";
}
sub cifs_delete_credentials {
my ($storeid) = @_;
if (my $cred_file = get_cred_file($storeid)) {
unlink($cred_file) or warn "removing cifs credientials '$cred_file' failed: $!\n";
}
}
sub cifs_set_credentials {
my ($password, $storeid) = @_;
my $cred_file = cifs_cred_file_name($storeid);
mkdir "/etc/pve/priv/storage";
PVE::Tools::file_set_contents($cred_file, "password=$password\n");
return $cred_file;
}
sub get_cred_file {
my ($storeid) = @_;
my $cred_file = cifs_cred_file_name($storeid);
if (-e $cred_file) {
return $cred_file;
}
return undef;
}
sub cifs_mount : prototype($$$$$) {
my ($scfg, $storeid, $smbver, $user, $domain) = @_;
my ($mountpoint, $server, $share) = $scfg->@{'path', 'server', 'share'};
my $subdir = $scfg->{subdir} // '';
$server = "[$server]" if Net::IP::ip_is_ipv6($server);
my $source = "//${server}/$share$subdir";
my $cmd = ['/bin/mount', '-t', 'cifs', $source, $mountpoint, '-o', 'soft', '-o'];
if (my $cred_file = get_cred_file($storeid)) {
push @$cmd, "username=$user", '-o', "credentials=$cred_file";
push @$cmd, '-o', "domain=$domain" if defined($domain);
} else {
push @$cmd, 'guest,username=guest';
}
push @$cmd, '-o', defined($smbver) ? "vers=$smbver" : "vers=default";
run_command($cmd, errmsg => "mount error");
}
# Configuration
sub type {
return 'cifs';
}
sub plugindata {
return {
content => [ { images => 1, rootdir => 1, vztmpl => 1, iso => 1,
backup => 1, snippets => 1}, { images => 1 }],
format => [ { raw => 1, qcow2 => 1, vmdk => 1 } , 'raw' ],
};
}
sub properties {
return {
share => {
description => "CIFS share.",
type => 'string',
},
password => {
description => "Password for accessing the share/datastore.",
type => 'string',
maxLength => 256,
},
domain => {
description => "CIFS domain.",
type => 'string',
optional => 1,
maxLength => 256,
},
smbversion => {
description => "SMB protocol version. 'default' if not set, negotiates the highest SMB2+"
." version supported by both the client and server.",
type => 'string',
default => 'default',
enum => ['default', '2.0', '2.1', '3', '3.0', '3.11'],
optional => 1,
},
};
}
sub options {
return {
path => { fixed => 1 },
'content-dirs' => { optional => 1 },
server => { fixed => 1 },
share => { fixed => 1 },
subdir => { optional => 1 },
nodes => { optional => 1 },
disable => { optional => 1 },
maxfiles => { optional => 1 },
'prune-backups' => { optional => 1 },
'max-protected-backups' => { optional => 1 },
content => { optional => 1 },
format => { optional => 1 },
username => { optional => 1 },
password => { optional => 1},
domain => { optional => 1},
smbversion => { optional => 1},
mkdir => { optional => 1 },
bwlimit => { optional => 1 },
preallocation => { optional => 1 },
};
}
sub check_config {
my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
$config->{path} = "/mnt/pve/$sectionId" if $create && !$config->{path};
return $class->SUPER::check_config($sectionId, $config, $create, $skipSchemaCheck);
}
# Storage implementation
sub on_add_hook {
my ($class, $storeid, $scfg, %sensitive) = @_;
if (defined($sensitive{password})) {
cifs_set_credentials($sensitive{password}, $storeid);
if (!exists($scfg->{username})) {
warn "storage $storeid: ignoring password parameter, no user set\n";
}
} else {
cifs_delete_credentials($storeid);
}
return;
}
sub on_update_hook {
my ($class, $storeid, $scfg, %sensitive) = @_;
return if !exists($sensitive{password});
if (defined($sensitive{password})) {
cifs_set_credentials($sensitive{password}, $storeid);
if (!exists($scfg->{username})) {
warn "storage $storeid: ignoring password parameter, no user set\n";
}
} else {
cifs_delete_credentials($storeid);
}
return;
}
sub on_delete_hook {
my ($class, $storeid, $scfg) = @_;
cifs_delete_credentials($storeid);
return;
}
sub status {
my ($class, $storeid, $scfg, $cache) = @_;
$cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
if !$cache->{mountdata};
return undef
if !cifs_is_mounted($scfg, $cache->{mountdata});
return $class->SUPER::status($storeid, $scfg, $cache);
}
sub activate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
$cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
if !$cache->{mountdata};
my $path = $scfg->{path};
if (!cifs_is_mounted($scfg, $cache->{mountdata})) {
mkpath $path if !(defined($scfg->{mkdir}) && !$scfg->{mkdir});
die "unable to activate storage '$storeid' - " .
"directory '$path' does not exist\n" if ! -d $path;
cifs_mount($scfg, $storeid, $scfg->{smbversion},
$scfg->{username}, $scfg->{domain});
}
$class->SUPER::activate_storage($storeid, $scfg, $cache);
}
sub deactivate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
$cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
if !$cache->{mountdata};
my $path = $scfg->{path};
if (cifs_is_mounted($scfg, $cache->{mountdata})) {
my $cmd = ['/bin/umount', $path];
run_command($cmd, errmsg => 'umount error');
}
}
sub check_connection {
my ($class, $storeid, $scfg) = @_;
my $servicename = '//'.$scfg->{server}.'/'.$scfg->{share};
my $cmd = ['/usr/bin/smbclient', $servicename, '-d', '0'];
if (defined($scfg->{smbversion}) && $scfg->{smbversion} ne 'default') {
# max-protocol version, so basically only relevant for smb2 vs smb3
push @$cmd, '-m', "smb" . int($scfg->{smbversion});
}
if (my $cred_file = get_cred_file($storeid)) {
push @$cmd, '-U', $scfg->{username}, '-A', $cred_file;
push @$cmd, '-W', $scfg->{domain} if defined($scfg->{domain});
} else {
push @$cmd, '-U', 'Guest','-N';
}
push @$cmd, '-c', 'echo 1 0';
my $out_str;
my $out = sub { $out_str .= shift };
eval { run_command($cmd, timeout => 10, outfunc => $out, errfunc => sub {}) };
if (my $err = $@) {
die "$out_str\n" if defined($out_str) &&
($out_str =~ m/NT_STATUS_(ACCESS_DENIED|LOGON_FAILURE)/);
return 0;
}
return 1;
}
# FIXME remove on the next APIAGE reset.
# Deprecated, use get_volume_attribute instead.
sub get_volume_notes {
my $class = shift;
PVE::Storage::DirPlugin::get_volume_notes($class, @_);
}
# FIXME remove on the next APIAGE reset.
# Deprecated, use update_volume_attribute instead.
sub update_volume_notes {
my $class = shift;
PVE::Storage::DirPlugin::update_volume_notes($class, @_);
}
sub get_volume_attribute {
return PVE::Storage::DirPlugin::get_volume_attribute(@_);
}
sub update_volume_attribute {
return PVE::Storage::DirPlugin::update_volume_attribute(@_);
}
1;

View File

@ -0,0 +1,262 @@
package PVE::Storage::CephFSPlugin;
use strict;
use warnings;
use IO::File;
use Net::IP;
use File::Path;
use PVE::CephConfig;
use PVE::JSONSchema qw(get_standard_option);
use PVE::ProcFSTools;
use PVE::Storage::Plugin;
use PVE::Systemd;
use PVE::Tools qw(run_command file_set_contents);
use base qw(PVE::Storage::Plugin);
sub cephfs_is_mounted {
my ($scfg, $storeid, $mountdata) = @_;
my $cmd_option = PVE::CephConfig::ceph_connect_option($scfg, $storeid);
my $configfile = $cmd_option->{ceph_conf};
my $subdir = $scfg->{subdir} // '/';
my $mountpoint = $scfg->{path};
$mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata;
return $mountpoint if grep {
$_->[2] =~ m#^ceph|fuse\.ceph-fuse# &&
$_->[0] =~ m#\Q:$subdir\E$|^ceph-fuse$# &&
$_->[1] eq $mountpoint
} @$mountdata;
warn "A filesystem is already mounted on $mountpoint\n"
if grep { $_->[1] eq $mountpoint } @$mountdata;
return undef;
}
# FIXME: remove once it's possible to specify _netdev for fuse.ceph mounts
sub systemd_netmount {
my ($where, $type, $what, $opts) = @_;
# don't do default deps, systemd v241 generator produces ordering deps on both
# local-fs(-pre) and remote-fs(-pre) targets if we use the required _netdev
# option. Over three corners this gets us an ordering cycle on shutdown, which
# may make shutdown hang if the random cycle breaking hits the "wrong" unit to
# delete.
my $unit = <<"EOF";
[Unit]
Description=${where}
DefaultDependencies=no
Requires=system.slice
Wants=network-online.target
Before=umount.target remote-fs.target
After=systemd-journald.socket system.slice network.target -.mount remote-fs-pre.target network-online.target
Conflicts=umount.target
[Mount]
Where=${where}
What=${what}
Type=${type}
Options=${opts}
EOF
my $unit_fn = PVE::Systemd::escape_unit($where, 1) . ".mount";
my $unit_path = "/run/systemd/system/$unit_fn";
my $daemon_needs_reload = -e $unit_path;
file_set_contents($unit_path, $unit);
run_command(['systemctl', 'daemon-reload'], errmsg => "daemon-reload error")
if $daemon_needs_reload;
run_command(['systemctl', 'start', $unit_fn], errmsg => "mount error");
}
sub cephfs_mount {
my ($scfg, $storeid) = @_;
my $mountpoint = $scfg->{path};
my $subdir = $scfg->{subdir} // '/';
my $cmd_option = PVE::CephConfig::ceph_connect_option($scfg, $storeid);
my $configfile = $cmd_option->{ceph_conf};
my $secretfile = $cmd_option->{keyring};
my $server = $cmd_option->{mon_host} // PVE::CephConfig::get_monaddr_list($configfile);
my $type = 'ceph';
my $fs_name = $scfg->{'fs-name'};
my @opts = ();
if ($scfg->{fuse}) {
$type = 'fuse.ceph';
push @opts, "ceph.id=$cmd_option->{userid}";
push @opts, "ceph.keyfile=$secretfile" if defined($secretfile);
push @opts, "ceph.conf=$configfile" if defined($configfile);
push @opts, "ceph.client_fs=$fs_name" if defined($fs_name);
} else {
push @opts, "name=$cmd_option->{userid}";
push @opts, "secretfile=$secretfile" if defined($secretfile);
push @opts, "conf=$configfile" if defined($configfile);
push @opts, "fs=$fs_name" if defined($fs_name);
}
push @opts, $scfg->{options} if $scfg->{options};
systemd_netmount($mountpoint, $type, "$server:$subdir", join(',', @opts));
}
# Configuration
sub type {
return 'cephfs';
}
sub plugindata {
return {
content => [ { vztmpl => 1, iso => 1, backup => 1, snippets => 1},
{ backup => 1 }],
};
}
sub properties {
return {
fuse => {
description => "Mount CephFS through FUSE.",
type => 'boolean',
},
'fs-name' => {
description => "The Ceph filesystem name.",
type => 'string', format => 'pve-configid',
},
};
}
sub options {
return {
path => { fixed => 1 },
'content-dirs' => { optional => 1 },
monhost => { optional => 1},
nodes => { optional => 1 },
subdir => { optional => 1 },
disable => { optional => 1 },
options => { optional => 1 },
username => { optional => 1 },
content => { optional => 1 },
format => { optional => 1 },
mkdir => { optional => 1 },
fuse => { optional => 1 },
bwlimit => { optional => 1 },
maxfiles => { optional => 1 },
keyring => { optional => 1 },
'prune-backups' => { optional => 1 },
'max-protected-backups' => { optional => 1 },
'fs-name' => { optional => 1 },
};
}
sub check_config {
my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
$config->{path} = "/mnt/pve/$sectionId" if $create && !$config->{path};
return $class->SUPER::check_config($sectionId, $config, $create, $skipSchemaCheck);
}
# Storage implementation
sub on_add_hook {
my ($class, $storeid, $scfg, %param) = @_;
PVE::CephConfig::ceph_create_keyfile($scfg->{type}, $storeid, $param{keyring});
return;
}
sub on_update_hook {
my ($class, $storeid, $scfg, %param) = @_;
if (exists($param{keyring})) {
if (defined($param{keyring})) {
PVE::CephConfig::ceph_create_keyfile($scfg->{type}, $storeid, $param{keyring});
} else {
PVE::CephConfig::ceph_remove_keyfile($scfg->{type}, $storeid);
}
}
return;
}
sub on_delete_hook {
my ($class, $storeid, $scfg) = @_;
PVE::CephConfig::ceph_remove_keyfile($scfg->{type}, $storeid);
return;
}
sub status {
my ($class, $storeid, $scfg, $cache) = @_;
$cache->{mountdata} //= PVE::ProcFSTools::parse_proc_mounts();
return undef if !cephfs_is_mounted($scfg, $storeid, $cache->{mountdata});
return $class->SUPER::status($storeid, $scfg, $cache);
}
sub activate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
$cache->{mountdata} //= PVE::ProcFSTools::parse_proc_mounts();
# NOTE: mkpath may hang if storage is mounted but not reachable
if (!cephfs_is_mounted($scfg, $storeid, $cache->{mountdata})) {
my $path = $scfg->{path};
mkpath $path if !(defined($scfg->{mkdir}) && !$scfg->{mkdir});
die "unable to activate storage '$storeid' - " .
"directory '$path' does not exist\n" if ! -d $path;
cephfs_mount($scfg, $storeid);
}
$class->SUPER::activate_storage($storeid, $scfg, $cache);
}
sub deactivate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
$cache->{mountdata} //= PVE::ProcFSTools::parse_proc_mounts();
my $path = $scfg->{path};
if (cephfs_is_mounted($scfg, $storeid, $cache->{mountdata})) {
run_command(['/bin/umount', $path], errmsg => 'umount error');
}
}
# FIXME remove on the next APIAGE reset.
# Deprecated, use get_volume_attribute instead.
sub get_volume_notes {
my $class = shift;
PVE::Storage::DirPlugin::get_volume_notes($class, @_);
}
# FIXME remove on the next APIAGE reset.
# Deprecated, use update_volume_attribute instead.
sub update_volume_notes {
my $class = shift;
PVE::Storage::DirPlugin::update_volume_notes($class, @_);
}
sub get_volume_attribute {
return PVE::Storage::DirPlugin::get_volume_attribute(@_);
}
sub update_volume_attribute {
return PVE::Storage::DirPlugin::update_volume_attribute(@_);
}
1;

View File

@ -0,0 +1,239 @@
package PVE::Storage::DirPlugin;
use strict;
use warnings;
use Cwd;
use Encode qw(decode encode);
use File::Path;
use IO::File;
use POSIX;
use PVE::Storage::Plugin;
use PVE::JSONSchema qw(get_standard_option);
use base qw(PVE::Storage::Plugin);
# Configuration
sub type {
return 'dir';
}
sub plugindata {
return {
content => [ { images => 1, rootdir => 1, vztmpl => 1, iso => 1, backup => 1, snippets => 1, none => 1 },
{ images => 1, rootdir => 1 }],
format => [ { raw => 1, qcow2 => 1, vmdk => 1, subvol => 1 } , 'raw' ],
};
}
sub properties {
return {
path => {
description => "File system path.",
type => 'string', format => 'pve-storage-path',
},
mkdir => {
description => "Create the directory if it doesn't exist.",
type => 'boolean',
default => 'yes',
},
is_mountpoint => {
description =>
"Assume the given path is an externally managed mountpoint " .
"and consider the storage offline if it is not mounted. ".
"Using a boolean (yes/no) value serves as a shortcut to using the target path in this field.",
type => 'string',
default => 'no',
},
bwlimit => get_standard_option('bwlimit'),
};
}
sub options {
return {
path => { fixed => 1 },
'content-dirs' => { optional => 1 },
nodes => { optional => 1 },
shared => { optional => 1 },
disable => { optional => 1 },
maxfiles => { optional => 1 },
'prune-backups' => { optional => 1 },
'max-protected-backups' => { optional => 1 },
content => { optional => 1 },
format => { optional => 1 },
mkdir => { optional => 1 },
is_mountpoint => { optional => 1 },
bwlimit => { optional => 1 },
preallocation => { optional => 1 },
};
}
# Storage implementation
#
# NOTE: should ProcFSTools::is_mounted accept an optional cache like this?
sub path_is_mounted {
my ($mountpoint, $mountdata) = @_;
$mountpoint = Cwd::realpath($mountpoint); # symlinks
return 0 if !defined($mountpoint); # path does not exist
$mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata;
return 1 if grep { $_->[1] eq $mountpoint } @$mountdata;
return undef;
}
sub parse_is_mountpoint {
my ($scfg) = @_;
my $is_mp = $scfg->{is_mountpoint};
return undef if !defined $is_mp;
if (defined(my $bool = PVE::JSONSchema::parse_boolean($is_mp))) {
return $bool ? $scfg->{path} : undef;
}
return $is_mp; # contains a path
}
# FIXME move into 'get_volume_attribute' when removing 'get_volume_notes'
my $get_volume_notes_impl = sub {
my ($class, $scfg, $storeid, $volname, $timeout) = @_;
my ($vtype) = $class->parse_volname($volname);
return if $vtype ne 'backup';
my $path = $class->filesystem_path($scfg, $volname);
$path .= $class->SUPER::NOTES_EXT;
if (-f $path) {
my $data = PVE::Tools::file_get_contents($path);
return eval { decode('UTF-8', $data, 1) } // $data;
}
return '';
};
# FIXME remove on the next APIAGE reset.
# Deprecated, use get_volume_attribute instead.
sub get_volume_notes {
my ($class, $scfg, $storeid, $volname, $timeout) = @_;
return $get_volume_notes_impl->($class, $scfg, $storeid, $volname, $timeout);
}
# FIXME move into 'update_volume_attribute' when removing 'update_volume_notes'
my $update_volume_notes_impl = sub {
my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_;
my ($vtype) = $class->parse_volname($volname);
die "only backups can have notes\n" if $vtype ne 'backup';
my $path = $class->filesystem_path($scfg, $volname);
$path .= $class->SUPER::NOTES_EXT;
if (defined($notes) && $notes ne '') {
my $encoded = encode('UTF-8', $notes);
PVE::Tools::file_set_contents($path, $encoded);
} else {
unlink $path or $! == ENOENT or die "could not delete notes - $!\n";
}
return;
};
# FIXME remove on the next APIAGE reset.
# Deprecated, use update_volume_attribute instead.
sub update_volume_notes {
my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_;
return $update_volume_notes_impl->($class, $scfg, $storeid, $volname, $notes, $timeout);
}
sub get_volume_attribute {
my ($class, $scfg, $storeid, $volname, $attribute) = @_;
if ($attribute eq 'notes') {
return $get_volume_notes_impl->($class, $scfg, $storeid, $volname);
}
my ($vtype) = $class->parse_volname($volname);
return if $vtype ne 'backup';
if ($attribute eq 'protected') {
my $path = $class->filesystem_path($scfg, $volname);
return -e PVE::Storage::protection_file_path($path) ? 1 : 0;
}
return;
}
sub update_volume_attribute {
my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_;
if ($attribute eq 'notes') {
return $update_volume_notes_impl->($class, $scfg, $storeid, $volname, $value);
}
my ($vtype) = $class->parse_volname($volname);
die "only backups support attribute '$attribute'\n" if $vtype ne 'backup';
if ($attribute eq 'protected') {
my $path = $class->filesystem_path($scfg, $volname);
my $protection_path = PVE::Storage::protection_file_path($path);
return if !((-e $protection_path) xor $value); # protection status already correct
if ($value) {
my $fh = IO::File->new($protection_path, O_CREAT, 0644)
or die "unable to create protection file '$protection_path' - $!\n";
close($fh);
} else {
unlink $protection_path or $! == ENOENT
or die "could not delete protection file '$protection_path' - $!\n";
}
return;
}
die "attribute '$attribute' is not supported for storage type '$scfg->{type}'\n";
}
sub status {
my ($class, $storeid, $scfg, $cache) = @_;
if (defined(my $mp = parse_is_mountpoint($scfg))) {
$cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
if !$cache->{mountdata};
return undef if !path_is_mounted($mp, $cache->{mountdata});
}
return $class->SUPER::status($storeid, $scfg, $cache);
}
sub activate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
my $path = $scfg->{path};
if (!defined($scfg->{mkdir}) || $scfg->{mkdir}) {
mkpath $path;
}
my $mp = parse_is_mountpoint($scfg);
if (defined($mp) && !path_is_mounted($mp, $cache->{mountdata})) {
die "unable to activate storage '$storeid' - " .
"directory is expected to be a mount point but is not mounted: '$mp'\n";
}
$class->SUPER::activate_storage($storeid, $scfg, $cache);
}
sub check_config {
my ($self, $sectionId, $config, $create, $skipSchemaCheck) = @_;
my $opts = PVE::SectionConfig::check_config($self, $sectionId, $config, $create, $skipSchemaCheck);
return $opts if !$create;
if ($opts->{path} !~ m@^/[-/a-zA-Z0-9_.]+$@) {
die "illegal path for directory storage: $opts->{path}\n";
}
return $opts;
}
1;

View File

@ -0,0 +1,354 @@
package PVE::Storage::GlusterfsPlugin;
use strict;
use warnings;
use IO::File;
use File::Path;
use PVE::Tools qw(run_command);
use PVE::ProcFSTools;
use PVE::Network;
use PVE::Storage::Plugin;
use PVE::JSONSchema qw(get_standard_option);
use base qw(PVE::Storage::Plugin);
# Glusterfs helper functions
my $server_test_results = {};
my $get_active_server = sub {
my ($scfg, $return_default_if_offline) = @_;
my $defaultserver = $scfg->{server} ? $scfg->{server} : 'localhost';
if ($return_default_if_offline && !defined($scfg->{server2})) {
# avoid delays (there is no backup server anyways)
return $defaultserver;
}
my $serverlist = [ $defaultserver ];
push @$serverlist, $scfg->{server2} if $scfg->{server2};
my $ctime = time();
foreach my $server (@$serverlist) {
my $stat = $server_test_results->{$server};
return $server if $stat && $stat->{active} && (($ctime - $stat->{time}) <= 2);
}
foreach my $server (@$serverlist) {
my $status = 0;
if ($server && $server ne 'localhost' && $server ne '127.0.0.1' && $server ne '::1') {
# ping the gluster daemon default port (24007) as heuristic
$status = PVE::Network::tcp_ping($server, 24007, 2);
} else {
my $parser = sub {
my $line = shift;
if ($line =~ m/Status: Started$/) {
$status = 1;
}
};
my $cmd = ['/usr/sbin/gluster', 'volume', 'info', $scfg->{volume}];
run_command($cmd, errmsg => "glusterfs error", errfunc => sub {}, outfunc => $parser);
}
$server_test_results->{$server} = { time => time(), active => $status };
return $server if $status;
}
return $defaultserver if $return_default_if_offline;
return undef;
};
sub glusterfs_is_mounted {
my ($volume, $mountpoint, $mountdata) = @_;
$mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata;
return $mountpoint if grep {
$_->[2] eq 'fuse.glusterfs' &&
$_->[0] =~ /^\S+:\Q$volume\E$/ &&
$_->[1] eq $mountpoint
} @$mountdata;
return undef;
}
sub glusterfs_mount {
my ($server, $volume, $mountpoint) = @_;
my $source = "$server:$volume";
my $cmd = ['/bin/mount', '-t', 'glusterfs', $source, $mountpoint];
run_command($cmd, errmsg => "mount error");
}
# Configuration
sub type {
return 'glusterfs';
}
sub plugindata {
return {
content => [ { images => 1, vztmpl => 1, iso => 1, backup => 1, snippets => 1},
{ images => 1 }],
format => [ { raw => 1, qcow2 => 1, vmdk => 1 } , 'raw' ],
};
}
sub properties {
return {
volume => {
description => "Glusterfs Volume.",
type => 'string',
},
server2 => {
description => "Backup volfile server IP or DNS name.",
type => 'string', format => 'pve-storage-server',
requires => 'server',
},
transport => {
description => "Gluster transport: tcp or rdma",
type => 'string',
enum => ['tcp', 'rdma', 'unix'],
},
};
}
sub options {
return {
path => { fixed => 1 },
server => { optional => 1 },
server2 => { optional => 1 },
volume => { fixed => 1 },
transport => { optional => 1 },
nodes => { optional => 1 },
disable => { optional => 1 },
maxfiles => { optional => 1 },
'prune-backups' => { optional => 1 },
'max-protected-backups' => { optional => 1 },
content => { optional => 1 },
format => { optional => 1 },
mkdir => { optional => 1 },
bwlimit => { optional => 1 },
preallocation => { optional => 1 },
};
}
sub check_config {
my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
$config->{path} = "/mnt/pve/$sectionId" if $create && !$config->{path};
return $class->SUPER::check_config($sectionId, $config, $create, $skipSchemaCheck);
}
# Storage implementation
sub parse_name_dir {
my $name = shift;
if ($name =~ m!^((base-)?[^/\s]+\.(raw|qcow2|vmdk))$!) {
return ($1, $3, $2);
}
die "unable to parse volume filename '$name'\n";
}
sub path {
my ($class, $scfg, $volname, $storeid, $snapname) = @_;
my ($vtype, $name, $vmid, undef, undef, $isBase, $format) =
$class->parse_volname($volname);
# Note: qcow2/qed has internal snapshot, so path is always
# the same (with or without snapshot => same file).
die "can't snapshot this image format\n"
if defined($snapname) && $format !~ m/^(qcow2|qed)$/;
my $path = undef;
if ($vtype eq 'images') {
my $server = &$get_active_server($scfg, 1);
my $glustervolume = $scfg->{volume};
my $transport = $scfg->{transport};
my $protocol = "gluster";
if ($transport) {
$protocol = "gluster+$transport";
}
$path = "$protocol://$server/$glustervolume/images/$vmid/$name";
} else {
my $dir = $class->get_subdir($scfg, $vtype);
$path = "$dir/$name";
}
return wantarray ? ($path, $vmid, $vtype) : $path;
}
sub clone_image {
my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
die "storage definition has no path\n" if !$scfg->{path};
my ($vtype, $basename, $basevmid, undef, undef, $isBase, $format) =
$class->parse_volname($volname);
die "clone_image on wrong vtype '$vtype'\n" if $vtype ne 'images';
die "this storage type does not support clone_image on snapshot\n" if $snap;
die "this storage type does not support clone_image on subvolumes\n" if $format eq 'subvol';
die "clone_image only works on base images\n" if !$isBase;
my $imagedir = $class->get_subdir($scfg, 'images');
$imagedir .= "/$vmid";
mkpath $imagedir;
my $name = $class->find_free_diskname($storeid, $scfg, $vmid, "qcow2", 1);
warn "clone $volname: $vtype, $name, $vmid to $name (base=../$basevmid/$basename)\n";
my $path = "$imagedir/$name";
die "disk image '$path' already exists\n" if -e $path;
my $server = &$get_active_server($scfg, 1);
my $glustervolume = $scfg->{volume};
my $volumepath = "gluster://$server/$glustervolume/images/$vmid/$name";
my $cmd = ['/usr/bin/qemu-img', 'create', '-b', "../$basevmid/$basename",
'-F', $format, '-f', 'qcow2', $volumepath];
run_command($cmd, errmsg => "unable to create image");
return "$basevmid/$basename/$vmid/$name";
}
sub alloc_image {
my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
my $imagedir = $class->get_subdir($scfg, 'images');
$imagedir .= "/$vmid";
mkpath $imagedir;
$name = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt, 1) if !$name;
my (undef, $tmpfmt) = parse_name_dir($name);
die "illegal name '$name' - wrong extension for format ('$tmpfmt != '$fmt')\n"
if $tmpfmt ne $fmt;
my $path = "$imagedir/$name";
die "disk image '$path' already exists\n" if -e $path;
my $server = &$get_active_server($scfg, 1);
my $glustervolume = $scfg->{volume};
my $volumepath = "gluster://$server/$glustervolume/images/$vmid/$name";
my $cmd = ['/usr/bin/qemu-img', 'create'];
my $prealloc_opt = PVE::Storage::Plugin::preallocation_cmd_option($scfg, $fmt);
push @$cmd, '-o', $prealloc_opt if defined($prealloc_opt);
push @$cmd, '-f', $fmt, $volumepath, "${size}K";
eval { run_command($cmd, errmsg => "unable to create image"); };
if ($@) {
unlink $path;
rmdir $imagedir;
die "$@";
}
return "$vmid/$name";
}
sub status {
my ($class, $storeid, $scfg, $cache) = @_;
$cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
if !$cache->{mountdata};
my $path = $scfg->{path};
my $volume = $scfg->{volume};
return undef if !glusterfs_is_mounted($volume, $path, $cache->{mountdata});
return $class->SUPER::status($storeid, $scfg, $cache);
}
sub activate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
$cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
if !$cache->{mountdata};
my $path = $scfg->{path};
my $volume = $scfg->{volume};
if (!glusterfs_is_mounted($volume, $path, $cache->{mountdata})) {
mkpath $path if !(defined($scfg->{mkdir}) && !$scfg->{mkdir});
die "unable to activate storage '$storeid' - " .
"directory '$path' does not exist\n" if ! -d $path;
my $server = &$get_active_server($scfg, 1);
glusterfs_mount($server, $volume, $path);
}
$class->SUPER::activate_storage($storeid, $scfg, $cache);
}
sub deactivate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
$cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
if !$cache->{mountdata};
my $path = $scfg->{path};
my $volume = $scfg->{volume};
if (glusterfs_is_mounted($volume, $path, $cache->{mountdata})) {
my $cmd = ['/bin/umount', $path];
run_command($cmd, errmsg => 'umount error');
}
}
sub activate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
# do nothing by default
}
sub deactivate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
# do nothing by default
}
sub check_connection {
my ($class, $storeid, $scfg, $cache) = @_;
my $server = &$get_active_server($scfg);
return defined($server) ? 1 : 0;
}
1;

View File

@ -0,0 +1,255 @@
package PVE::Storage::ISCSIDirectPlugin;
use strict;
use warnings;
use IO::File;
use HTTP::Request;
use LWP::UserAgent;
use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach);
use PVE::Storage::Plugin;
use PVE::JSONSchema qw(get_standard_option);
use base qw(PVE::Storage::Plugin);
sub iscsi_ls {
my ($scfg, $storeid) = @_;
my $portal = $scfg->{portal};
my $cmd = ['/usr/bin/iscsi-ls', '-s', 'iscsi://'.$portal ];
my $list = {};
my %unittobytes = (
"k" => 1024,
"M" => 1024*1024,
"G" => 1024*1024*1024,
"T" => 1024*1024*1024*1024
);
eval {
run_command($cmd, errmsg => "iscsi error", errfunc => sub {}, outfunc => sub {
my $line = shift;
$line = trim($line);
if( $line =~ /Lun:(\d+)\s+([A-Za-z0-9\-\_\.\:]*)\s+\(Size:([0-9\.]*)(k|M|G|T)\)/ ) {
my $image = "lun".$1;
my $size = $3;
my $unit = $4;
$list->{$storeid}->{$image} = {
name => $image,
size => $size * $unittobytes{$unit},
format => 'raw',
};
}
});
};
my $err = $@;
die $err if $err && $err !~ m/TESTUNITREADY failed with SENSE KEY/ ;
return $list;
}
# Configuration
sub type {
return 'iscsidirect';
}
sub plugindata {
return {
content => [ {images => 1, none => 1}, { images => 1 }],
select_existing => 1,
};
}
sub options {
return {
portal => { fixed => 1 },
target => { fixed => 1 },
nodes => { optional => 1},
disable => { optional => 1},
content => { optional => 1},
bwlimit => { optional => 1 },
};
}
# Storage implementation
sub parse_volname {
my ($class, $volname) = @_;
if ($volname =~ m/^lun(\d+)$/) {
return ('images', $1, undef, undef, undef, undef, 'raw');
}
die "unable to parse iscsi volume name '$volname'\n";
}
sub path {
my ($class, $scfg, $volname, $storeid, $snapname) = @_;
die "volume snapshot is not possible on iscsi device"
if defined($snapname);
my ($vtype, $lun, $vmid) = $class->parse_volname($volname);
my $target = $scfg->{target};
my $portal = $scfg->{portal};
my $path = "iscsi://$portal/$target/$lun";
return ($path, $vmid, $vtype);
}
sub create_base {
my ($class, $storeid, $scfg, $volname) = @_;
die "can't create base images in iscsi storage\n";
}
sub clone_image {
my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
die "can't clone images in iscsi storage\n";
}
sub alloc_image {
my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
die "can't allocate space in iscsi storage\n";
}
sub free_image {
my ($class, $storeid, $scfg, $volname, $isBase) = @_;
die "can't free space in iscsi storage\n";
}
sub list_images {
my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
my $res = [];
$cache->{directiscsi} = iscsi_ls($scfg,$storeid) if !$cache->{directiscsi};
# we have no owner for iscsi devices
my $target = $scfg->{target};
if (my $dat = $cache->{directiscsi}->{$storeid}) {
foreach my $volname (keys %$dat) {
my $volid = "$storeid:$volname";
if ($vollist) {
my $found = grep { $_ eq $volid } @$vollist;
next if !$found;
} else {
# we have no owner for iscsi devices
next if defined($vmid);
}
my $info = $dat->{$volname};
$info->{volid} = $volid;
push @$res, $info;
}
}
return $res;
}
sub status {
my ($class, $storeid, $scfg, $cache) = @_;
my $total = 0;
my $free = 0;
my $used = 0;
my $active = 1;
return ($total,$free,$used,$active);
return undef;
}
sub activate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
return 1;
}
sub deactivate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
return 1;
}
sub activate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
die "volume snapshot is not possible on iscsi device" if $snapname;
return 1;
}
sub deactivate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
die "volume snapshot is not possible on iscsi device" if $snapname;
return 1;
}
sub volume_size_info {
my ($class, $scfg, $storeid, $volname, $timeout) = @_;
my $vollist = iscsi_ls($scfg,$storeid);
my $info = $vollist->{$storeid}->{$volname};
return wantarray ? ($info->{size}, 'raw', 0, undef) : $info->{size};
}
sub volume_resize {
my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
die "volume resize is not possible on iscsi device";
}
sub volume_snapshot {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
die "volume snapshot is not possible on iscsi device";
}
sub volume_snapshot_rollback {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
die "volume snapshot rollback is not possible on iscsi device";
}
sub volume_snapshot_delete {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
die "volume snapshot delete is not possible on iscsi device";
}
sub volume_has_feature {
my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
my $features = {
copy => { current => 1},
};
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
$class->parse_volname($volname);
my $key = undef;
if($snapname){
$key = 'snap';
}else{
$key = $isBase ? 'base' : 'current';
}
return 1 if $features->{$feature}->{$key};
return undef;
}
1;

View File

@ -0,0 +1,437 @@
package PVE::Storage::ISCSIPlugin;
use strict;
use warnings;
use File::stat;
use IO::Dir;
use IO::File;
use PVE::JSONSchema qw(get_standard_option);
use PVE::Storage::Plugin;
use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach $IPV4RE $IPV6RE);
use base qw(PVE::Storage::Plugin);
# iscsi helper function
my $ISCSIADM = '/usr/bin/iscsiadm';
$ISCSIADM = undef if ! -X $ISCSIADM;
sub check_iscsi_support {
my $noerr = shift;
if (!$ISCSIADM) {
my $msg = "no iscsi support - please install open-iscsi";
if ($noerr) {
warn "warning: $msg\n";
return 0;
}
die "error: $msg\n";
}
return 1;
}
sub iscsi_session_list {
check_iscsi_support ();
my $cmd = [$ISCSIADM, '--mode', 'session'];
my $res = {};
eval {
run_command($cmd, errmsg => 'iscsi session scan failed', outfunc => sub {
my $line = shift;
if ($line =~ m/^tcp:\s+\[(\S+)\]\s+\S+\s+(\S+)(\s+\S+)?\s*$/) {
my ($session, $target) = ($1, $2);
# there can be several sessions per target (multipath)
push @{$res->{$target}}, $session;
}
});
};
if (my $err = $@) {
die $err if $err !~ m/: No active sessions.$/i;
}
return $res;
}
sub iscsi_test_portal {
my ($portal) = @_;
my ($server, $port) = PVE::Tools::parse_host_and_port($portal);
return 0 if !$server;
return PVE::Network::tcp_ping($server, $port || 3260, 2);
}
sub iscsi_discovery {
my ($portal) = @_;
check_iscsi_support ();
my $res = {};
return $res if !iscsi_test_portal($portal); # fixme: raise exception here?
my $cmd = [$ISCSIADM, '--mode', 'discovery', '--type', 'sendtargets', '--portal', $portal];
run_command($cmd, outfunc => sub {
my $line = shift;
if ($line =~ m/^((?:$IPV4RE|\[$IPV6RE\]):\d+)\,\S+\s+(\S+)\s*$/) {
my $portal = $1;
my $target = $2;
# one target can have more than one portal (multipath).
push @{$res->{$target}}, $portal;
}
});
return $res;
}
sub iscsi_login {
my ($target, $portal_in) = @_;
check_iscsi_support();
eval { iscsi_discovery($portal_in); };
warn $@ if $@;
run_command([$ISCSIADM, '--mode', 'node', '--targetname', $target, '--login']);
}
sub iscsi_logout {
my ($target, $portal) = @_;
check_iscsi_support();
run_command([$ISCSIADM, '--mode', 'node', '--targetname', $target, '--logout']);
}
my $rescan_filename = "/var/run/pve-iscsi-rescan.lock";
sub iscsi_session_rescan {
my $session_list = shift;
check_iscsi_support();
my $rstat = stat($rescan_filename);
if (!$rstat) {
if (my $fh = IO::File->new($rescan_filename, "a")) {
utime undef, undef, $fh;
close($fh);
}
} else {
my $atime = $rstat->atime;
my $tdiff = time() - $atime;
# avoid frequent rescans
return if !($tdiff < 0 || $tdiff > 10);
utime undef, undef, $rescan_filename;
}
foreach my $session (@$session_list) {
my $cmd = [$ISCSIADM, '--mode', 'session', '--sid', $session, '--rescan'];
eval { run_command($cmd, outfunc => sub {}); };
warn $@ if $@;
}
}
sub load_stable_scsi_paths {
my $stable_paths = {};
my $stabledir = "/dev/disk/by-id";
if (my $dh = IO::Dir->new($stabledir)) {
foreach my $tmp (sort $dh->read) {
# exclude filenames with part in name (same disk but partitions)
# use only filenames with scsi(with multipath i have the same device
# with dm-uuid-mpath , dm-name and scsi in name)
if($tmp !~ m/-part\d+$/ && ($tmp =~ m/^scsi-/ || $tmp =~ m/^dm-uuid-mpath-/)) {
my $path = "$stabledir/$tmp";
my $bdevdest = readlink($path);
if ($bdevdest && $bdevdest =~ m|^../../([^/]+)|) {
$stable_paths->{$1}=$tmp;
}
}
}
$dh->close;
}
return $stable_paths;
}
sub iscsi_device_list {
my $res = {};
my $dirname = '/sys/class/iscsi_session';
my $stable_paths = load_stable_scsi_paths();
dir_glob_foreach($dirname, 'session(\d+)', sub {
my ($ent, $session) = @_;
my $target = file_read_firstline("$dirname/$ent/targetname");
return if !$target;
my (undef, $host) = dir_glob_regex("$dirname/$ent/device", 'target(\d+):.*');
return if !defined($host);
dir_glob_foreach("/sys/bus/scsi/devices", "$host:" . '(\d+):(\d+):(\d+)', sub {
my ($tmp, $channel, $id, $lun) = @_;
my $type = file_read_firstline("/sys/bus/scsi/devices/$tmp/type");
return if !defined($type) || $type ne '0'; # list disks only
my $bdev;
if (-d "/sys/bus/scsi/devices/$tmp/block") { # newer kernels
(undef, $bdev) = dir_glob_regex("/sys/bus/scsi/devices/$tmp/block/", '([A-Za-z]\S*)');
} else {
(undef, $bdev) = dir_glob_regex("/sys/bus/scsi/devices/$tmp", 'block:(\S+)');
}
return if !$bdev;
#check multipath
if (-d "/sys/block/$bdev/holders") {
my $multipathdev = dir_glob_regex("/sys/block/$bdev/holders", '[A-Za-z]\S*');
$bdev = $multipathdev if $multipathdev;
}
my $blockdev = $stable_paths->{$bdev};
return if !$blockdev;
my $size = file_read_firstline("/sys/block/$bdev/size");
return if !$size;
my $volid = "$channel.$id.$lun.$blockdev";
$res->{$target}->{$volid} = {
'format' => 'raw',
'size' => int($size * 512),
'vmid' => 0, # not assigned to any vm
'channel' => int($channel),
'id' => int($id),
'lun' => int($lun),
};
#print "TEST: $target $session $host,$bus,$tg,$lun $blockdev\n";
});
});
return $res;
}
# Configuration
sub type {
return 'iscsi';
}
sub plugindata {
return {
content => [ {images => 1, none => 1}, { images => 1 }],
select_existing => 1,
};
}
sub properties {
return {
target => {
description => "iSCSI target.",
type => 'string',
},
portal => {
description => "iSCSI portal (IP or DNS name with optional port).",
type => 'string', format => 'pve-storage-portal-dns',
},
};
}
sub options {
return {
portal => { fixed => 1 },
target => { fixed => 1 },
nodes => { optional => 1},
disable => { optional => 1},
content => { optional => 1},
bwlimit => { optional => 1 },
};
}
# Storage implementation
sub parse_volname {
my ($class, $volname) = @_;
if ($volname =~ m!^\d+\.\d+\.\d+\.(\S+)$!) {
return ('images', $1, undef, undef, undef, undef, 'raw');
}
die "unable to parse iscsi volume name '$volname'\n";
}
sub filesystem_path {
my ($class, $scfg, $volname, $snapname) = @_;
die "snapshot is not possible on iscsi storage\n" if defined($snapname);
my ($vtype, $name, $vmid) = $class->parse_volname($volname);
my $path = "/dev/disk/by-id/$name";
return wantarray ? ($path, $vmid, $vtype) : $path;
}
sub create_base {
my ($class, $storeid, $scfg, $volname) = @_;
die "can't create base images in iscsi storage\n";
}
sub clone_image {
my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
die "can't clone images in iscsi storage\n";
}
sub alloc_image {
my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
die "can't allocate space in iscsi storage\n";
}
sub free_image {
my ($class, $storeid, $scfg, $volname, $isBase) = @_;
die "can't free space in iscsi storage\n";
}
# list all luns regardless of set content_types, since we need it for
# listing in the gui and we can only have images anyway
sub list_volumes {
my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
my $res = $class->list_images($storeid, $scfg, $vmid);
for my $item (@$res) {
$item->{content} = 'images'; # we only have images
}
return $res;
}
sub list_images {
my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
my $res = [];
$cache->{iscsi_devices} = iscsi_device_list() if !$cache->{iscsi_devices};
# we have no owner for iscsi devices
my $target = $scfg->{target};
if (my $dat = $cache->{iscsi_devices}->{$target}) {
foreach my $volname (keys %$dat) {
my $volid = "$storeid:$volname";
if ($vollist) {
my $found = grep { $_ eq $volid } @$vollist;
next if !$found;
} else {
# we have no owner for iscsi devices
next if defined($vmid);
}
my $info = $dat->{$volname};
$info->{volid} = $volid;
push @$res, $info;
}
}
return $res;
}
sub iscsi_session {
my ($cache, $target) = @_;
$cache->{iscsi_sessions} = iscsi_session_list() if !$cache->{iscsi_sessions};
return $cache->{iscsi_sessions}->{$target};
}
sub status {
my ($class, $storeid, $scfg, $cache) = @_;
my $session = iscsi_session($cache, $scfg->{target});
my $active = defined($session) ? 1 : 0;
return (0, 0, 0, $active);
}
sub activate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
return if !check_iscsi_support(1);
my $session = iscsi_session($cache, $scfg->{target});
if (!defined ($session)) {
eval { iscsi_login($scfg->{target}, $scfg->{portal}); };
warn $@ if $@;
} else {
# make sure we get all devices
iscsi_session_rescan($session);
}
}
sub deactivate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
return if !check_iscsi_support(1);
if (defined(iscsi_session($cache, $scfg->{target}))) {
iscsi_logout($scfg->{target}, $scfg->{portal});
}
}
sub check_connection {
my ($class, $storeid, $scfg) = @_;
my $portal = $scfg->{portal};
return iscsi_test_portal($portal);
}
sub volume_resize {
my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
die "volume resize is not possible on iscsi device";
}
sub volume_has_feature {
my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
my $features = {
copy => { current => 1},
};
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
$class->parse_volname($volname);
my $key = undef;
if($snapname){
$key = 'snap';
}else{
$key = $isBase ? 'base' : 'current';
}
return 1 if $features->{$feature}->{$key};
return undef;
}
1;

View File

@ -0,0 +1,741 @@
package PVE::Storage::LVMPlugin;
use strict;
use warnings;
use IO::File;
use PVE::Tools qw(run_command trim);
use PVE::Storage::Plugin;
use PVE::JSONSchema qw(get_standard_option);
use base qw(PVE::Storage::Plugin);
# lvm helper functions
my $ignore_no_medium_warnings = sub {
my $line = shift;
# ignore those, most of the time they're from (virtual) IPMI/iKVM devices
# and just spam the log..
if ($line !~ /open failed: No medium found/) {
print STDERR "$line\n";
}
};
sub lvm_pv_info {
my ($device) = @_;
die "no device specified" if !$device;
my $has_label = 0;
my $cmd = ['/usr/bin/file', '-L', '-s', $device];
run_command($cmd, outfunc => sub {
my $line = shift;
$has_label = 1 if $line =~ m/LVM2/;
});
return undef if !$has_label;
$cmd = ['/sbin/pvs', '--separator', ':', '--noheadings', '--units', 'k',
'--unbuffered', '--nosuffix', '--options',
'pv_name,pv_size,vg_name,pv_uuid', $device];
my $pvinfo;
run_command($cmd, outfunc => sub {
my $line = shift;
$line = trim($line);
my ($pvname, $size, $vgname, $uuid) = split(':', $line);
die "found multiple pvs entries for device '$device'\n"
if $pvinfo;
$pvinfo = {
pvname => $pvname,
size => int($size),
vgname => $vgname,
uuid => $uuid,
};
});
return $pvinfo;
}
sub clear_first_sector {
my ($dev) = shift;
if (my $fh = IO::File->new($dev, "w")) {
my $buf = 0 x 512;
syswrite $fh, $buf;
$fh->close();
}
}
sub lvm_create_volume_group {
my ($device, $vgname, $shared) = @_;
my $res = lvm_pv_info($device);
if ($res->{vgname}) {
return if $res->{vgname} eq $vgname; # already created
die "device '$device' is already used by volume group '$res->{vgname}'\n";
}
clear_first_sector($device); # else pvcreate fails
# we use --metadatasize 250k, which reseults in "pe_start = 512"
# so pe_start is aligned on a 128k boundary (advantage for SSDs)
my $cmd = ['/sbin/pvcreate', '--metadatasize', '250k', $device];
run_command($cmd, errmsg => "pvcreate '$device' error");
$cmd = ['/sbin/vgcreate', $vgname, $device];
# push @$cmd, '-c', 'y' if $shared; # we do not use this yet
run_command($cmd, errmsg => "vgcreate $vgname $device error", errfunc => $ignore_no_medium_warnings, outfunc => $ignore_no_medium_warnings);
}
sub lvm_destroy_volume_group {
my ($vgname) = @_;
run_command(
['vgremove', '-y', $vgname],
errmsg => "unable to remove volume group $vgname",
errfunc => $ignore_no_medium_warnings,
outfunc => $ignore_no_medium_warnings,
);
}
sub lvm_vgs {
my ($includepvs) = @_;
my $cmd = ['/sbin/vgs', '--separator', ':', '--noheadings', '--units', 'b',
'--unbuffered', '--nosuffix', '--options'];
my $cols = [qw(vg_name vg_size vg_free lv_count)];
if ($includepvs) {
push @$cols, qw(pv_name pv_size pv_free);
}
push @$cmd, join(',', @$cols);
my $vgs = {};
eval {
run_command($cmd, outfunc => sub {
my $line = shift;
$line = trim($line);
my ($name, $size, $free, $lvcount, $pvname, $pvsize, $pvfree) = split (':', $line);
$vgs->{$name} //= {
size => int ($size),
free => int ($free),
lvcount => int($lvcount)
};
if (defined($pvname) && defined($pvsize) && defined($pvfree)) {
push @{$vgs->{$name}->{pvs}}, {
name => $pvname,
size => int($pvsize),
free => int($pvfree),
};
}
},
errfunc => $ignore_no_medium_warnings,
);
};
my $err = $@;
# just warn (vgs return error code 5 if clvmd does not run)
# but output is still OK (list without clustered VGs)
warn $err if $err;
return $vgs;
}
sub lvm_list_volumes {
my ($vgname) = @_;
my $option_list = 'vg_name,lv_name,lv_size,lv_attr,pool_lv,data_percent,metadata_percent,snap_percent,uuid,tags,metadata_size,time';
my $cmd = [
'/sbin/lvs', '--separator', ':', '--noheadings', '--units', 'b',
'--unbuffered', '--nosuffix',
'--config', 'report/time_format="%s"',
'--options', $option_list,
];
push @$cmd, $vgname if $vgname;
my $lvs = {};
run_command($cmd, outfunc => sub {
my $line = shift;
$line = trim($line);
my ($vg_name, $lv_name, $lv_size, $lv_attr, $pool_lv, $data_percent, $meta_percent, $snap_percent, $uuid, $tags, $meta_size, $ctime) = split(':', $line);
return if !$vg_name;
return if !$lv_name;
my $lv_type = substr($lv_attr, 0, 1);
my $d = {
lv_size => int($lv_size),
lv_state => substr($lv_attr, 4, 1),
lv_type => $lv_type,
};
$d->{pool_lv} = $pool_lv if $pool_lv;
$d->{tags} = $tags if $tags;
$d->{ctime} = $ctime;
if ($lv_type eq 't') {
$data_percent ||= 0;
$meta_percent ||= 0;
$snap_percent ||= 0;
$d->{metadata_size} = int($meta_size);
$d->{metadata_used} = int(($meta_percent * $meta_size)/100);
$d->{used} = int(($data_percent * $lv_size)/100);
}
$lvs->{$vg_name}->{$lv_name} = $d;
},
errfunc => $ignore_no_medium_warnings,
);
return $lvs;
}
# Configuration
sub type {
return 'lvm';
}
sub plugindata {
return {
content => [ {images => 1, rootdir => 1}, { images => 1 }],
};
}
sub properties {
return {
vgname => {
description => "Volume group name.",
type => 'string', format => 'pve-storage-vgname',
},
base => {
description => "Base volume. This volume is automatically activated.",
type => 'string', format => 'pve-volume-id',
},
saferemove => {
description => "Zero-out data when removing LVs.",
type => 'boolean',
},
saferemove_throughput => {
description => "Wipe throughput (cstream -t parameter value).",
type => 'string',
},
tagged_only => {
description => "Only use logical volumes tagged with 'pve-vm-ID'.",
type => 'boolean',
}
};
}
sub options {
return {
vgname => { fixed => 1 },
nodes => { optional => 1 },
shared => { optional => 1 },
disable => { optional => 1 },
saferemove => { optional => 1 },
saferemove_throughput => { optional => 1 },
content => { optional => 1 },
base => { fixed => 1, optional => 1 },
tagged_only => { optional => 1 },
bwlimit => { optional => 1 },
};
}
# Storage implementation
sub on_add_hook {
my ($class, $storeid, $scfg, %param) = @_;
if (my $base = $scfg->{base}) {
my ($baseid, $volname) = PVE::Storage::parse_volume_id($base);
my $cfg = PVE::Storage::config();
my $basecfg = PVE::Storage::storage_config ($cfg, $baseid, 1);
die "base storage ID '$baseid' does not exist\n" if !$basecfg;
# we only support iscsi for now
die "unsupported base type '$basecfg->{type}'"
if $basecfg->{type} ne 'iscsi';
my $path = PVE::Storage::path($cfg, $base);
PVE::Storage::activate_storage($cfg, $baseid);
lvm_create_volume_group($path, $scfg->{vgname}, $scfg->{shared});
}
return;
}
sub parse_volname {
my ($class, $volname) = @_;
PVE::Storage::Plugin::parse_lvm_name($volname);
if ($volname =~ m/^(vm-(\d+)-\S+)$/) {
return ('images', $1, $2, undef, undef, undef, 'raw');
}
die "unable to parse lvm volume name '$volname'\n";
}
sub filesystem_path {
my ($class, $scfg, $volname, $snapname) = @_;
die "lvm snapshot is not implemented"if defined($snapname);
my ($vtype, $name, $vmid) = $class->parse_volname($volname);
my $vg = $scfg->{vgname};
my $path = "/dev/$vg/$name";
return wantarray ? ($path, $vmid, $vtype) : $path;
}
sub create_base {
my ($class, $storeid, $scfg, $volname) = @_;
die "can't create base images in lvm storage\n";
}
sub clone_image {
my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
die "can't clone images in lvm storage\n";
}
sub find_free_diskname {
my ($class, $storeid, $scfg, $vmid, $fmt, $add_fmt_suffix) = @_;
my $vg = $scfg->{vgname};
my $lvs = lvm_list_volumes($vg);
my $disk_list = [ keys %{$lvs->{$vg}} ];
return PVE::Storage::Plugin::get_next_vm_diskname($disk_list, $storeid, $vmid, undef, $scfg);
}
sub lvcreate {
my ($vg, $name, $size, $tags) = @_;
if ($size =~ m/\d$/) { # no unit is given
$size .= "k"; # default to kilobytes
}
my $cmd = ['/sbin/lvcreate', '-aly', '-Wy', '--yes', '--size', $size, '--name', $name];
for my $tag (@$tags) {
push @$cmd, '--addtag', $tag;
}
push @$cmd, $vg;
run_command($cmd, errmsg => "lvcreate '$vg/$name' error");
}
sub lvrename {
my ($vg, $oldname, $newname) = @_;
run_command(
['/sbin/lvrename', $vg, $oldname, $newname],
errmsg => "lvrename '${vg}/${oldname}' to '${newname}' error",
);
}
sub alloc_image {
my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
die "unsupported format '$fmt'" if $fmt ne 'raw';
die "illegal name '$name' - should be 'vm-$vmid-*'\n"
if $name && $name !~ m/^vm-$vmid-/;
my $vgs = lvm_vgs();
my $vg = $scfg->{vgname};
die "no such volume group '$vg'\n" if !defined ($vgs->{$vg});
my $free = int($vgs->{$vg}->{free});
die "not enough free space ($free < $size)\n" if $free < $size;
$name = $class->find_free_diskname($storeid, $scfg, $vmid)
if !$name;
lvcreate($vg, $name, $size, ["pve-vm-$vmid"]);
return $name;
}
sub free_image {
my ($class, $storeid, $scfg, $volname, $isBase) = @_;
my $vg = $scfg->{vgname};
# we need to zero out LVM data for security reasons
# and to allow thin provisioning
my $zero_out_worker = sub {
print "zero-out data on image $volname (/dev/$vg/del-$volname)\n";
# wipe throughput up to 10MB/s by default; may be overwritten with saferemove_throughput
my $throughput = '-10485760';
if ($scfg->{saferemove_throughput}) {
$throughput = $scfg->{saferemove_throughput};
}
my $cmd = [
'/usr/bin/cstream',
'-i', '/dev/zero',
'-o', "/dev/$vg/del-$volname",
'-T', '10',
'-v', '1',
'-b', '1048576',
'-t', "$throughput"
];
eval { run_command($cmd, errmsg => "zero out finished (note: 'No space left on device' is ok here)"); };
warn $@ if $@;
$class->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub {
my $cmd = ['/sbin/lvremove', '-f', "$vg/del-$volname"];
run_command($cmd, errmsg => "lvremove '$vg/del-$volname' error");
});
print "successfully removed volume $volname ($vg/del-$volname)\n";
};
my $cmd = ['/sbin/lvchange', '-aly', "$vg/$volname"];
run_command($cmd, errmsg => "can't activate LV '$vg/$volname' to zero-out its data");
$cmd = ['/sbin/lvchange', '--refresh', "$vg/$volname"];
run_command($cmd, errmsg => "can't refresh LV '$vg/$volname' to zero-out its data");
if ($scfg->{saferemove}) {
# avoid long running task, so we only rename here
$cmd = ['/sbin/lvrename', $vg, $volname, "del-$volname"];
run_command($cmd, errmsg => "lvrename '$vg/$volname' error");
return $zero_out_worker;
} else {
my $tmpvg = $scfg->{vgname};
$cmd = ['/sbin/lvremove', '-f', "$tmpvg/$volname"];
run_command($cmd, errmsg => "lvremove '$tmpvg/$volname' error");
}
return undef;
}
my $check_tags = sub {
my ($tags) = @_;
return defined($tags) && $tags =~ /(^|,)pve-vm-\d+(,|$)/;
};
sub list_images {
my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
my $vgname = $scfg->{vgname};
$cache->{lvs} = lvm_list_volumes() if !$cache->{lvs};
my $res = [];
if (my $dat = $cache->{lvs}->{$vgname}) {
foreach my $volname (keys %$dat) {
next if $volname !~ m/^vm-(\d+)-/;
my $owner = $1;
my $info = $dat->{$volname};
next if $scfg->{tagged_only} && !&$check_tags($info->{tags});
# Allow mirrored and RAID LVs
next if $info->{lv_type} !~ m/^[-mMrR]$/;
my $volid = "$storeid:$volname";
if ($vollist) {
my $found = grep { $_ eq $volid } @$vollist;
next if !$found;
} else {
next if defined($vmid) && ($owner ne $vmid);
}
push @$res, {
volid => $volid, format => 'raw', size => $info->{lv_size}, vmid => $owner,
ctime => $info->{ctime},
};
}
}
return $res;
}
sub status {
my ($class, $storeid, $scfg, $cache) = @_;
$cache->{vgs} = lvm_vgs() if !$cache->{vgs};
my $vgname = $scfg->{vgname};
if (my $info = $cache->{vgs}->{$vgname}) {
return ($info->{size}, $info->{free}, $info->{size} - $info->{free}, 1);
}
return undef;
}
sub activate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
$cache->{vgs} = lvm_vgs() if !$cache->{vgs};
# In LVM2, vgscans take place automatically;
# this is just to be sure
if ($cache->{vgs} && !$cache->{vgscaned} &&
!$cache->{vgs}->{$scfg->{vgname}}) {
$cache->{vgscaned} = 1;
my $cmd = ['/sbin/vgscan', '--ignorelockingfailure', '--mknodes'];
eval { run_command($cmd, outfunc => sub {}); };
warn $@ if $@;
}
# we do not acticate any volumes here ('vgchange -aly')
# instead, volumes are activate individually later
}
sub deactivate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
my $cmd = ['/sbin/vgchange', '-aln', $scfg->{vgname}];
run_command($cmd, errmsg => "can't deactivate VG '$scfg->{vgname}'");
}
sub activate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
#fix me lvmchange is not provided on
my $path = $class->path($scfg, $volname, $snapname);
my $lvm_activate_mode = 'ey';
my $cmd = ['/sbin/lvchange', "-a$lvm_activate_mode", $path];
run_command($cmd, errmsg => "can't activate LV '$path'");
$cmd = ['/sbin/lvchange', '--refresh', $path];
run_command($cmd, errmsg => "can't refresh LV '$path' for activation");
}
sub deactivate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
my $path = $class->path($scfg, $volname, $snapname);
return if ! -b $path;
my $cmd = ['/sbin/lvchange', '-aln', $path];
run_command($cmd, errmsg => "can't deactivate LV '$path'");
}
sub volume_resize {
my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
$size = ($size/1024/1024) . "M";
my $path = $class->path($scfg, $volname);
my $cmd = ['/sbin/lvextend', '-L', $size, $path];
$class->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub {
run_command($cmd, errmsg => "error resizing volume '$path'");
});
return 1;
}
sub volume_size_info {
my ($class, $scfg, $storeid, $volname, $timeout) = @_;
my $path = $class->filesystem_path($scfg, $volname);
my $cmd = ['/sbin/lvs', '--separator', ':', '--noheadings', '--units', 'b',
'--unbuffered', '--nosuffix', '--options', 'lv_size', $path];
my $size;
run_command($cmd, timeout => $timeout, errmsg => "can't get size of '$path'",
outfunc => sub {
$size = int(shift);
});
return wantarray ? ($size, 'raw', 0, undef) : $size;
}
sub volume_snapshot {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
die "lvm snapshot is not implemented";
}
sub volume_snapshot_rollback {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
die "lvm snapshot rollback is not implemented";
}
sub volume_snapshot_delete {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
die "lvm snapshot delete is not implemented";
}
sub volume_has_feature {
my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
my $features = {
copy => { base => 1, current => 1},
rename => {current => 1},
};
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
$class->parse_volname($volname);
my $key = undef;
if($snapname){
$key = 'snap';
}else{
$key = $isBase ? 'base' : 'current';
}
return 1 if $features->{$feature}->{$key};
return undef;
}
sub volume_export_formats {
my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
return () if defined($snapshot); # lvm-thin only
return volume_import_formats($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots);
}
sub volume_export {
my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots) = @_;
die "volume export format $format not available for $class\n"
if $format ne 'raw+size';
die "cannot export volumes together with their snapshots in $class\n"
if $with_snapshots;
die "cannot export a snapshot in $class\n" if defined($snapshot);
die "cannot export an incremental stream in $class\n" if defined($base_snapshot);
my $file = $class->path($scfg, $volname, $storeid);
my $size;
# should be faster than querying LVM, also checks for the device file's availability
run_command(['/sbin/blockdev', '--getsize64', $file], outfunc => sub {
my ($line) = @_;
die "unexpected output from /sbin/blockdev: $line\n" if $line !~ /^(\d+)$/;
$size = int($1);
});
PVE::Storage::Plugin::write_common_header($fh, $size);
run_command(['dd', "if=$file", "bs=64k"], output => '>&'.fileno($fh));
}
sub volume_import_formats {
my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
return () if $with_snapshots; # not supported
return () if defined($base_snapshot); # not supported
return ('raw+size');
}
sub volume_import {
my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots, $allow_rename) = @_;
die "volume import format $format not available for $class\n"
if $format ne 'raw+size';
die "cannot import volumes together with their snapshots in $class\n"
if $with_snapshots;
die "cannot import an incremental stream in $class\n" if defined($base_snapshot);
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $file_format) =
$class->parse_volname($volname);
die "cannot import format $format into a file of format $file_format\n"
if $file_format ne 'raw';
my $vg = $scfg->{vgname};
my $lvs = lvm_list_volumes($vg);
if ($lvs->{$vg}->{$volname}) {
die "volume $vg/$volname already exists\n" if !$allow_rename;
warn "volume $vg/$volname already exists - importing with a different name\n";
$name = undef;
}
my ($size) = PVE::Storage::Plugin::read_common_header($fh);
$size = int($size/1024);
eval {
my $allocname = $class->alloc_image($storeid, $scfg, $vmid, 'raw', $name, $size);
my $oldname = $volname;
$volname = $allocname;
if (defined($name) && $allocname ne $oldname) {
die "internal error: unexpected allocated name: '$allocname' != '$oldname'\n";
}
my $file = $class->path($scfg, $volname, $storeid)
or die "internal error: failed to get path to newly allocated volume $volname\n";
$class->volume_import_write($fh, $file);
};
if (my $err = $@) {
my $cleanup_worker = eval { $class->free_image($storeid, $scfg, $volname, 0) };
warn $@ if $@;
if ($cleanup_worker) {
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
$rpcenv->fork_worker('imgdel', undef, $authuser, $cleanup_worker);
}
die $err;
}
return "$storeid:$volname";
}
sub volume_import_write {
my ($class, $input_fh, $output_file) = @_;
run_command(['dd', "of=$output_file", 'bs=64k'],
input => '<&'.fileno($input_fh));
}
sub rename_volume {
my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
my (
undef,
$source_image,
$source_vmid,
$base_name,
$base_vmid,
undef,
$format
) = $class->parse_volname($source_volname);
$target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format)
if !$target_volname;
my $vg = $scfg->{vgname};
my $lvs = lvm_list_volumes($vg);
die "target volume '${target_volname}' already exists\n"
if ($lvs->{$vg}->{$target_volname});
lvrename($vg, $source_volname, $target_volname);
return "${storeid}:${target_volname}";
}
1;

View File

@ -0,0 +1,115 @@
package PVE::Storage::LunCmd::Comstar;
use strict;
use warnings;
use Digest::MD5 qw(md5_hex);
use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach);
my @ssh_opts = ('-o', 'BatchMode=yes');
my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
my $id_rsa_path = '/etc/pve/priv/zfs';
my $get_lun_cmd_map = sub {
my ($method) = @_;
my $stmfadmcmd = "/usr/sbin/stmfadm";
my $sbdadmcmd = "/usr/sbin/sbdadm";
my $cmdmap = {
create_lu => { cmd => $stmfadmcmd, method => 'create-lu' },
delete_lu => { cmd => $stmfadmcmd, method => 'delete-lu' },
import_lu => { cmd => $stmfadmcmd, method => 'import-lu' },
modify_lu => { cmd => $stmfadmcmd, method => 'modify-lu' },
add_view => { cmd => $stmfadmcmd, method => 'add-view' },
list_view => { cmd => $stmfadmcmd, method => 'list-view' },
list_lu => { cmd => $sbdadmcmd, method => 'list-lu' },
};
die "unknown command '$method'" unless exists $cmdmap->{$method};
return $cmdmap->{$method};
};
sub get_base {
return '/dev/zvol/rdsk';
}
sub run_lun_command {
my ($scfg, $timeout, $method, @params) = @_;
my $msg = '';
my $luncmd;
my $target;
my $guid;
$timeout = 10 if !$timeout;
my $output = sub {
my $line = shift;
$msg .= "$line\n";
};
if ($method eq 'create_lu') {
my $wcd = 'false';
if ($scfg->{nowritecache}) {
$wcd = 'true';
}
my $prefix = '600144f';
my $digest = md5_hex($params[0]);
$digest =~ /(\w{7}(.*))/;
$guid = "$prefix$2";
@params = ('-p', "wcd=$wcd", '-p', "guid=$guid", @params);
} elsif ($method eq 'modify_lu') {
@params = ('-s', @params);
} elsif ($method eq 'list_view') {
@params = ('-l', @params);
} elsif ($method eq 'list_lu') {
$guid = $params[0];
@params = undef;
} elsif ($method eq 'add_view') {
if ($scfg->{comstar_tg}) {
unshift @params, $scfg->{comstar_tg};
unshift @params, '--target-group';
}
if ($scfg->{comstar_hg}) {
unshift @params, $scfg->{comstar_hg};
unshift @params, '--host-group';
}
}
my $cmdmap = $get_lun_cmd_map->($method);
$luncmd = $cmdmap->{cmd};
my $lunmethod = $cmdmap->{method};
$target = 'root@' . $scfg->{portal};
my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $lunmethod, @params];
run_command($cmd, outfunc => $output, timeout => $timeout);
if ($method eq 'list_view') {
my @lines = split /\n/, $msg;
$msg = undef;
foreach my $line (@lines) {
if ($line =~ /^\s*LUN\s*:\s*(\d+)$/) {
$msg = $1;
last;
}
}
} elsif ($method eq 'list_lu') {
my $object = $guid;
my @lines = split /\n/, $msg;
$msg = undef;
foreach my $line (@lines) {
if ($line =~ /(\w+)\s+\d+\s+$object$/) {
$msg = $1;
last;
}
}
} elsif ($method eq 'create_lu') {
$msg = $guid;
}
return $msg;
}

View File

@ -0,0 +1,478 @@
package PVE::Storage::LunCmd::Iet;
# iscsi storage running Debian
# 1) apt-get install iscsitarget iscsitarget-dkms
# 2) Create target like (/etc/iet/ietd.conf):
# Target iqn.2001-04.com.example:tank
# Alias tank
# 3) Activate daemon (/etc/default/iscsitarget)
# ISCSITARGET_ENABLE=true
# 4) service iscsitarget start
#
# On one of the proxmox nodes:
# 1) Login as root
# 2) ssh-copy-id <ip_of_iscsi_storage>
use strict;
use warnings;
use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach);
sub get_base;
# A logical unit can max have 16864 LUNs
# http://manpages.ubuntu.com/manpages/precise/man5/ietd.conf.5.html
my $MAX_LUNS = 16864;
my $CONFIG_FILE = '/etc/iet/ietd.conf';
my $DAEMON = '/usr/sbin/ietadm';
my $SETTINGS = undef;
my $CONFIG = undef;
my $OLD_CONFIG = undef;
my @ssh_opts = ('-o', 'BatchMode=yes');
my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
my @scp_cmd = ('/usr/bin/scp', @ssh_opts);
my $id_rsa_path = '/etc/pve/priv/zfs';
my $ietadm = '/usr/sbin/ietadm';
my $execute_command = sub {
my ($scfg, $exec, $timeout, $method, @params) = @_;
my $msg = '';
my $err = undef;
my $target;
my $cmd;
my $res = ();
$timeout = 10 if !$timeout;
my $output = sub {
my $line = shift;
$msg .= "$line\n";
};
my $errfunc = sub {
my $line = shift;
$err .= "$line";
};
if ($exec eq 'scp') {
$target = 'root@[' . $scfg->{portal} . ']';
$cmd = [@scp_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", '--', $method, "$target:$params[0]"];
} else {
$target = 'root@' . $scfg->{portal};
$cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, '--', $method, @params];
}
eval {
run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
};
if ($@) {
$res = {
result => 0,
msg => $err,
}
} else {
$res = {
result => 1,
msg => $msg,
}
}
return $res;
};
my $read_config = sub {
my ($scfg, $timeout) = @_;
my $msg = '';
my $err = undef;
my $luncmd = 'cat';
my $target;
$timeout = 10 if !$timeout;
my $output = sub {
my $line = shift;
$msg .= "$line\n";
};
my $errfunc = sub {
my $line = shift;
$err .= "$line";
};
$target = 'root@' . $scfg->{portal};
my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $CONFIG_FILE];
eval {
run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
};
if ($@) {
die $err if ($err !~ /No such file or directory/);
die "No configuration found. Install iet on $scfg->{portal}" if $msg eq '';
}
return $msg;
};
my $get_config = sub {
my ($scfg) = @_;
my @conf = undef;
my $config = $read_config->($scfg, undef);
die "Missing config file" unless $config;
$OLD_CONFIG = $config;
return $config;
};
my $parser = sub {
my ($scfg) = @_;
my $line = 0;
my $base = get_base;
my $config = $get_config->($scfg);
my @cfgfile = split "\n", $config;
my $cfg_target = 0;
foreach (@cfgfile) {
$line++;
if ($_ =~ /^\s*Target\s*([\w\-\:\.]+)\s*$/) {
if ($1 eq $scfg->{target} && ! $cfg_target) {
# start colect info
die "$line: Parse error [$_]" if $SETTINGS;
$SETTINGS->{target} = $1;
$cfg_target = 1;
} elsif ($1 eq $scfg->{target} && $cfg_target) {
die "$line: Parse error [$_]";
} elsif ($cfg_target) {
$cfg_target = 0;
$CONFIG .= "$_\n";
} else {
$CONFIG .= "$_\n";
}
} else {
if ($cfg_target) {
$SETTINGS->{text} .= "$_\n";
next if ($_ =~ /^\s*#/ || ! $_);
my $option = $_;
if ($_ =~ /^(\w+)\s*#/) {
$option = $1;
}
if ($option =~ /^\s*(\w+)\s+(\w+)\s*$/) {
if ($1 eq 'Lun') {
die "$line: Parse error [$_]";
}
$SETTINGS->{$1} = $2;
} elsif ($option =~ /^\s*(\w+)\s+(\d+)\s+([\w\-\/=,]+)\s*$/) {
die "$line: Parse error [$option]" unless ($1 eq 'Lun');
my $conf = undef;
my $num = $2;
my @lun = split ',', $3;
die "$line: Parse error [$option]" unless (scalar(@lun) > 1);
foreach (@lun) {
my @lun_opt = split '=', $_;
die "$line: Parse error [$option]" unless (scalar(@lun_opt) == 2);
$conf->{$lun_opt[0]} = $lun_opt[1];
}
if ($conf->{Path} && $conf->{Path} =~ /^$base\/$scfg->{pool}\/([\w\-]+)$/) {
$conf->{include} = 1;
} else {
$conf->{include} = 0;
}
$conf->{lun} = $num;
push @{$SETTINGS->{luns}}, $conf;
} else {
die "$line: Parse error [$option]";
}
} else {
$CONFIG .= "$_\n";
}
}
}
$CONFIG =~ s/^\s+|\s+$|"\s*//g;
};
my $update_config = sub {
my ($scfg) = @_;
my $file = "/tmp/config$$";
my $config = '';
while ((my $option, my $value) = each(%$SETTINGS)) {
next if ($option eq 'include' || $option eq 'luns' || $option eq 'Path' || $option eq 'text' || $option eq 'used');
if ($option eq 'target') {
$config = "\n\nTarget " . $SETTINGS->{target} . "\n" . $config;
} else {
$config .= "\t$option\t\t\t$value\n";
}
}
foreach my $lun (@{$SETTINGS->{luns}}) {
my $lun_opt = '';
while ((my $option, my $value) = each(%$lun)) {
next if ($option eq 'include' || $option eq 'lun' || $option eq 'Path');
if ($lun_opt eq '') {
$lun_opt = $option . '=' . $value;
} else {
$lun_opt .= ',' . $option . '=' . $value;
}
}
$config .= "\tLun $lun->{lun} Path=$lun->{Path},$lun_opt\n";
}
open(my $fh, '>', $file) or die "Could not open file '$file' $!";
print $fh $CONFIG;
print $fh $config;
close $fh;
my @params = ($CONFIG_FILE);
my $res = $execute_command->($scfg, 'scp', undef, $file, @params);
unlink $file;
die $res->{msg} unless $res->{result};
};
my $get_target_tid = sub {
my ($scfg) = @_;
my $proc = '/proc/net/iet/volume';
my $tid = undef;
my @params = ($proc);
my $res = $execute_command->($scfg, 'ssh', undef, 'cat', @params);
die $res->{msg} unless $res->{result};
my @cfg = split "\n", $res->{msg};
foreach (@cfg) {
if ($_ =~ /^\s*tid:(\d+)\s+name:([\w\-\:\.]+)\s*$/) {
if ($2 && $2 eq $scfg->{target}) {
$tid = $1;
last;
}
}
}
return $tid;
};
my $get_lu_name = sub {
my $used = ();
my $i;
if (! exists $SETTINGS->{used}) {
for ($i = 0; $i < $MAX_LUNS; $i++) {
$used->{$i} = 0;
}
foreach my $lun (@{$SETTINGS->{luns}}) {
$used->{$lun->{lun}} = 1;
}
$SETTINGS->{used} = $used;
}
$used = $SETTINGS->{used};
for ($i = 0; $i < $MAX_LUNS; $i++) {
last unless $used->{$i};
}
$SETTINGS->{used}->{$i} = 1;
return $i;
};
my $init_lu_name = sub {
my $used = ();
if (! exists($SETTINGS->{used})) {
for (my $i = 0; $i < $MAX_LUNS; $i++) {
$used->{$i} = 0;
}
$SETTINGS->{used} = $used;
}
foreach my $lun (@{$SETTINGS->{luns}}) {
$SETTINGS->{used}->{$lun->{lun}} = 1;
}
};
my $free_lu_name = sub {
my ($lu_name) = @_;
my $new;
foreach my $lun (@{$SETTINGS->{luns}}) {
if ($lun->{lun} != $lu_name) {
push @$new, $lun;
}
}
$SETTINGS->{luns} = $new;
$SETTINGS->{used}->{$lu_name} = 0;
};
my $make_lun = sub {
my ($scfg, $path) = @_;
die 'Maximum number of LUNs per target is 16384' if scalar @{$SETTINGS->{luns}} >= $MAX_LUNS;
my $lun = $get_lu_name->();
my $conf = {
lun => $lun,
Path => $path,
Type => 'blockio',
include => 1,
};
push @{$SETTINGS->{luns}}, $conf;
return $conf;
};
my $list_view = sub {
my ($scfg, $timeout, $method, @params) = @_;
my $lun = undef;
my $object = $params[0];
foreach my $lun (@{$SETTINGS->{luns}}) {
next unless $lun->{include} == 1;
if ($lun->{Path} =~ /^$object$/) {
return $lun->{lun} if (defined($lun->{lun}));
die "$lun->{Path}: Missing LUN";
}
}
return $lun;
};
my $list_lun = sub {
my ($scfg, $timeout, $method, @params) = @_;
my $name = undef;
my $object = $params[0];
foreach my $lun (@{$SETTINGS->{luns}}) {
next unless $lun->{include} == 1;
if ($lun->{Path} =~ /^$object$/) {
return $lun->{Path};
}
}
return $name;
};
my $create_lun = sub {
my ($scfg, $timeout, $method, @params) = @_;
if ($list_lun->($scfg, $timeout, $method, @params)) {
die "$params[0]: LUN exists";
}
my $lun = $params[0];
$lun = $make_lun->($scfg, $lun);
my $tid = $get_target_tid->($scfg);
$update_config->($scfg);
my $path = "Path=$lun->{Path},Type=$lun->{Type}";
@params = ('--op', 'new', "--tid=$tid", "--lun=$lun->{lun}", '--params', $path);
my $res = $execute_command->($scfg, 'ssh', $timeout, $ietadm, @params);
do {
$free_lu_name->($lun->{lun});
$update_config->($scfg);
die $res->{msg};
} unless $res->{result};
return $res->{msg};
};
my $delete_lun = sub {
my ($scfg, $timeout, $method, @params) = @_;
my $res = {msg => undef};
my $path = $params[0];
my $tid = $get_target_tid->($scfg);
foreach my $lun (@{$SETTINGS->{luns}}) {
if ($lun->{Path} eq $path) {
@params = ('--op', 'delete', "--tid=$tid", "--lun=$lun->{lun}");
$res = $execute_command->($scfg, 'ssh', $timeout, $ietadm, @params);
if ($res->{result}) {
$free_lu_name->($lun->{lun});
$update_config->($scfg);
last;
} else {
die $res->{msg};
}
}
}
return $res->{msg};
};
my $import_lun = sub {
my ($scfg, $timeout, $method, @params) = @_;
return $create_lun->($scfg, $timeout, $method, @params);
};
my $modify_lun = sub {
my ($scfg, $timeout, $method, @params) = @_;
my $lun;
my $res;
my $path = $params[1];
my $tid = $get_target_tid->($scfg);
foreach my $cfg (@{$SETTINGS->{luns}}) {
if ($cfg->{Path} eq $path) {
$lun = $cfg;
last;
}
}
@params = ('--op', 'delete', "--tid=$tid", "--lun=$lun->{lun}");
$res = $execute_command->($scfg, 'ssh', $timeout, $ietadm, @params);
die $res->{msg} unless $res->{result};
$path = "Path=$lun->{Path},Type=$lun->{Type}";
@params = ('--op', 'new', "--tid=$tid", "--lun=$lun->{lun}", '--params', $path);
$res = $execute_command->($scfg, 'ssh', $timeout, $ietadm, @params);
die $res->{msg} unless $res->{result};
return $res->{msg};
};
my $add_view = sub {
my ($scfg, $timeout, $method, @params) = @_;
return '';
};
my $get_lun_cmd_map = sub {
my ($method) = @_;
my $cmdmap = {
create_lu => { cmd => $create_lun },
delete_lu => { cmd => $delete_lun },
import_lu => { cmd => $import_lun },
modify_lu => { cmd => $modify_lun },
add_view => { cmd => $add_view },
list_view => { cmd => $list_view },
list_lu => { cmd => $list_lun },
};
die "unknown command '$method'" unless exists $cmdmap->{$method};
return $cmdmap->{$method};
};
sub run_lun_command {
my ($scfg, $timeout, $method, @params) = @_;
$parser->($scfg) unless $SETTINGS;
my $cmdmap = $get_lun_cmd_map->($method);
my $msg = $cmdmap->{cmd}->($scfg, $timeout, $method, @params);
return $msg;
}
sub get_base {
return '/dev';
}
1;

View File

@ -0,0 +1,601 @@
package PVE::Storage::LunCmd::Istgt;
# TODO
# Create initial target and LUN if target is missing ?
# Create and use list of free LUNs
use strict;
use warnings;
use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach);
my @CONFIG_FILES = (
'/usr/local/etc/istgt/istgt.conf', # FreeBSD, FreeNAS
'/var/etc/iscsi/istgt.conf' # NAS4Free
);
my @DAEMONS = (
'/usr/local/etc/rc.d/istgt', # FreeBSD, FreeNAS
'/var/etc/rc.d/istgt' # NAS4Free
);
# A logical unit can max have 63 LUNs
# https://code.google.com/p/istgt/source/browse/src/istgt_lu.h#39
my $MAX_LUNS = 64;
my $CONFIG_FILE = undef;
my $DAEMON = undef;
my $SETTINGS = undef;
my $CONFIG = undef;
my $OLD_CONFIG = undef;
my @ssh_opts = ('-o', 'BatchMode=yes');
my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
my @scp_cmd = ('/usr/bin/scp', @ssh_opts);
my $id_rsa_path = '/etc/pve/priv/zfs';
#Current SIGHUP reload limitations (http://www.peach.ne.jp/archives/istgt/):
#
# The parameters other than PG, IG, and LU are not reloaded by SIGHUP.
# LU connected by the initiator can't be reloaded by SIGHUP.
# PG and IG mapped to LU can't be deleted by SIGHUP.
# If you delete an active LU, all connections of the LU are closed by SIGHUP.
# Updating IG is not affected until the next login.
#
# FreeBSD
# 1. Alt-F2 to change to native shell (zfsguru)
# 2. pw mod user root -w yes (change password for root to root)
# 3. vi /etc/ssh/sshd_config
# 4. uncomment PermitRootLogin yes
# 5. change PasswordAuthentication no to PasswordAuthentication yes
# 5. /etc/rc.d/sshd restart
# 6. On one of the proxmox nodes login as root and run: ssh-copy-id ip_freebsd_host
# 7. vi /etc/ssh/sshd_config
# 8. comment PermitRootLogin yes
# 9. change PasswordAuthentication yes to PasswordAuthentication no
# 10. /etc/rc.d/sshd restart
# 11. Reset passwd -> pw mod user root -w no
# 12. Alt-Ctrl-F1 to return to zfsguru shell (zfsguru)
sub get_base;
sub run_lun_command;
my $read_config = sub {
my ($scfg, $timeout, $method) = @_;
my $msg = '';
my $err = undef;
my $luncmd = 'cat';
my $target;
$timeout = 10 if !$timeout;
my $output = sub {
my $line = shift;
$msg .= "$line\n";
};
my $errfunc = sub {
my $line = shift;
$err .= "$line";
};
$target = 'root@' . $scfg->{portal};
my $daemon = 0;
foreach my $config (@CONFIG_FILES) {
$err = undef;
my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $config];
eval {
run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
};
do {
$err = undef;
$DAEMON = $DAEMONS[$daemon];
$CONFIG_FILE = $config;
last;
} unless $@;
$daemon++;
}
die $err if ($err && $err !~ /No such file or directory/);
die "No configuration found. Install istgt on $scfg->{portal}" if $msg eq '';
return $msg;
};
my $get_config = sub {
my ($scfg) = @_;
my @conf = undef;
my $config = $read_config->($scfg, undef, 'get_config');
die "Missing config file" unless $config;
$OLD_CONFIG = $config;
return $config;
};
my $parse_size = sub {
my ($text) = @_;
return 0 if !$text;
if ($text =~ m/^(\d+(\.\d+)?)([TGMK]B)?$/) {
my ($size, $reminder, $unit) = ($1, $2, $3);
return $size if !$unit;
if ($unit eq 'KB') {
$size *= 1024;
} elsif ($unit eq 'MB') {
$size *= 1024*1024;
} elsif ($unit eq 'GB') {
$size *= 1024*1024*1024;
} elsif ($unit eq 'TB') {
$size *= 1024*1024*1024*1024;
}
if ($reminder) {
$size = ceil($size);
}
return $size;
} elsif ($text =~ /^auto$/i) {
return 'AUTO';
} else {
return 0;
}
};
my $size_with_unit = sub {
my ($size, $n) = (shift, 0);
return '0KB' if !$size;
return $size if $size eq 'AUTO';
if ($size =~ m/^\d+$/) {
++$n and $size /= 1024 until $size < 1024;
if ($size =~ /\./) {
return sprintf "%.2f%s", $size, ( qw[bytes KB MB GB TB] )[ $n ];
} else {
return sprintf "%d%s", $size, ( qw[bytes KB MB GB TB] )[ $n ];
}
}
die "$size: Not a number";
};
my $lun_dumper = sub {
my ($lun) = @_;
my $config = '';
$config .= "\n[$lun]\n";
$config .= 'TargetName ' . $SETTINGS->{$lun}->{TargetName} . "\n";
$config .= 'Mapping ' . $SETTINGS->{$lun}->{Mapping} . "\n";
$config .= 'AuthGroup ' . $SETTINGS->{$lun}->{AuthGroup} . "\n";
$config .= 'UnitType ' . $SETTINGS->{$lun}->{UnitType} . "\n";
$config .= 'QueueDepth ' . $SETTINGS->{$lun}->{QueueDepth} . "\n";
foreach my $conf (@{$SETTINGS->{$lun}->{luns}}) {
$config .= "$conf->{lun} Storage " . $conf->{Storage};
$config .= ' ' . $size_with_unit->($conf->{Size}) . "\n";
foreach ($conf->{options}) {
if ($_) {
$config .= "$conf->{lun} Option " . $_ . "\n";
}
}
}
$config .= "\n";
return $config;
};
my $get_lu_name = sub {
my ($target) = @_;
my $used = ();
my $i;
if (! exists $SETTINGS->{$target}->{used}) {
for ($i = 0; $i < $MAX_LUNS; $i++) {
$used->{$i} = 0;
}
foreach my $lun (@{$SETTINGS->{$target}->{luns}}) {
$lun->{lun} =~ /^LUN(\d+)$/;
$used->{$1} = 1;
}
$SETTINGS->{$target}->{used} = $used;
}
$used = $SETTINGS->{$target}->{used};
for ($i = 0; $i < $MAX_LUNS; $i++) {
last unless $used->{$i};
}
$SETTINGS->{$target}->{used}->{$i} = 1;
return "LUN$i";
};
my $init_lu_name = sub {
my ($target) = @_;
my $used = ();
if (! exists($SETTINGS->{$target}->{used})) {
for (my $i = 0; $i < $MAX_LUNS; $i++) {
$used->{$i} = 0;
}
$SETTINGS->{$target}->{used} = $used;
}
foreach my $lun (@{$SETTINGS->{$target}->{luns}}) {
$lun->{lun} =~ /^LUN(\d+)$/;
$SETTINGS->{$target}->{used}->{$1} = 1;
}
};
my $free_lu_name = sub {
my ($target, $lu_name) = @_;
$lu_name =~ /^LUN(\d+)$/;
$SETTINGS->{$target}->{used}->{$1} = 0;
};
my $make_lun = sub {
my ($scfg, $path) = @_;
my $target = $SETTINGS->{current};
die 'Maximum number of LUNs per target is 63' if scalar @{$SETTINGS->{$target}->{luns}} >= $MAX_LUNS;
my @options = ();
my $lun = $get_lu_name->($target);
if ($scfg->{nowritecache}) {
push @options, "WriteCache Disable";
}
my $conf = {
lun => $lun,
Storage => $path,
Size => 'AUTO',
options => @options,
};
push @{$SETTINGS->{$target}->{luns}}, $conf;
return $conf->{lun};
};
my $parser = sub {
my ($scfg) = @_;
my $lun = undef;
my $line = 0;
my $config = $get_config->($scfg);
my @cfgfile = split "\n", $config;
foreach (@cfgfile) {
$line++;
if ($_ =~ /^\s*\[(PortalGroup\d+)\]\s*/) {
$lun = undef;
$SETTINGS->{$1} = ();
} elsif ($_ =~ /^\s*\[(InitiatorGroup\d+)\]\s*/) {
$lun = undef;
$SETTINGS->{$1} = ();
} elsif ($_ =~ /^\s*PidFile\s+"?([\w\/\.]+)"?\s*/) {
$lun = undef;
$SETTINGS->{pidfile} = $1;
} elsif ($_ =~ /^\s*NodeBase\s+"?([\w\-\.]+)"?\s*/) {
$lun = undef;
$SETTINGS->{nodebase} = $1;
} elsif ($_ =~ /^\s*\[(LogicalUnit\d+)\]\s*/) {
$lun = $1;
$SETTINGS->{$lun} = ();
$SETTINGS->{targets}++;
} elsif ($lun) {
next if (($_ =~ /^\s*#/) || ($_ =~ /^\s*$/));
if ($_ =~ /^\s*(\w+)\s+(.+)\s*/) {
my $arg1 = $1;
my $arg2 = $2;
$arg2 =~ s/^\s+|\s+$|"\s*//g;
if ($arg2 =~ /^Storage\s*(.+)/i) {
$SETTINGS->{$lun}->{$arg1}->{storage} = $1;
} elsif ($arg2 =~ /^Option\s*(.+)/i) {
push @{$SETTINGS->{$lun}->{$arg1}->{options}}, $1;
} else {
$SETTINGS->{$lun}->{$arg1} = $arg2;
}
} else {
die "$line: parse error [$_]";
}
}
$CONFIG .= "$_\n" unless $lun;
}
$CONFIG =~ s/\n$//;
die "$scfg->{target}: Target not found" unless $SETTINGS->{targets};
my $max = $SETTINGS->{targets};
my $base = get_base;
for (my $i = 1; $i <= $max; $i++) {
my $target = $SETTINGS->{nodebase}.':'.$SETTINGS->{"LogicalUnit$i"}->{TargetName};
if ($target eq $scfg->{target}) {
my $lu = ();
while ((my $key, my $val) = each(%{$SETTINGS->{"LogicalUnit$i"}})) {
if ($key =~ /^LUN\d+/) {
$val->{storage} =~ /^([\w\/\-]+)\s+(\w+)/;
my $storage = $1;
my $size = $parse_size->($2);
my $conf = undef;
my @options = ();
if ($val->{options}) {
@options = @{$val->{options}};
}
if ($storage =~ /^$base\/$scfg->{pool}\/([\w\-]+)$/) {
$conf = {
lun => $key,
Storage => $storage,
Size => $size,
options => @options,
}
}
push @$lu, $conf if $conf;
delete $SETTINGS->{"LogicalUnit$i"}->{$key};
}
}
$SETTINGS->{"LogicalUnit$i"}->{luns} = $lu;
$SETTINGS->{current} = "LogicalUnit$i";
$init_lu_name->("LogicalUnit$i");
} else {
$CONFIG .= $lun_dumper->("LogicalUnit$i");
delete $SETTINGS->{"LogicalUnit$i"};
$SETTINGS->{targets}--;
}
}
die "$scfg->{target}: Target not found" unless $SETTINGS->{targets} > 0;
};
my $list_lun = sub {
my ($scfg, $timeout, $method, @params) = @_;
my $name = undef;
my $object = $params[0];
for my $key (keys %$SETTINGS) {
next unless $key =~ /^LogicalUnit\d+$/;
foreach my $lun (@{$SETTINGS->{$key}->{luns}}) {
if ($lun->{Storage} =~ /^$object$/) {
return $lun->{Storage};
}
}
}
return $name;
};
my $create_lun = sub {
my ($scfg, $timeout, $method, @params) = @_;
my $res = ();
my $file = "/tmp/config$$";
if ($list_lun->($scfg, $timeout, $method, @params)) {
die "$params[0]: LUN exists";
}
my $lun = $params[0];
$lun = $make_lun->($scfg, $lun);
my $config = $lun_dumper->($SETTINGS->{current});
open(my $fh, '>', $file) or die "Could not open file '$file' $!";
print $fh $CONFIG;
print $fh $config;
close $fh;
@params = ($CONFIG_FILE);
$res = {
cmd => 'scp',
method => $file,
params => \@params,
msg => $lun,
post_exe => sub {
unlink $file;
},
};
return $res;
};
my $delete_lun = sub {
my ($scfg, $timeout, $method, @params) = @_;
my $res = ();
my $file = "/tmp/config$$";
my $target = $SETTINGS->{current};
my $luns = ();
foreach my $conf (@{$SETTINGS->{$target}->{luns}}) {
if ($conf->{Storage} =~ /^$params[0]$/) {
$free_lu_name->($target, $conf->{lun});
} else {
push @$luns, $conf;
}
}
$SETTINGS->{$target}->{luns} = $luns;
my $config = $lun_dumper->($SETTINGS->{current});
open(my $fh, '>', $file) or die "Could not open file '$file' $!";
print $fh $CONFIG;
print $fh $config;
close $fh;
@params = ($CONFIG_FILE);
$res = {
cmd => 'scp',
method => $file,
params => \@params,
post_exe => sub {
unlink $file;
run_lun_command($scfg, undef, 'add_view', 'restart');
},
};
return $res;
};
my $import_lun = sub {
my ($scfg, $timeout, $method, @params) = @_;
my $res = $create_lun->($scfg, $timeout, $method, @params);
return $res;
};
my $add_view = sub {
my ($scfg, $timeout, $method, @params) = @_;
my $cmdmap;
if (@params && $params[0] eq 'restart') {
@params = ('onerestart', '>&', '/dev/null');
$cmdmap = {
cmd => 'ssh',
method => $DAEMON,
params => \@params,
};
} else {
@params = ('-HUP', '`cat '. "$SETTINGS->{pidfile}`");
$cmdmap = {
cmd => 'ssh',
method => 'kill',
params => \@params,
};
}
return $cmdmap;
};
my $modify_lun = sub {
my ($scfg, $timeout, $method, @params) = @_;
# Current SIGHUP reload limitations
# LU connected by the initiator can't be reloaded by SIGHUP.
# Until above limitation persists modifying a LUN will require
# a restart of the daemon breaking all current connections
#die 'Modify a connected LUN is not currently supported by istgt';
@params = ('restart', @params);
return $add_view->($scfg, $timeout, $method, @params);
};
my $list_view = sub {
my ($scfg, $timeout, $method, @params) = @_;
my $lun = undef;
my $object = $params[0];
for my $key (keys %$SETTINGS) {
next unless $key =~ /^LogicalUnit\d+$/;
foreach my $lun (@{$SETTINGS->{$key}->{luns}}) {
if ($lun->{Storage} =~ /^$object$/) {
if ($lun->{lun} =~ /^LUN(\d+)/) {
return $1;
}
die "$lun->{Storage}: Missing LUN";
}
}
}
return $lun;
};
my $get_lun_cmd_map = sub {
my ($method) = @_;
my $cmdmap = {
create_lu => { cmd => $create_lun },
delete_lu => { cmd => $delete_lun },
import_lu => { cmd => $import_lun },
modify_lu => { cmd => $modify_lun },
add_view => { cmd => $add_view },
list_view => { cmd => $list_view },
list_lu => { cmd => $list_lun },
};
die "unknown command '$method'" unless exists $cmdmap->{$method};
return $cmdmap->{$method};
};
sub run_lun_command {
my ($scfg, $timeout, $method, @params) = @_;
my $msg = '';
my $luncmd;
my $target;
my $cmd;
my $res;
$timeout = 10 if !$timeout;
my $is_add_view = 0;
my $output = sub {
my $line = shift;
$msg .= "$line\n";
};
$target = 'root@' . $scfg->{portal};
$parser->($scfg) unless $SETTINGS;
my $cmdmap = $get_lun_cmd_map->($method);
if ($method eq 'add_view') {
$is_add_view = 1 ;
$timeout = 15;
}
if (ref $cmdmap->{cmd} eq 'CODE') {
$res = $cmdmap->{cmd}->($scfg, $timeout, $method, @params);
if (ref $res) {
$method = $res->{method};
@params = @{$res->{params}};
if ($res->{cmd} eq 'scp') {
$cmd = [@scp_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $method, "$target:$params[0]"];
} else {
$cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $method, @params];
}
} else {
return $res;
}
} else {
$luncmd = $cmdmap->{cmd};
$method = $cmdmap->{method};
$cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $method, @params];
}
eval {
run_command($cmd, outfunc => $output, timeout => $timeout);
};
if ($@ && $is_add_view) {
my $err = $@;
if ($OLD_CONFIG) {
my $err1 = undef;
my $file = "/tmp/config$$";
open(my $fh, '>', $file) or die "Could not open file '$file' $!";
print $fh $OLD_CONFIG;
close $fh;
$cmd = [@scp_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $file, $CONFIG_FILE];
eval {
run_command($cmd, outfunc => $output, timeout => $timeout);
};
$err1 = $@ if $@;
unlink $file;
die "$err\n$err1" if $err1;
eval {
run_lun_command($scfg, undef, 'add_view', 'restart');
};
die "$err\n$@" if ($@);
}
die $err;
} elsif ($@) {
die $@;
} elsif ($is_add_view) {
$OLD_CONFIG = undef;
}
if ($res->{post_exe} && ref $res->{post_exe} eq 'CODE') {
$res->{post_exe}->();
}
if ($res->{msg}) {
$msg = $res->{msg};
}
return $msg;
}
sub get_base {
return '/dev/zvol';
}
1;

View File

@ -0,0 +1,420 @@
package PVE::Storage::LunCmd::LIO;
# lightly based on code from Iet.pm
#
# additional changes:
# -----------------------------------------------------------------
# Copyright (c) 2018 BestSolution.at EDV Systemhaus GmbH
# All Rights Reserved.
#
# This software is released under the terms of the
#
# "GNU Affero General Public License"
#
# and may only be distributed and used under the terms of the
# mentioned license. You should have received a copy of the license
# along with this software product, if not you can download it from
# https://www.gnu.org/licenses/agpl-3.0.en.html
#
# Author: udo.rader@bestsolution.at
# -----------------------------------------------------------------
use strict;
use warnings;
use PVE::Tools qw(run_command);
use JSON;
sub get_base;
# targetcli constants
# config file location differs from distro to distro
my @CONFIG_FILES = (
'/etc/rtslib-fb-target/saveconfig.json', # Debian 9.x et al
'/etc/target/saveconfig.json' , # ArchLinux, CentOS
);
my $BACKSTORE = '/backstores/block';
my $SETTINGS = undef;
my $SETTINGS_TIMESTAMP = 0;
my $SETTINGS_MAXAGE = 15; # in seconds
my @ssh_opts = ('-o', 'BatchMode=yes');
my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
my $id_rsa_path = '/etc/pve/priv/zfs';
my $targetcli = '/usr/bin/targetcli';
my $execute_remote_command = sub {
my ($scfg, $timeout, $remote_command, @params) = @_;
my $msg = '';
my $err = undef;
my $target;
my $cmd;
my $res = ();
$timeout = 10 if !$timeout;
my $output = sub { $msg .= "$_[0]\n" };
my $errfunc = sub { $err .= "$_[0]\n" };
$target = 'root@' . $scfg->{portal};
$cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, '--', $remote_command, @params];
eval {
run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
};
if ($@) {
$res = {
result => 0,
msg => $err,
}
} else {
$res = {
result => 1,
msg => $msg,
}
}
return $res;
};
# fetch targetcli configuration from the portal
my $read_config = sub {
my ($scfg, $timeout) = @_;
my $msg = '';
my $err = undef;
my $luncmd = 'cat';
my $target;
my $retry = 1;
$timeout = 10 if !$timeout;
my $output = sub { $msg .= "$_[0]\n" };
my $errfunc = sub { $err .= "$_[0]\n" };
$target = 'root@' . $scfg->{portal};
foreach my $oneFile (@CONFIG_FILES) {
my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $oneFile];
eval {
run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
};
if ($@) {
die $err if ($err !~ /No such file or directory/);
}
return $msg if $msg ne '';
}
die "No configuration found. Install targetcli on $scfg->{portal}\n" if $msg eq '';
return $msg;
};
my $get_config = sub {
my ($scfg) = @_;
my @conf = undef;
my $config = $read_config->($scfg, undef);
die "Missing config file" unless $config;
return $config;
};
# Return settings of a specific target
my $get_target_settings = sub {
my ($scfg) = @_;
my $id = "$scfg->{portal}.$scfg->{target}";
return undef if !$SETTINGS;
return $SETTINGS->{$id};
};
# fetches and parses targetcli config from the portal
my $parser = sub {
my ($scfg) = @_;
my $tpg = $scfg->{lio_tpg} || die "Target Portal Group not set, aborting!\n";
my $tpg_tag;
if ($tpg =~ /^tpg(\d+)$/) {
$tpg_tag = $1;
} else {
die "Target Portal Group has invalid value, must contain string 'tpg' and a suffix number, eg 'tpg17'\n";
}
my $config = $get_config->($scfg);
my $jsonconfig = JSON->new->utf8->decode($config);
my $haveTarget = 0;
foreach my $target (@{$jsonconfig->{targets}}) {
# only interested in iSCSI targets
next if !($target->{fabric} eq 'iscsi' && $target->{wwn} eq $scfg->{target});
# find correct TPG
foreach my $tpg (@{$target->{tpgs}}) {
if ($tpg->{tag} == $tpg_tag) {
my $res = [];
foreach my $lun (@{$tpg->{luns}}) {
my ($idx, $storage_object);
if ($lun->{index} =~ /^(\d+)$/) {
$idx = $1;
}
if ($lun->{storage_object} =~ m|^($BACKSTORE/.*)$|) {
$storage_object = $1;
}
die "Invalid lun definition in config!\n"
if !(defined($idx) && defined($storage_object));
push @$res, { index => $idx, storage_object => $storage_object };
}
my $id = "$scfg->{portal}.$scfg->{target}";
$SETTINGS->{$id}->{luns} = $res;
$haveTarget = 1;
last;
}
}
}
# seriously unhappy if the target server lacks iSCSI target configuration ...
if (!$haveTarget) {
die "target portal group tpg$tpg_tag not found!\n";
}
};
# Get prefix for backstores
my $get_backstore_prefix = sub {
my ($scfg) = @_;
my $pool = $scfg->{pool};
$pool =~ s/\//-/g;
return $pool . '-';
};
# removes the given lu_name from the local list of luns
my $free_lu_name = sub {
my ($scfg, $lu_name) = @_;
my $new = [];
my $target = $get_target_settings->($scfg);
foreach my $lun (@{$target->{luns}}) {
if ($lun->{storage_object} ne "$BACKSTORE/$lu_name") {
push @$new, $lun;
}
}
$target->{luns} = $new;
};
# locally registers a new lun
my $register_lun = sub {
my ($scfg, $idx, $volname) = @_;
my $conf = {
index => $idx,
storage_object => "$BACKSTORE/$volname",
is_new => 1,
};
my $target = $get_target_settings->($scfg);
push @{$target->{luns}}, $conf;
return $conf;
};
# extracts the ZFS volume name from a device path
my $extract_volname = sub {
my ($scfg, $lunpath) = @_;
my $volname = undef;
my $base = get_base;
if ($lunpath =~ /^$base\/$scfg->{pool}\/([\w\-]+)$/) {
$volname = $1;
my $prefix = $get_backstore_prefix->($scfg);
my $target = $get_target_settings->($scfg);
foreach my $lun (@{$target->{luns}}) {
# If we have a lun with the pool prefix matching this vol, then return this one
# like pool-pve-vm-100-disk-0
# Else, just fallback to the old name scheme which is vm-100-disk-0
if ($lun->{storage_object} =~ /^$BACKSTORE\/($prefix$volname)$/) {
return $1;
}
}
}
return $volname;
};
# retrieves the LUN index for a particular object
my $list_view = sub {
my ($scfg, $timeout, $method, @params) = @_;
my $lun = undef;
my $object = $params[0];
my $volname = $extract_volname->($scfg, $object);
my $target = $get_target_settings->($scfg);
return undef if !defined($volname); # nothing to search for..
foreach my $lun (@{$target->{luns}}) {
if ($lun->{storage_object} eq "$BACKSTORE/$volname") {
return $lun->{index};
}
}
return $lun;
};
# determines, if the given object exists on the portal
my $list_lun = sub {
my ($scfg, $timeout, $method, @params) = @_;
my $object = $params[0];
my $volname = $extract_volname->($scfg, $object);
my $target = $get_target_settings->($scfg);
foreach my $lun (@{$target->{luns}}) {
if ($lun->{storage_object} eq "$BACKSTORE/$volname") {
return $object;
}
}
return undef;
};
# adds a new LUN to the target
my $create_lun = sub {
my ($scfg, $timeout, $method, @params) = @_;
if ($list_lun->($scfg, $timeout, $method, @params)) {
die "$params[0]: LUN already exists!";
}
my $device = $params[0];
my $volname = $extract_volname->($scfg, $device);
# Here we create a new device, so we didn't get the volname prefixed with the pool name
# as extract_volname couldn't find a matching vol yet
$volname = $get_backstore_prefix->($scfg) . $volname;
my $tpg = $scfg->{lio_tpg} || die "Target Portal Group not set, aborting!\n";
# step 1: create backstore for device
my @cliparams = ($BACKSTORE, 'create', "name=$volname", "dev=$device" );
my $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
die $res->{msg} if !$res->{result};
# step 2: enable unmap support on the backstore
@cliparams = ($BACKSTORE . '/' . $volname, 'set', 'attribute', 'emulate_tpu=1' );
$res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
die $res->{msg} if !$res->{result};
# step 3: register lun with target
# targetcli /iscsi/iqn.2018-04.at.bestsolution.somehost:target/tpg1/luns/ create /backstores/block/foobar
@cliparams = ("/iscsi/$scfg->{target}/$tpg/luns/", 'create', "$BACKSTORE/$volname" );
$res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
die $res->{msg} if !$res->{result};
# targetcli responds with "Created LUN 99"
# not calculating the index ourselves, because the index at the portal might have
# changed without our knowledge, so relying on the number that targetcli returns
my $lun_idx;
if ($res->{msg} =~ /LUN (\d+)/) {
$lun_idx = $1;
} else {
die "unable to determine new LUN index: $res->{msg}";
}
$register_lun->($scfg, $lun_idx, $volname);
# step 3: unfortunately, targetcli doesn't always save changes, no matter
# if auto_save_on_exit is true or not. So saving to be safe ...
$execute_remote_command->($scfg, $timeout, $targetcli, 'saveconfig');
return $res->{msg};
};
my $delete_lun = sub {
my ($scfg, $timeout, $method, @params) = @_;
my $res = {msg => undef};
my $tpg = $scfg->{lio_tpg} || die "Target Portal Group not set, aborting!\n";
my $path = $params[0];
my $volname = $extract_volname->($scfg, $path);
my $target = $get_target_settings->($scfg);
foreach my $lun (@{$target->{luns}}) {
next if $lun->{storage_object} ne "$BACKSTORE/$volname";
# step 1: delete the lun
my @cliparams = ("/iscsi/$scfg->{target}/$tpg/luns/", 'delete', "lun$lun->{index}" );
my $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
do {
die $res->{msg};
} unless $res->{result};
# step 2: delete the backstore
@cliparams = ($BACKSTORE, 'delete', $volname);
$res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
do {
die $res->{msg};
} unless $res->{result};
# step 3: save to be safe ...
$execute_remote_command->($scfg, $timeout, $targetcli, 'saveconfig');
# update internal cache
$free_lu_name->($scfg, $volname);
last;
}
return $res->{msg};
};
my $import_lun = sub {
my ($scfg, $timeout, $method, @params) = @_;
return $create_lun->($scfg, $timeout, $method, @params);
};
# needed for example when the underlying ZFS volume has been resized
my $modify_lun = sub {
my ($scfg, $timeout, $method, @params) = @_;
# Nothing to do on volume modification for LIO
return undef;
};
my $add_view = sub {
my ($scfg, $timeout, $method, @params) = @_;
return '';
};
my %lun_cmd_map = (
create_lu => $create_lun,
delete_lu => $delete_lun,
import_lu => $import_lun,
modify_lu => $modify_lun,
add_view => $add_view,
list_view => $list_view,
list_lu => $list_lun,
);
sub run_lun_command {
my ($scfg, $timeout, $method, @params) = @_;
# fetch configuration from target if we haven't yet or if it is stale
my $timediff = time - $SETTINGS_TIMESTAMP;
my $target = $get_target_settings->($scfg);
if (!$target || $timediff > $SETTINGS_MAXAGE) {
$SETTINGS_TIMESTAMP = time;
$parser->($scfg);
}
die "unknown command '$method'" unless exists $lun_cmd_map{$method};
my $msg = $lun_cmd_map{$method}->($scfg, $timeout, $method, @params);
return $msg;
}
sub get_base {
return '/dev';
}
1;

View File

@ -0,0 +1,5 @@
SOURCES=Comstar.pm Istgt.pm Iet.pm LIO.pm
.PHONY: install
install:
for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/Storage/LunCmd/$$i; done

View File

@ -0,0 +1,393 @@
package PVE::Storage::LvmThinPlugin;
use strict;
use warnings;
use IO::File;
use PVE::Tools qw(run_command trim);
use PVE::Storage::Plugin;
use PVE::Storage::LVMPlugin;
use PVE::JSONSchema qw(get_standard_option);
# see: man lvmthin
# lvcreate -n ThinDataLV -L LargeSize VG
# lvconvert --type thin-pool VG/ThinDataLV
# lvcreate -n pvepool -L 20G pve
# lvconvert --type thin-pool pve/pvepool
# NOTE: volumes which were created as linked clones of another base volume
# are currently not tracking this relationship in their volume IDs. this is
# generally not a problem, as LVM thin allows deletion of such base volumes
# without affecting the linked clones. this leads to increased disk usage
# when migrating LVM-thin volumes, which is normally prevented for linked clones.
use base qw(PVE::Storage::LVMPlugin);
sub type {
return 'lvmthin';
}
sub plugindata {
return {
content => [ {images => 1, rootdir => 1}, { images => 1, rootdir => 1}],
};
}
sub properties {
return {
thinpool => {
description => "LVM thin pool LV name.",
type => 'string', format => 'pve-storage-vgname',
},
};
}
sub options {
return {
thinpool => { fixed => 1 },
vgname => { fixed => 1 },
nodes => { optional => 1 },
disable => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
};
}
# NOTE: the fourth and fifth element of the returned array are always
# undef, even if the volume is a linked clone of another volume. see note
# at beginning of file.
sub parse_volname {
my ($class, $volname) = @_;
PVE::Storage::Plugin::parse_lvm_name($volname);
if ($volname =~ m/^((vm|base)-(\d+)-\S+)$/) {
return ('images', $1, $3, undef, undef, $2 eq 'base', 'raw');
}
die "unable to parse lvm volume name '$volname'\n";
}
sub filesystem_path {
my ($class, $scfg, $volname, $snapname) = @_;
my ($vtype, $name, $vmid) = $class->parse_volname($volname);
my $vg = $scfg->{vgname};
my $path = defined($snapname) ? "/dev/$vg/snap_${name}_$snapname": "/dev/$vg/$name";
return wantarray ? ($path, $vmid, $vtype) : $path;
}
sub alloc_image {
my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
die "unsupported format '$fmt'" if $fmt ne 'raw';
die "illegal name '$name' - should be 'vm-$vmid-*'\n"
if $name && $name !~ m/^vm-$vmid-/;
my $vgs = PVE::Storage::LVMPlugin::lvm_vgs();
my $vg = $scfg->{vgname};
die "no such volume group '$vg'\n" if !defined ($vgs->{$vg});
$name = $class->find_free_diskname($storeid, $scfg, $vmid)
if !$name;
my $cmd = ['/sbin/lvcreate', '-aly', '-V', "${size}k", '--name', $name,
'--thinpool', "$vg/$scfg->{thinpool}" ];
run_command($cmd, errmsg => "lvcreate '$vg/$name' error");
return $name;
}
sub free_image {
my ($class, $storeid, $scfg, $volname, $isBase) = @_;
my $vg = $scfg->{vgname};
my $lvs = PVE::Storage::LVMPlugin::lvm_list_volumes($vg);
if (my $dat = $lvs->{$scfg->{vgname}}) {
# remove all volume snapshots first
foreach my $lv (keys %$dat) {
next if $lv !~ m/^snap_${volname}_${PVE::JSONSchema::CONFIGID_RE}$/;
my $cmd = ['/sbin/lvremove', '-f', "$vg/$lv"];
run_command($cmd, errmsg => "lvremove snapshot '$vg/$lv' error");
}
# finally remove original (if exists)
if ($dat->{$volname}) {
my $cmd = ['/sbin/lvremove', '-f', "$vg/$volname"];
run_command($cmd, errmsg => "lvremove '$vg/$volname' error");
}
}
return undef;
}
sub list_images {
my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
my $vgname = $scfg->{vgname};
$cache->{lvs} = PVE::Storage::LVMPlugin::lvm_list_volumes() if !$cache->{lvs};
my $res = [];
if (my $dat = $cache->{lvs}->{$vgname}) {
foreach my $volname (keys %$dat) {
next if $volname !~ m/^(vm|base)-(\d+)-/;
my $owner = $2;
my $info = $dat->{$volname};
next if $info->{lv_type} ne 'V';
next if $info->{pool_lv} ne $scfg->{thinpool};
my $volid = "$storeid:$volname";
if ($vollist) {
my $found = grep { $_ eq $volid } @$vollist;
next if !$found;
} else {
next if defined($vmid) && ($owner ne $vmid);
}
push @$res, {
volid => $volid, format => 'raw', size => $info->{lv_size}, vmid => $owner,
ctime => $info->{ctime},
};
}
}
return $res;
}
sub list_thinpools {
my ($vg) = @_;
my $lvs = PVE::Storage::LVMPlugin::lvm_list_volumes($vg);
my $thinpools = [];
foreach my $vg (keys %$lvs) {
foreach my $lvname (keys %{$lvs->{$vg}}) {
next if $lvs->{$vg}->{$lvname}->{lv_type} ne 't';
my $lv = $lvs->{$vg}->{$lvname};
$lv->{lv} = $lvname;
$lv->{vg} = $vg;
push @$thinpools, $lv;
}
}
return $thinpools;
}
sub status {
my ($class, $storeid, $scfg, $cache) = @_;
my $lvs = $cache->{lvs} ||= PVE::Storage::LVMPlugin::lvm_list_volumes();
return if !$lvs->{$scfg->{vgname}};
my $info = $lvs->{$scfg->{vgname}}->{$scfg->{thinpool}};
return if !$info || $info->{lv_type} ne 't' || !$info->{lv_size};
return (
$info->{lv_size},
$info->{lv_size} - $info->{used},
$info->{used},
$info->{lv_state} eq 'a' ? 1 : 0,
);
}
my $activate_lv = sub {
my ($vg, $lv, $cache) = @_;
my $lvs = $cache->{lvs} ||= PVE::Storage::LVMPlugin::lvm_list_volumes();
die "no such logical volume $vg/$lv\n" if !$lvs->{$vg} || !$lvs->{$vg}->{$lv};
return if $lvs->{$vg}->{$lv}->{lv_state} eq 'a';
run_command(['lvchange', '-ay', '-K', "$vg/$lv"], errmsg => "activating LV '$vg/$lv' failed");
$lvs->{$vg}->{$lv}->{lv_state} = 'a'; # update cache
return;
};
sub activate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
$class->SUPER::activate_storage($storeid, $scfg, $cache);
$activate_lv->($scfg->{vgname}, $scfg->{thinpool}, $cache);
}
sub activate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
my $vg = $scfg->{vgname};
my $lv = $snapname ? "snap_${volname}_$snapname" : $volname;
$activate_lv->($vg, $lv, $cache);
}
sub deactivate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
return if !$snapname && $volname !~ /^base-/; # other volumes are kept active
my $vg = $scfg->{vgname};
my $lv = $snapname ? "snap_${volname}_$snapname" : $volname;
run_command(['lvchange', '-an', "$vg/$lv"], errmsg => "deactivate_volume '$vg/$lv' error");
$cache->{lvs}->{$vg}->{$lv}->{lv_state} = '-' # update cache
if $cache->{lvs} && $cache->{lvs}->{$vg} && $cache->{lvs}->{$vg}->{$lv};
return;
}
sub clone_image {
my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
my $vg = $scfg->{vgname};
my $lv;
if ($snap) {
$lv = "$vg/snap_${volname}_$snap";
} else {
my ($vtype, undef, undef, undef, undef, $isBase, $format) =
$class->parse_volname($volname);
die "clone_image only works on base images\n" if !$isBase;
$lv = "$vg/$volname";
}
my $name = $class->find_free_diskname($storeid, $scfg, $vmid);
my $cmd = ['/sbin/lvcreate', '-n', $name, '-prw', '-kn', '-s', $lv];
run_command($cmd, errmsg => "clone image '$lv' error");
return $name;
}
sub create_base {
my ($class, $storeid, $scfg, $volname) = @_;
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
$class->parse_volname($volname);
die "create_base not possible with base image\n" if $isBase;
my $vg = $scfg->{vgname};
my $lvs = PVE::Storage::LVMPlugin::lvm_list_volumes($vg);
if (my $dat = $lvs->{$vg}) {
# to avoid confusion, reject if we find volume snapshots
foreach my $lv (keys %$dat) {
die "unable to create base volume - found snaphost '$lv'\n"
if $lv =~ m/^snap_${volname}_(\w+)$/;
}
}
my $newname = $name;
$newname =~ s/^vm-/base-/;
my $cmd = ['/sbin/lvrename', $vg, $volname, $newname];
run_command($cmd, errmsg => "lvrename '$vg/$volname' => '$vg/$newname' error");
# set inactive, read-only and activationskip flags
$cmd = ['/sbin/lvchange', '-an', '-pr', '-ky', "$vg/$newname"];
eval { run_command($cmd); };
warn $@ if $@;
my $newvolname = $newname;
return $newvolname;
}
# sub volume_resize {} reuse code from parent class
sub volume_snapshot {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
my $vg = $scfg->{vgname};
my $snapvol = "snap_${volname}_$snap";
my $cmd = ['/sbin/lvcreate', '-n', $snapvol, '-pr', '-s', "$vg/$volname"];
run_command($cmd, errmsg => "lvcreate snapshot '$vg/$snapvol' error");
}
sub volume_snapshot_rollback {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
my $vg = $scfg->{vgname};
my $snapvol = "snap_${volname}_$snap";
my $cmd = ['/sbin/lvremove', '-f', "$vg/$volname"];
run_command($cmd, errmsg => "lvremove '$vg/$volname' error");
$cmd = ['/sbin/lvcreate', '-kn', '-n', $volname, '-s', "$vg/$snapvol"];
run_command($cmd, errmsg => "lvm rollback '$vg/$snapvol' error");
}
sub volume_snapshot_delete {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
my $vg = $scfg->{vgname};
my $snapvol = "snap_${volname}_$snap";
my $cmd = ['/sbin/lvremove', '-f', "$vg/$snapvol"];
run_command($cmd, errmsg => "lvremove snapshot '$vg/$snapvol' error");
}
sub volume_has_feature {
my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
my $features = {
snapshot => { current => 1 },
clone => { base => 1, snap => 1},
template => { current => 1},
copy => { base => 1, current => 1, snap => 1},
sparseinit => { base => 1, current => 1},
rename => {current => 1},
};
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
$class->parse_volname($volname);
my $key = undef;
if($snapname){
$key = 'snap';
}else{
$key = $isBase ? 'base' : 'current';
}
return 1 if $features->{$feature}->{$key};
return undef;
}
# used in LVMPlugin->volume_import
sub volume_import_write {
my ($class, $input_fh, $output_file) = @_;
run_command(['dd', "of=$output_file", 'conv=sparse', 'bs=64k'],
input => '<&'.fileno($input_fh));
}
1;

21
src/PVE/Storage/Makefile Normal file
View File

@ -0,0 +1,21 @@
SOURCES= \
Plugin.pm \
DirPlugin.pm \
LVMPlugin.pm \
NFSPlugin.pm \
CIFSPlugin.pm \
ISCSIPlugin.pm \
CephFSPlugin.pm \
RBDPlugin.pm \
ISCSIDirectPlugin.pm \
GlusterfsPlugin.pm \
ZFSPoolPlugin.pm \
ZFSPlugin.pm \
PBSPlugin.pm \
BTRFSPlugin.pm \
LvmThinPlugin.pm
.PHONY: install
install:
for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/Storage/$$i; done
make -C LunCmd install

View File

@ -0,0 +1,228 @@
package PVE::Storage::NFSPlugin;
use strict;
use warnings;
use IO::File;
use Net::IP;
use File::Path;
use PVE::Network;
use PVE::Tools qw(run_command);
use PVE::ProcFSTools;
use PVE::Storage::Plugin;
use PVE::JSONSchema qw(get_standard_option);
use base qw(PVE::Storage::Plugin);
# NFS helper functions
sub nfs_is_mounted {
my ($server, $export, $mountpoint, $mountdata) = @_;
$server = "[$server]" if Net::IP::ip_is_ipv6($server);
my $source = "$server:$export";
$mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata;
return $mountpoint if grep {
$_->[2] =~ /^nfs/ &&
$_->[0] =~ m|^\Q$source\E/?$| &&
$_->[1] eq $mountpoint
} @$mountdata;
return undef;
}
sub nfs_mount {
my ($server, $export, $mountpoint, $options) = @_;
$server = "[$server]" if Net::IP::ip_is_ipv6($server);
my $source = "$server:$export";
my $cmd = ['/bin/mount', '-t', 'nfs', $source, $mountpoint];
if ($options) {
push @$cmd, '-o', $options;
}
run_command($cmd, errmsg => "mount error");
}
# Configuration
sub type {
return 'nfs';
}
sub plugindata {
return {
content => [ { images => 1, rootdir => 1, vztmpl => 1, iso => 1, backup => 1, snippets => 1 },
{ images => 1 }],
format => [ { raw => 1, qcow2 => 1, vmdk => 1 } , 'raw' ],
};
}
sub properties {
return {
export => {
description => "NFS export path.",
type => 'string', format => 'pve-storage-path',
},
server => {
description => "Server IP or DNS name.",
type => 'string', format => 'pve-storage-server',
},
options => {
description => "NFS mount options (see 'man nfs')",
type => 'string', format => 'pve-storage-options',
},
};
}
sub options {
return {
path => { fixed => 1 },
'content-dirs' => { optional => 1 },
server => { fixed => 1 },
export => { fixed => 1 },
nodes => { optional => 1 },
disable => { optional => 1 },
maxfiles => { optional => 1 },
'prune-backups' => { optional => 1 },
'max-protected-backups' => { optional => 1 },
options => { optional => 1 },
content => { optional => 1 },
format => { optional => 1 },
mkdir => { optional => 1 },
bwlimit => { optional => 1 },
preallocation => { optional => 1 },
};
}
sub check_config {
my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
$config->{path} = "/mnt/pve/$sectionId" if $create && !$config->{path};
return $class->SUPER::check_config($sectionId, $config, $create, $skipSchemaCheck);
}
# Storage implementation
sub status {
my ($class, $storeid, $scfg, $cache) = @_;
$cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
if !$cache->{mountdata};
my $path = $scfg->{path};
my $server = $scfg->{server};
my $export = $scfg->{export};
return undef if !nfs_is_mounted($server, $export, $path, $cache->{mountdata});
return $class->SUPER::status($storeid, $scfg, $cache);
}
sub activate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
$cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
if !$cache->{mountdata};
my $path = $scfg->{path};
my $server = $scfg->{server};
my $export = $scfg->{export};
if (!nfs_is_mounted($server, $export, $path, $cache->{mountdata})) {
# NOTE: only call mkpath when not mounted (avoid hang when NFS server is offline
mkpath $path if !(defined($scfg->{mkdir}) && !$scfg->{mkdir});
die "unable to activate storage '$storeid' - " .
"directory '$path' does not exist\n" if ! -d $path;
nfs_mount($server, $export, $path, $scfg->{options});
}
$class->SUPER::activate_storage($storeid, $scfg, $cache);
}
sub deactivate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
$cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
if !$cache->{mountdata};
my $path = $scfg->{path};
my $server = $scfg->{server};
my $export = $scfg->{export};
if (nfs_is_mounted($server, $export, $path, $cache->{mountdata})) {
my $cmd = ['/bin/umount', $path];
run_command($cmd, errmsg => 'umount error');
}
}
sub check_connection {
my ($class, $storeid, $scfg) = @_;
my $server = $scfg->{server};
my $opts = $scfg->{options};
my $cmd;
my $is_v4 = defined($opts) && $opts =~ /vers=4.*/;
if ($is_v4) {
my $ip = PVE::JSONSchema::pve_verify_ip($server, 1);
if (!defined($ip)) {
$ip = PVE::Network::get_ip_from_hostname($server);
}
my $transport = PVE::JSONSchema::pve_verify_ipv4($ip, 1) ? 'tcp' : 'tcp6';
# nfsv4 uses a pseudo-filesystem always beginning with /
# no exports are listed
$cmd = ['/usr/sbin/rpcinfo', '-T', $transport, $ip, 'nfs', '4'];
} else {
$cmd = ['/sbin/showmount', '--no-headers', '--exports', $server];
}
eval { run_command($cmd, timeout => 10, outfunc => sub {}, errfunc => sub {}) };
if (my $err = $@) {
if ($is_v4) {
my $port = 2049;
$port = $1 if defined($opts) && $opts =~ /port=(\d+)/;
# rpcinfo is expected to work when the port is 0 (see 'man 5 nfs') and tcp_ping()
# defaults to port 7 when passing in 0.
return 0 if $port == 0;
return PVE::Network::tcp_ping($server, $port, 2);
}
return 0;
}
return 1;
}
# FIXME remove on the next APIAGE reset.
# Deprecated, use get_volume_attribute instead.
sub get_volume_notes {
my $class = shift;
PVE::Storage::DirPlugin::get_volume_notes($class, @_);
}
# FIXME remove on the next APIAGE reset.
# Deprecated, use update_volume_attribute instead.
sub update_volume_notes {
my $class = shift;
PVE::Storage::DirPlugin::update_volume_notes($class, @_);
}
sub get_volume_attribute {
return PVE::Storage::DirPlugin::get_volume_attribute(@_);
}
sub update_volume_attribute {
return PVE::Storage::DirPlugin::update_volume_attribute(@_);
}
1;

View File

@ -0,0 +1,981 @@
package PVE::Storage::PBSPlugin;
# Plugin to access Proxmox Backup Server
use strict;
use warnings;
use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
use IO::File;
use JSON;
use MIME::Base64 qw(decode_base64);
use POSIX qw(mktime strftime ENOENT);
use POSIX::strptime;
use PVE::APIClient::LWP;
use PVE::JSONSchema qw(get_standard_option);
use PVE::Network;
use PVE::PBSClient;
use PVE::Storage::Plugin;
use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach $IPV6RE);
use base qw(PVE::Storage::Plugin);
# Configuration
sub type {
return 'pbs';
}
sub plugindata {
return {
content => [ {backup => 1, none => 1}, { backup => 1 }],
};
}
sub properties {
return {
datastore => {
description => "Proxmox Backup Server datastore name.",
type => 'string',
},
# openssl s_client -connect <host>:8007 2>&1 |openssl x509 -fingerprint -sha256
fingerprint => get_standard_option('fingerprint-sha256'),
'encryption-key' => {
description => "Encryption key. Use 'autogen' to generate one automatically without passphrase.",
type => 'string',
},
'master-pubkey' => {
description => "Base64-encoded, PEM-formatted public RSA key. Used to encrypt a copy of the encryption-key which will be added to each encrypted backup.",
type => 'string',
},
port => {
description => "For non default port.",
type => 'integer',
minimum => 1,
maximum => 65535,
default => 8007,
},
};
}
sub options {
return {
server => { fixed => 1 },
datastore => { fixed => 1 },
namespace => { optional => 1 },
port => { optional => 1 },
nodes => { optional => 1},
disable => { optional => 1},
content => { optional => 1},
username => { optional => 1 },
password => { optional => 1 },
'encryption-key' => { optional => 1 },
'master-pubkey' => { optional => 1 },
maxfiles => { optional => 1 },
'prune-backups' => { optional => 1 },
'max-protected-backups' => { optional => 1 },
fingerprint => { optional => 1 },
};
}
# Helpers
sub pbs_password_file_name {
my ($scfg, $storeid) = @_;
return "/etc/pve/priv/storage/${storeid}.pw";
}
sub pbs_set_password {
my ($scfg, $storeid, $password) = @_;
my $pwfile = pbs_password_file_name($scfg, $storeid);
mkdir "/etc/pve/priv/storage";
PVE::Tools::file_set_contents($pwfile, "$password\n");
}
sub pbs_delete_password {
my ($scfg, $storeid) = @_;
my $pwfile = pbs_password_file_name($scfg, $storeid);
unlink $pwfile;
}
sub pbs_get_password {
my ($scfg, $storeid) = @_;
my $pwfile = pbs_password_file_name($scfg, $storeid);
return PVE::Tools::file_read_firstline($pwfile);
}
sub pbs_encryption_key_file_name {
my ($scfg, $storeid) = @_;
return "/etc/pve/priv/storage/${storeid}.enc";
}
sub pbs_set_encryption_key {
my ($scfg, $storeid, $key) = @_;
my $pwfile = pbs_encryption_key_file_name($scfg, $storeid);
mkdir "/etc/pve/priv/storage";
PVE::Tools::file_set_contents($pwfile, "$key\n");
}
sub pbs_delete_encryption_key {
my ($scfg, $storeid) = @_;
my $pwfile = pbs_encryption_key_file_name($scfg, $storeid);
if (!unlink $pwfile) {
return if $! == ENOENT;
die "failed to delete encryption key! $!\n";
}
delete $scfg->{'encryption-key'};
}
sub pbs_get_encryption_key {
my ($scfg, $storeid) = @_;
my $pwfile = pbs_encryption_key_file_name($scfg, $storeid);
return PVE::Tools::file_get_contents($pwfile);
}
# Returns a file handle if there is an encryption key, or `undef` if there is not. Dies on error.
sub pbs_open_encryption_key {
my ($scfg, $storeid) = @_;
my $encryption_key_file = pbs_encryption_key_file_name($scfg, $storeid);
my $keyfd;
if (!open($keyfd, '<', $encryption_key_file)) {
if ($! == ENOENT) {
my $encryption_fp = $scfg->{'encryption-key'};
die "encryption configured ('$encryption_fp') but no encryption key file found!\n"
if $encryption_fp;
return undef;
}
die "failed to open encryption key: $encryption_key_file: $!\n";
}
return $keyfd;
}
sub pbs_master_pubkey_file_name {
my ($scfg, $storeid) = @_;
return "/etc/pve/priv/storage/${storeid}.master.pem";
}
sub pbs_set_master_pubkey {
my ($scfg, $storeid, $key) = @_;
my $pwfile = pbs_master_pubkey_file_name($scfg, $storeid);
mkdir "/etc/pve/priv/storage";
PVE::Tools::file_set_contents($pwfile, "$key\n");
}
sub pbs_delete_master_pubkey {
my ($scfg, $storeid) = @_;
my $pwfile = pbs_master_pubkey_file_name($scfg, $storeid);
if (!unlink $pwfile) {
return if $! == ENOENT;
die "failed to delete master public key! $!\n";
}
delete $scfg->{'master-pubkey'};
}
sub pbs_get_master_pubkey {
my ($scfg, $storeid) = @_;
my $pwfile = pbs_master_pubkey_file_name($scfg, $storeid);
return PVE::Tools::file_get_contents($pwfile);
}
# Returns a file handle if there is a master key, or `undef` if there is not. Dies on error.
sub pbs_open_master_pubkey {
my ($scfg, $storeid) = @_;
my $master_pubkey_file = pbs_master_pubkey_file_name($scfg, $storeid);
my $keyfd;
if (!open($keyfd, '<', $master_pubkey_file)) {
if ($! == ENOENT) {
die "master public key configured but no key file found!\n"
if $scfg->{'master-pubkey'};
return undef;
}
die "failed to open master public key: $master_pubkey_file: $!\n";
}
return $keyfd;
}
sub print_volid {
my ($storeid, $btype, $bid, $btime) = @_;
my $time_str = strftime("%FT%TZ", gmtime($btime));
my $volname = "backup/${btype}/${bid}/${time_str}";
return "${storeid}:${volname}";
}
my sub ns : prototype($$) {
my ($scfg, $name) = @_;
my $ns = $scfg->{namespace};
return defined($ns) ? ($name, $ns) : ();
}
# essentially the inverse of print_volid
my sub api_param_from_volname : prototype($$$) {
my ($class, $scfg, $volname) = @_;
my $name = ($class->parse_volname($volname))[1];
my ($btype, $bid, $timestr) = split('/', $name);
my @tm = (POSIX::strptime($timestr, "%FT%TZ"));
# expect sec, min, hour, mday, mon, year
die "error parsing time from '$volname'" if grep { !defined($_) } @tm[0..5];
my $btime;
{
local $ENV{TZ} = 'UTC'; # $timestr is UTC
# Fill in isdst to avoid undef warning. No daylight saving time for UTC.
$tm[8] //= 0;
my $since_epoch = mktime(@tm) or die "error converting time from '$volname'\n";
$btime = int($since_epoch);
}
return {
(ns($scfg, 'ns')),
'backup-type' => $btype,
'backup-id' => $bid,
'backup-time' => $btime,
};
}
my $USE_CRYPT_PARAMS = {
backup => 1,
restore => 1,
'upload-log' => 1,
};
my $USE_MASTER_KEY = {
backup => 1,
};
my sub do_raw_client_cmd {
my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
my $use_crypto = $USE_CRYPT_PARAMS->{$client_cmd};
my $use_master = $USE_MASTER_KEY->{$client_cmd};
my $client_exe = '/usr/bin/proxmox-backup-client';
die "executable not found '$client_exe'! Proxmox backup client not installed?\n"
if ! -x $client_exe;
my $repo = PVE::PBSClient::get_repository($scfg);
my $userns_cmd = delete $opts{userns_cmd};
my $cmd = [];
push @$cmd, @$userns_cmd if defined($userns_cmd);
push @$cmd, $client_exe, $client_cmd;
# This must live in the top scope to not get closed before the `run_command`
my ($keyfd, $master_fd);
if ($use_crypto) {
if (defined($keyfd = pbs_open_encryption_key($scfg, $storeid))) {
my $flags = fcntl($keyfd, F_GETFD, 0)
// die "failed to get file descriptor flags: $!\n";
fcntl($keyfd, F_SETFD, $flags & ~FD_CLOEXEC)
or die "failed to remove FD_CLOEXEC from encryption key file descriptor\n";
push @$cmd, '--crypt-mode=encrypt', '--keyfd='.fileno($keyfd);
if ($use_master && defined($master_fd = pbs_open_master_pubkey($scfg, $storeid))) {
my $flags = fcntl($master_fd, F_GETFD, 0)
// die "failed to get file descriptor flags: $!\n";
fcntl($master_fd, F_SETFD, $flags & ~FD_CLOEXEC)
or die "failed to remove FD_CLOEXEC from master public key file descriptor\n";
push @$cmd, '--master-pubkey-fd='.fileno($master_fd);
}
} else {
push @$cmd, '--crypt-mode=none';
}
}
push @$cmd, @$param if defined($param);
push @$cmd, "--repository", $repo;
if ($client_cmd ne 'status' && defined(my $ns = $scfg->{namespace})) {
push @$cmd, '--ns', $ns;
}
local $ENV{PBS_PASSWORD} = pbs_get_password($scfg, $storeid);
local $ENV{PBS_FINGERPRINT} = $scfg->{fingerprint};
# no ascii-art on task logs
local $ENV{PROXMOX_OUTPUT_NO_BORDER} = 1;
local $ENV{PROXMOX_OUTPUT_NO_HEADER} = 1;
if (my $logfunc = $opts{logfunc}) {
$logfunc->("run: " . join(' ', @$cmd));
}
run_command($cmd, %opts);
}
# FIXME: External perl code should NOT have access to this.
#
# There should be separate functions to
# - make backups
# - restore backups
# - restore files
# with a sane API
sub run_raw_client_cmd {
my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
return do_raw_client_cmd($scfg, $storeid, $client_cmd, $param, %opts);
}
sub run_client_cmd {
my ($scfg, $storeid, $client_cmd, $param, $no_output) = @_;
my $json_str = '';
my $outfunc = sub { $json_str .= "$_[0]\n" };
$param = [] if !defined($param);
$param = [ $param ] if !ref($param);
$param = [@$param, '--output-format=json'] if !$no_output;
do_raw_client_cmd($scfg, $storeid, $client_cmd, $param,
outfunc => $outfunc, errmsg => 'proxmox-backup-client failed');
return undef if $no_output;
my $res = decode_json($json_str);
return $res;
}
# Storage implementation
sub extract_vzdump_config {
my ($class, $scfg, $volname, $storeid) = @_;
my ($vtype, $name, $vmid, undef, undef, undef, $format) = $class->parse_volname($volname);
my $config = '';
my $outfunc = sub { $config .= "$_[0]\n" };
my $config_name;
if ($format eq 'pbs-vm') {
$config_name = 'qemu-server.conf';
} elsif ($format eq 'pbs-ct') {
$config_name = 'pct.conf';
} else {
die "unable to extract configuration for backup format '$format'\n";
}
do_raw_client_cmd($scfg, $storeid, 'restore', [ $name, $config_name, '-' ],
outfunc => $outfunc, errmsg => 'proxmox-backup-client failed');
return $config;
}
sub prune_backups {
my ($class, $scfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc) = @_;
$logfunc //= sub { print "$_[1]\n" };
$type = 'vm' if defined($type) && $type eq 'qemu';
$type = 'ct' if defined($type) && $type eq 'lxc';
my $backup_groups = {};
if (defined($vmid) && defined($type)) {
# no need to get the list of volumes, we only got a single backup group anyway
$backup_groups->{"$type/$vmid"} = 1;
} else {
my $backups = eval { $class->list_volumes($storeid, $scfg, $vmid, ['backup']) };
die "failed to get list of all backups to prune - $@" if $@;
foreach my $backup (@{$backups}) {
(my $backup_type = $backup->{format}) =~ s/^pbs-//;
next if defined($type) && $backup_type ne $type;
my $backup_group = "$backup_type/$backup->{vmid}";
$backup_groups->{$backup_group} = 1;
}
}
my @param;
my $keep_all = delete $keep->{'keep-all'};
if (!$keep_all) {
foreach my $opt (keys %{$keep}) {
next if $keep->{$opt} == 0;
push @param, "--$opt";
push @param, "$keep->{$opt}";
}
} else { # no need to pass anything to PBS
$keep = { 'keep-all' => 1 };
}
push @param, '--dry-run' if $dryrun;
my $prune_list = [];
my $failed;
foreach my $backup_group (keys %{$backup_groups}) {
$logfunc->('info', "running 'proxmox-backup-client prune' for '$backup_group'")
if !$dryrun;
eval {
my $res = run_client_cmd($scfg, $storeid, 'prune', [ $backup_group, @param ]);
foreach my $backup (@{$res}) {
die "result from proxmox-backup-client is not as expected\n"
if !defined($backup->{'backup-time'})
|| !defined($backup->{'backup-type'})
|| !defined($backup->{'backup-id'})
|| !defined($backup->{'keep'});
my $ctime = $backup->{'backup-time'};
my $type = $backup->{'backup-type'};
my $vmid = $backup->{'backup-id'};
my $volid = print_volid($storeid, $type, $vmid, $ctime);
my $mark = $backup->{keep} ? 'keep' : 'remove';
$mark = 'protected' if $backup->{protected};
push @{$prune_list}, {
ctime => $ctime,
mark => $mark,
type => $type eq 'vm' ? 'qemu' : 'lxc',
vmid => $vmid,
volid => $volid,
};
}
};
if (my $err = $@) {
$logfunc->('err', "prune '$backup_group': $err\n");
$failed = 1;
}
}
die "error pruning backups - check log\n" if $failed;
return $prune_list;
}
my $autogen_encryption_key = sub {
my ($scfg, $storeid) = @_;
my $encfile = pbs_encryption_key_file_name($scfg, $storeid);
if (-f $encfile) {
rename $encfile, "$encfile.old";
}
my $cmd = ['proxmox-backup-client', 'key', 'create', '--kdf', 'none', $encfile];
run_command($cmd, errmsg => 'failed to create encryption key');
return PVE::Tools::file_get_contents($encfile);
};
sub on_add_hook {
my ($class, $storeid, $scfg, %param) = @_;
my $res = {};
if (defined(my $password = $param{password})) {
pbs_set_password($scfg, $storeid, $password);
} else {
pbs_delete_password($scfg, $storeid);
}
if (defined(my $encryption_key = $param{'encryption-key'})) {
my $decoded_key;
if ($encryption_key eq 'autogen') {
$res->{'encryption-key'} = $autogen_encryption_key->($scfg, $storeid);
$decoded_key = decode_json($res->{'encryption-key'});
} else {
$decoded_key = eval { decode_json($encryption_key) };
if ($@ || !exists($decoded_key->{data})) {
die "Value does not seems like a valid, JSON formatted encryption key!\n";
}
pbs_set_encryption_key($scfg, $storeid, $encryption_key);
$res->{'encryption-key'} = $encryption_key;
}
$scfg->{'encryption-key'} = $decoded_key->{fingerprint} || 1;
} else {
pbs_delete_encryption_key($scfg, $storeid);
}
if (defined(my $master_key = delete $param{'master-pubkey'})) {
die "'master-pubkey' can only be used together with 'encryption-key'\n"
if !defined($scfg->{'encryption-key'});
my $decoded = decode_base64($master_key);
pbs_set_master_pubkey($scfg, $storeid, $decoded);
$scfg->{'master-pubkey'} = 1;
} else {
pbs_delete_master_pubkey($scfg, $storeid);
}
return $res;
}
sub on_update_hook {
my ($class, $storeid, $scfg, %param) = @_;
my $res = {};
if (exists($param{password})) {
if (defined($param{password})) {
pbs_set_password($scfg, $storeid, $param{password});
} else {
pbs_delete_password($scfg, $storeid);
}
}
if (exists($param{'encryption-key'})) {
if (defined(my $encryption_key = delete($param{'encryption-key'}))) {
my $decoded_key;
if ($encryption_key eq 'autogen') {
$res->{'encryption-key'} = $autogen_encryption_key->($scfg, $storeid);
$decoded_key = decode_json($res->{'encryption-key'});
} else {
$decoded_key = eval { decode_json($encryption_key) };
if ($@ || !exists($decoded_key->{data})) {
die "Value does not seems like a valid, JSON formatted encryption key!\n";
}
pbs_set_encryption_key($scfg, $storeid, $encryption_key);
$res->{'encryption-key'} = $encryption_key;
}
$scfg->{'encryption-key'} = $decoded_key->{fingerprint} || 1;
} else {
pbs_delete_encryption_key($scfg, $storeid);
delete $scfg->{'encryption-key'};
}
}
if (exists($param{'master-pubkey'})) {
if (defined(my $master_key = delete($param{'master-pubkey'}))) {
my $decoded = decode_base64($master_key);
pbs_set_master_pubkey($scfg, $storeid, $decoded);
$scfg->{'master-pubkey'} = 1;
} else {
pbs_delete_master_pubkey($scfg, $storeid);
}
}
return $res;
}
sub on_delete_hook {
my ($class, $storeid, $scfg) = @_;
pbs_delete_password($scfg, $storeid);
pbs_delete_encryption_key($scfg, $storeid);
pbs_delete_master_pubkey($scfg, $storeid);
return;
}
sub parse_volname {
my ($class, $volname) = @_;
if ($volname =~ m!^backup/([^\s_]+)/([^\s_]+)/([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z)$!) {
my $btype = $1;
my $bid = $2;
my $btime = $3;
my $format = "pbs-$btype";
my $name = "$btype/$bid/$btime";
if ($bid =~ m/^\d+$/) {
return ('backup', $name, $bid, undef, undef, undef, $format);
} else {
return ('backup', $name, undef, undef, undef, undef, $format);
}
}
die "unable to parse PBS volume name '$volname'\n";
}
sub path {
my ($class, $scfg, $volname, $storeid, $snapname) = @_;
die "volume snapshot is not possible on pbs storage"
if defined($snapname);
my ($vtype, $name, $vmid) = $class->parse_volname($volname);
my $repo = PVE::PBSClient::get_repository($scfg);
# artificial url - we currently do not use that anywhere
my $path = "pbs://$repo/$name";
if (defined(my $ns = $scfg->{namespace})) {
$ns =~ s|/|%2f|g; # other characters to escape aren't allowed in the namespace schema
$path .= "?ns=$ns";
}
return ($path, $vmid, $vtype);
}
sub create_base {
my ($class, $storeid, $scfg, $volname) = @_;
die "can't create base images in pbs storage\n";
}
sub clone_image {
my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
die "can't clone images in pbs storage\n";
}
sub alloc_image {
my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
die "can't allocate space in pbs storage\n";
}
sub free_image {
my ($class, $storeid, $scfg, $volname, $isBase) = @_;
my ($vtype, $name, $vmid) = $class->parse_volname($volname);
run_client_cmd($scfg, $storeid, "forget", [ $name ], 1);
return;
}
sub list_images {
my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
my $res = [];
return $res;
}
my sub snapshot_files_encrypted {
my ($files) = @_;
return 0 if !$files;
my $any;
my $all = 1;
for my $file (@$files) {
my $fn = $file->{filename};
next if $fn eq 'client.log.blob' || $fn eq 'index.json.blob';
my $crypt = $file->{'crypt-mode'};
$all = 0 if !$crypt || $crypt ne 'encrypt';
$any ||= defined($crypt) && $crypt eq 'encrypt';
}
return $any && $all;
}
# TODO: use a client with native rust/proxmox-backup bindings to profit from
# API schema checks and types
my sub pbs_api_connect {
my ($scfg, $password, $timeout) = @_;
my $params = {};
my $user = $scfg->{username} // 'root@pam';
if (my $tokenid = PVE::AccessControl::pve_verify_tokenid($user, 1)) {
$params->{apitoken} = "PBSAPIToken=${tokenid}:${password}";
} else {
$params->{password} = $password;
$params->{username} = $user;
}
if (my $fp = $scfg->{fingerprint}) {
$params->{cached_fingerprints}->{uc($fp)} = 1;
}
my $conn = PVE::APIClient::LWP->new(
%$params,
host => $scfg->{server},
port => $scfg->{port} // 8007,
timeout => ($timeout // 7), # cope with a 401 (3s api delay) and high latency
cookie_name => 'PBSAuthCookie',
);
return $conn;
}
sub list_volumes {
my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
my $res = [];
return $res if !grep { $_ eq 'backup' } @$content_types;
my $password = pbs_get_password($scfg, $storeid);
my $conn = pbs_api_connect($scfg, $password, 120);
my $datastore = $scfg->{datastore};
my $param = {};
$param->{'backup-id'} = "$vmid" if defined($vmid);
$param->{'ns'} = "$scfg->{namespace}" if defined($scfg->{namespace});
my $data = eval { $conn->get("/api2/json/admin/datastore/$datastore/snapshots", $param); };
die "error listing snapshots - $@" if $@;
foreach my $item (@$data) {
my $btype = $item->{"backup-type"};
my $bid = $item->{"backup-id"};
my $epoch = $item->{"backup-time"};
my $size = $item->{size} // 1;
next if !($btype eq 'vm' || $btype eq 'ct');
next if $bid !~ m/^\d+$/;
next if defined($vmid) && $bid ne $vmid;
my $volid = print_volid($storeid, $btype, $bid, $epoch);
my $info = {
volid => $volid,
format => "pbs-$btype",
size => $size,
content => 'backup',
vmid => int($bid),
ctime => $epoch,
subtype => $btype eq 'vm' ? 'qemu' : 'lxc', # convert to PVE backup type
};
$info->{verification} = $item->{verification} if defined($item->{verification});
$info->{notes} = $item->{comment} if defined($item->{comment});
$info->{protected} = 1 if $item->{protected};
if (defined($item->{fingerprint})) {
$info->{encrypted} = $item->{fingerprint};
} elsif (snapshot_files_encrypted($item->{files})) {
$info->{encrypted} = '1';
}
push @$res, $info;
}
return $res;
}
sub status {
my ($class, $storeid, $scfg, $cache) = @_;
my $total = 0;
my $free = 0;
my $used = 0;
my $active = 0;
eval {
my $res = run_client_cmd($scfg, $storeid, "status");
$active = 1;
$total = $res->{total};
$used = $res->{used};
$free = $res->{avail};
};
if (my $err = $@) {
warn $err;
}
return ($total, $free, $used, $active);
}
# can also be used for not (yet) added storages, pass $scfg with
# {
# server
# user
# port (optional default to 8007)
# fingerprint (optional for trusted certs)
# }
sub scan_datastores {
my ($scfg, $password) = @_;
my $conn = pbs_api_connect($scfg, $password);
my $response = eval { $conn->get('/api2/json/admin/datastore', {}) };
die "error fetching datastores - $@" if $@;
return $response;
}
sub activate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
my $password = pbs_get_password($scfg, $storeid);
my $datastores = eval { scan_datastores($scfg, $password) };
die "$storeid: $@" if $@;
my $datastore = $scfg->{datastore};
for my $ds (@$datastores) {
if ($ds->{store} eq $datastore) {
return 1;
}
}
die "$storeid: Cannot find datastore '$datastore', check permissions and existence!\n";
}
sub deactivate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
return 1;
}
sub activate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
die "volume snapshot is not possible on pbs device" if $snapname;
return 1;
}
sub deactivate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
die "volume snapshot is not possible on pbs device" if $snapname;
return 1;
}
# FIXME remove on the next APIAGE reset.
# Deprecated, use get_volume_attribute instead.
sub get_volume_notes {
my ($class, $scfg, $storeid, $volname, $timeout) = @_;
my (undef, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
my $data = run_client_cmd($scfg, $storeid, "snapshot", [ "notes", "show", $name ]);
return $data->{notes};
}
# FIXME remove on the next APIAGE reset.
# Deprecated, use update_volume_attribute instead.
sub update_volume_notes {
my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_;
my (undef, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
run_client_cmd($scfg, $storeid, "snapshot", [ "notes", "update", $name, $notes ], 1);
return undef;
}
sub get_volume_attribute {
my ($class, $scfg, $storeid, $volname, $attribute) = @_;
if ($attribute eq 'notes') {
return $class->get_volume_notes($scfg, $storeid, $volname);
}
if ($attribute eq 'protected') {
my $param = api_param_from_volname($class, $scfg, $volname);
my $password = pbs_get_password($scfg, $storeid);
my $conn = pbs_api_connect($scfg, $password);
my $datastore = $scfg->{datastore};
my $res = eval { $conn->get("/api2/json/admin/datastore/$datastore/$attribute", $param); };
if (my $err = $@) {
return if $err->{code} == 404; # not supported
die $err;
}
return $res;
}
return;
}
sub update_volume_attribute {
my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_;
if ($attribute eq 'notes') {
return $class->update_volume_notes($scfg, $storeid, $volname, $value);
}
if ($attribute eq 'protected') {
my $param = api_param_from_volname($class, $scfg, $volname);
$param->{$attribute} = $value;
my $password = pbs_get_password($scfg, $storeid);
my $conn = pbs_api_connect($scfg, $password);
my $datastore = $scfg->{datastore};
eval { $conn->put("/api2/json/admin/datastore/$datastore/$attribute", $param); };
if (my $err = $@) {
die "Server is not recent enough to support feature '$attribute'\n"
if $err->{code} == 404;
die $err;
}
return;
}
die "attribute '$attribute' is not supported for storage type '$scfg->{type}'\n";
}
sub volume_size_info {
my ($class, $scfg, $storeid, $volname, $timeout) = @_;
my ($vtype, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
my $data = run_client_cmd($scfg, $storeid, "files", [ $name ]);
my $size = 0;
foreach my $info (@$data) {
if ($info->{size} && $info->{size} =~ /^(\d+)$/) { # untaints
$size += $1;
}
}
my $used = $size;
return wantarray ? ($size, $format, $used, undef) : $size;
}
sub volume_resize {
my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
die "volume resize is not possible on pbs device";
}
sub volume_snapshot {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
die "volume snapshot is not possible on pbs device";
}
sub volume_snapshot_rollback {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
die "volume snapshot rollback is not possible on pbs device";
}
sub volume_snapshot_delete {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
die "volume snapshot delete is not possible on pbs device";
}
sub volume_has_feature {
my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
return undef;
}
1;

1701
src/PVE/Storage/Plugin.pm Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,891 @@
package PVE::Storage::RBDPlugin;
use strict;
use warnings;
use Cwd qw(abs_path);
use IO::File;
use JSON;
use Net::IP;
use PVE::CephConfig;
use PVE::Cluster qw(cfs_read_file);;
use PVE::JSONSchema qw(get_standard_option);
use PVE::ProcFSTools;
use PVE::RADOS;
use PVE::RPCEnvironment;
use PVE::Storage::Plugin;
use PVE::Tools qw(run_command trim file_read_firstline);
use base qw(PVE::Storage::Plugin);
my $get_parent_image_name = sub {
my ($parent) = @_;
return undef if !$parent;
return $parent->{image} . "@" . $parent->{snapshot};
};
my $librados_connect = sub {
my ($scfg, $storeid, $options) = @_;
$options->{timeout} = 60
if !defined($options->{timeout}) && PVE::RPCEnvironment->is_worker();
my $librados_config = PVE::CephConfig::ceph_connect_option($scfg, $storeid, $options->%*);
my $rados = PVE::RADOS->new(%$librados_config);
return $rados;
};
my sub get_rbd_path {
my ($scfg, $volume) = @_;
my $path = $scfg->{pool} ? $scfg->{pool} : 'rbd';
$path .= "/$scfg->{namespace}" if defined($scfg->{namespace});
$path .= "/$volume" if defined($volume);
return $path;
};
my sub get_rbd_dev_path {
my ($scfg, $storeid, $volume) = @_;
my $cluster_id = '';
if ($scfg->{fsid}) {
# NOTE: the config doesn't support this currently (but it could!), hack for qemu-server tests
$cluster_id = $scfg->{fsid};
} elsif ($scfg->{monhost}) {
my $rados = $librados_connect->($scfg, $storeid);
$cluster_id = $rados->mon_command({ prefix => 'fsid', format => 'json' })->{fsid};
} else {
$cluster_id = cfs_read_file('ceph.conf')->{global}->{fsid};
}
my $uuid_pattern = "([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})";
if ($cluster_id =~ qr/^${uuid_pattern}$/is) {
$cluster_id = $1; # use untained value
} else {
die "cluster fsid has invalid format\n";
}
my $rbd_path = get_rbd_path($scfg, $volume);
my $pve_path = "/dev/rbd-pve/${cluster_id}/${rbd_path}";
my $path = "/dev/rbd/${rbd_path}";
if (!-e $pve_path && -e $path) {
# possibly mapped before rbd-pve rule existed
my $real_dev = abs_path($path);
my ($rbd_id) = ($real_dev =~ m|/dev/rbd([0-9]+)$|);
my $dev_cluster_id = file_read_firstline("/sys/devices/rbd/${rbd_id}/cluster_fsid");
return $path if $cluster_id eq $dev_cluster_id;
}
return $pve_path;
}
my $build_cmd = sub {
my ($binary, $scfg, $storeid, $op, @options) = @_;
my $cmd_option = PVE::CephConfig::ceph_connect_option($scfg, $storeid);
my $pool = $scfg->{pool} ? $scfg->{pool} : 'rbd';
my $cmd = [$binary, '-p', $pool];
if (defined(my $namespace = $scfg->{namespace})) {
# some subcommands will fail if the --namespace parameter is present
my $no_namespace_parameter = {
unmap => 1,
};
push @$cmd, '--namespace', "$namespace" if !$no_namespace_parameter->{$op};
}
push @$cmd, '-c', $cmd_option->{ceph_conf} if ($cmd_option->{ceph_conf});
push @$cmd, '-m', $cmd_option->{mon_host} if ($cmd_option->{mon_host});
push @$cmd, '--auth_supported', $cmd_option->{auth_supported} if ($cmd_option->{auth_supported});
push @$cmd, '-n', "client.$cmd_option->{userid}" if ($cmd_option->{userid});
push @$cmd, '--keyring', $cmd_option->{keyring} if ($cmd_option->{keyring});
push @$cmd, $op;
push @$cmd, @options if scalar(@options);
return $cmd;
};
my $rbd_cmd = sub {
my ($scfg, $storeid, $op, @options) = @_;
return $build_cmd->('/usr/bin/rbd', $scfg, $storeid, $op, @options);
};
my $rados_cmd = sub {
my ($scfg, $storeid, $op, @options) = @_;
return $build_cmd->('/usr/bin/rados', $scfg, $storeid, $op, @options);
};
# needed for volumes created using ceph jewel (or higher)
my $krbd_feature_update = sub {
my ($scfg, $storeid, $name) = @_;
my (@disable, @enable);
my ($kmajor, $kminor) = PVE::ProcFSTools::kernel_version();
if ($kmajor > 5 || $kmajor == 5 && $kminor >= 3) {
# 'deep-flatten' can only be disabled, not enabled after image creation
push @enable, 'fast-diff', 'object-map';
} else {
push @disable, 'fast-diff', 'object-map', 'deep-flatten';
}
if ($kmajor >= 5) {
push @enable, 'exclusive-lock';
} else {
push @disable, 'exclusive-lock';
}
my $active_features_list = (rbd_volume_info($scfg, $storeid, $name))[4];
my $active_features = { map { $_ => 1 } @$active_features_list };
my $to_disable = join(',', grep { $active_features->{$_} } @disable);
my $to_enable = join(',', grep { !$active_features->{$_} } @enable );
if ($to_disable) {
print "disable RBD image features this kernel RBD drivers is not compatible with: $to_disable\n";
my $cmd = $rbd_cmd->($scfg, $storeid, 'feature', 'disable', $name, $to_disable);
run_rbd_command(
$cmd,
errmsg => "could not disable krbd-incompatible image features '$to_disable' for rbd image: $name",
);
}
if ($to_enable) {
print "enable RBD image features this kernel RBD drivers supports: $to_enable\n";
eval {
my $cmd = $rbd_cmd->($scfg, $storeid, 'feature', 'enable', $name, $to_enable);
run_rbd_command(
$cmd,
errmsg => "could not enable krbd-compatible image features '$to_enable' for rbd image: $name",
);
};
warn "$@" if $@;
}
};
sub run_rbd_command {
my ($cmd, %args) = @_;
my $lasterr;
my $errmsg = $args{errmsg} . ": " || "";
if (!exists($args{errfunc})) {
# ' error: 2014-02-06 11:51:59.839135 7f09f94d0760 -1 librbd: snap_unprotect: can't unprotect;
# at least 1 child(ren) in pool cephstor1
$args{errfunc} = sub {
my $line = shift;
if ($line =~ m/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [0-9a-f]+ [\-\d]+ librbd: (.*)$/) {
$lasterr = "$1\n";
} else {
$lasterr = $line;
}
print STDERR $lasterr;
*STDERR->flush();
};
}
eval { run_command($cmd, %args); };
if (my $err = $@) {
die $errmsg . $lasterr if length($lasterr);
die $err;
}
return undef;
}
sub rbd_ls {
my ($scfg, $storeid) = @_;
my $pool = $scfg->{pool} ? $scfg->{pool} : 'rbd';
$pool .= "/$scfg->{namespace}" if defined($scfg->{namespace});
my $raw = '';
my $parser = sub { $raw .= shift };
my $cmd = $rbd_cmd->($scfg, $storeid, 'ls', '-l', '--format', 'json');
eval {
run_rbd_command($cmd, errmsg => "rbd error", errfunc => sub {}, outfunc => $parser);
};
my $err = $@;
die $err if $err && $err !~ m/doesn't contain rbd images/ ;
my $result;
if ($raw eq '') {
$result = [];
} elsif ($raw =~ m/^(\[.*\])$/s) { # untaint
$result = JSON::decode_json($1);
} else {
die "got unexpected data from rbd ls: '$raw'\n";
}
my $list = {};
foreach my $el (@$result) {
next if defined($el->{snapshot});
my $image = $el->{image};
my ($owner) = $image =~ m/^(?:vm|base)-(\d+)-/;
next if !defined($owner);
$list->{$pool}->{$image} = {
name => $image,
size => $el->{size},
parent => $get_parent_image_name->($el->{parent}),
vmid => $owner
};
}
return $list;
}
sub rbd_ls_snap {
my ($scfg, $storeid, $name) = @_;
my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'ls', $name, '--format', 'json');
my $raw = '';
run_rbd_command($cmd, errmsg => "rbd error", errfunc => sub {}, outfunc => sub { $raw .= shift; });
my $list;
if ($raw =~ m/^(\[.*\])$/s) { # untaint
$list = eval { JSON::decode_json($1) };
die "invalid JSON output from 'rbd snap ls $name': $@\n" if $@;
} else {
die "got unexpected data from 'rbd snap ls $name': '$raw'\n";
}
$list = [] if !defined($list);
my $res = {};
foreach my $el (@$list) {
my $snap = $el->{name};
my $protected = defined($el->{protected}) && $el->{protected} eq "true" ? 1 : undef;
$res->{$snap} = {
name => $snap,
id => $el->{id} // undef,
size => $el->{size} // 0,
protected => $protected,
};
}
return $res;
}
sub rbd_volume_info {
my ($scfg, $storeid, $volname, $snap) = @_;
my $cmd = undef;
my @options = ('info', $volname, '--format', 'json');
if ($snap) {
push @options, '--snap', $snap;
}
$cmd = $rbd_cmd->($scfg, $storeid, @options);
my $raw = '';
my $parser = sub { $raw .= shift };
run_rbd_command($cmd, errmsg => "rbd error", errfunc => sub {}, outfunc => $parser);
my $volume;
if ($raw eq '') {
$volume = {};
} elsif ($raw =~ m/^(\{.*\})$/s) { # untaint
$volume = JSON::decode_json($1);
} else {
die "got unexpected data from rbd info: '$raw'\n";
}
$volume->{parent} = $get_parent_image_name->($volume->{parent});
$volume->{protected} = defined($volume->{protected}) && $volume->{protected} eq "true" ? 1 : undef;
return $volume->@{qw(size parent format protected features)};
}
sub rbd_volume_du {
my ($scfg, $storeid, $volname) = @_;
my @options = ('du', $volname, '--format', 'json');
my $cmd = $rbd_cmd->($scfg, $storeid, @options);
my $raw = '';
my $parser = sub { $raw .= shift };
run_rbd_command($cmd, errmsg => "rbd error", errfunc => sub {}, outfunc => $parser);
my $volume;
if ($raw eq '') {
$volume = {};
} elsif ($raw =~ m/^(\{.*\})$/s) { # untaint
$volume = JSON::decode_json($1);
} else {
die "got unexpected data from rbd du: '$raw'\n";
}
if (!defined($volume->{images})) {
die "got no images from rbd du\n";
}
# `rbd du` returns array of images for name matching `volname`,
# including snapshots.
my $images = $volume->{images};
foreach my $image (@$images) {
next if defined($image->{snapshot});
next if !defined($image->{used_size}) || !defined($image->{name});
# Return `used_size` of first volume with matching name which
# is not a snapshot.
return $image->{used_size} if $image->{name} eq $volname;
}
die "got no matching image from rbd du\n";
}
# Configuration
sub type {
return 'rbd';
}
sub plugindata {
return {
content => [ {images => 1, rootdir => 1}, { images => 1 }],
};
}
sub properties {
return {
monhost => {
description => "IP addresses of monitors (for external clusters).",
type => 'string', format => 'pve-storage-portal-dns-list',
},
pool => {
description => "Pool.",
type => 'string',
},
'data-pool' => {
description => "Data Pool (for erasure coding only)",
type => 'string',
},
namespace => {
description => "Namespace.",
type => 'string',
},
username => {
description => "RBD Id.",
type => 'string',
},
authsupported => {
description => "Authsupported.",
type => 'string',
},
krbd => {
description => "Always access rbd through krbd kernel module.",
type => 'boolean',
},
keyring => {
description => "Client keyring contents (for external clusters).",
type => 'string',
},
};
}
sub options {
return {
nodes => { optional => 1 },
disable => { optional => 1 },
monhost => { optional => 1},
pool => { optional => 1 },
'data-pool' => { optional => 1 },
namespace => { optional => 1 },
username => { optional => 1 },
content => { optional => 1 },
krbd => { optional => 1 },
keyring => { optional => 1 },
bwlimit => { optional => 1 },
};
}
# Storage implementation
sub on_add_hook {
my ($class, $storeid, $scfg, %param) = @_;
PVE::CephConfig::ceph_create_keyfile($scfg->{type}, $storeid, $param{keyring});
return;
}
sub on_update_hook {
my ($class, $storeid, $scfg, %param) = @_;
if (exists($param{keyring})) {
if (defined($param{keyring})) {
PVE::CephConfig::ceph_create_keyfile($scfg->{type}, $storeid, $param{keyring});
} else {
PVE::CephConfig::ceph_remove_keyfile($scfg->{type}, $storeid);
}
}
return;
}
sub on_delete_hook {
my ($class, $storeid, $scfg) = @_;
PVE::CephConfig::ceph_remove_keyfile($scfg->{type}, $storeid);
return;
}
sub parse_volname {
my ($class, $volname) = @_;
if ($volname =~ m/^((base-(\d+)-\S+)\/)?((base)?(vm)?-(\d+)-\S+)$/) {
return ('images', $4, $7, $2, $3, $5, 'raw');
}
die "unable to parse rbd volume name '$volname'\n";
}
sub path {
my ($class, $scfg, $volname, $storeid, $snapname) = @_;
my $cmd_option = PVE::CephConfig::ceph_connect_option($scfg, $storeid);
my ($vtype, $name, $vmid) = $class->parse_volname($volname);
$name .= '@'.$snapname if $snapname;
if ($scfg->{krbd}) {
my $rbd_dev_path = get_rbd_dev_path($scfg, $storeid, $name);
return ($rbd_dev_path, $vmid, $vtype);
}
my $rbd_path = get_rbd_path($scfg, $name);
my $path = "rbd:${rbd_path}";
$path .= ":conf=$cmd_option->{ceph_conf}" if $cmd_option->{ceph_conf};
if (defined($scfg->{monhost})) {
my $monhost = PVE::CephConfig::hostlist($scfg->{monhost}, ';');
$monhost =~ s/:/\\:/g;
$path .= ":mon_host=$monhost";
$path .= ":auth_supported=$cmd_option->{auth_supported}";
}
$path .= ":id=$cmd_option->{userid}:keyring=$cmd_option->{keyring}" if ($cmd_option->{keyring});
return ($path, $vmid, $vtype);
}
sub find_free_diskname {
my ($class, $storeid, $scfg, $vmid, $fmt, $add_fmt_suffix) = @_;
my $cmd = $rbd_cmd->($scfg, $storeid, 'ls');
my $disk_list = [];
my $parser = sub {
my $line = shift;
if ($line =~ m/^(.*)$/) { # untaint
push @$disk_list, $1;
}
};
eval {
run_rbd_command($cmd, errmsg => "rbd error", errfunc => sub {}, outfunc => $parser);
};
my $err = $@;
die $err if $err && $err !~ m/doesn't contain rbd images/;
return PVE::Storage::Plugin::get_next_vm_diskname($disk_list, $storeid, $vmid, undef, $scfg);
}
sub create_base {
my ($class, $storeid, $scfg, $volname) = @_;
my $snap = '__base__';
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
$class->parse_volname($volname);
die "create_base not possible with base image\n" if $isBase;
my ($size, $parent, $format, undef) = rbd_volume_info($scfg, $storeid, $name);
die "rbd volume info on '$name' failed\n" if !($size);
die "rbd image must be at format V2" if $format ne "2";
die "volname '$volname' contains wrong information about parent $parent $basename\n"
if $basename && (!$parent || $parent ne $basename."@".$snap);
my $newname = $name;
$newname =~ s/^vm-/base-/;
my $newvolname = $basename ? "$basename/$newname" : "$newname";
my $cmd = $rbd_cmd->(
$scfg,
$storeid,
'rename',
get_rbd_path($scfg, $name),
get_rbd_path($scfg, $newname),
);
run_rbd_command($cmd, errmsg => "rbd rename '$name' error");
eval { $class->unmap_volume($storeid, $scfg, $volname); };
warn $@ if $@;
my $running = undef; #fixme : is create_base always offline ?
$class->volume_snapshot($scfg, $storeid, $newname, $snap, $running);
my (undef, undef, undef, $protected) = rbd_volume_info($scfg, $storeid, $newname, $snap);
if (!$protected){
my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'protect', $newname, '--snap', $snap);
run_rbd_command($cmd, errmsg => "rbd protect $newname snap '$snap' error");
}
return $newvolname;
}
sub clone_image {
my ($class, $scfg, $storeid, $volname, $vmid, $snapname) = @_;
my $snap = '__base__';
$snap = $snapname if length $snapname;
my ($vtype, $basename, $basevmid, undef, undef, $isBase) =
$class->parse_volname($volname);
die "$volname is not a base image and snapname is not provided\n"
if !$isBase && !length($snapname);
my $name = $class->find_free_diskname($storeid, $scfg, $vmid);
warn "clone $volname: $basename snapname $snap to $name\n";
if (length($snapname)) {
my (undef, undef, undef, $protected) = rbd_volume_info($scfg, $storeid, $volname, $snapname);
if (!$protected) {
my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'protect', $volname, '--snap', $snapname);
run_rbd_command($cmd, errmsg => "rbd protect $volname snap $snapname error");
}
}
my $newvol = "$basename/$name";
$newvol = $name if length($snapname);
my @options = (
get_rbd_path($scfg, $basename),
'--snap', $snap,
);
push @options, ('--data-pool', $scfg->{'data-pool'}) if $scfg->{'data-pool'};
my $cmd = $rbd_cmd->($scfg, $storeid, 'clone', @options, get_rbd_path($scfg, $name));
run_rbd_command($cmd, errmsg => "rbd clone '$basename' error");
return $newvol;
}
sub alloc_image {
my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
die "illegal name '$name' - should be 'vm-$vmid-*'\n"
if $name && $name !~ m/^vm-$vmid-/;
$name = $class->find_free_diskname($storeid, $scfg, $vmid) if !$name;
my @options = (
'--image-format' , 2,
'--size', int(($size + 1023) / 1024),
);
push @options, ('--data-pool', $scfg->{'data-pool'}) if $scfg->{'data-pool'};
my $cmd = $rbd_cmd->($scfg, $storeid, 'create', @options, $name);
run_rbd_command($cmd, errmsg => "rbd create '$name' error");
return $name;
}
sub free_image {
my ($class, $storeid, $scfg, $volname, $isBase) = @_;
my ($vtype, $name, $vmid, undef, undef, undef) =
$class->parse_volname($volname);
my $snaps = rbd_ls_snap($scfg, $storeid, $name);
foreach my $snap (keys %$snaps) {
if ($snaps->{$snap}->{protected}) {
my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'unprotect', $name, '--snap', $snap);
run_rbd_command($cmd, errmsg => "rbd unprotect $name snap '$snap' error");
}
}
$class->deactivate_volume($storeid, $scfg, $volname);
my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'purge', $name);
run_rbd_command($cmd, errmsg => "rbd snap purge '$name' error");
$cmd = $rbd_cmd->($scfg, $storeid, 'rm', $name);
run_rbd_command($cmd, errmsg => "rbd rm '$name' error");
return undef;
}
sub list_images {
my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
$cache->{rbd} = rbd_ls($scfg, $storeid) if !$cache->{rbd};
my $dat = $cache->{rbd}->{get_rbd_path($scfg)};
return [] if !$dat; # nothing found
my $res = [];
for my $image (sort keys %$dat) {
my $info = $dat->{$image};
my ($volname, $parent, $owner) = $info->@{'name', 'parent', 'vmid'};
if ($parent && $parent =~ m/^(base-\d+-\S+)\@__base__$/) {
$info->{volid} = "$storeid:$1/$volname";
} else {
$info->{volid} = "$storeid:$volname";
}
if ($vollist) {
my $found = grep { $_ eq $info->{volid} } @$vollist;
next if !$found;
} else {
next if defined ($vmid) && ($owner ne $vmid);
}
$info->{format} = 'raw';
push @$res, $info;
}
return $res;
}
sub status {
my ($class, $storeid, $scfg, $cache) = @_;
my $rados = $librados_connect->($scfg, $storeid);
my $df = $rados->mon_command({ prefix => 'df', format => 'json' });
my $pool = $scfg->{'data-pool'} // $scfg->{pool} // 'rbd';
my ($d) = grep { $_->{name} eq $pool } @{$df->{pools}};
if (!defined($d)) {
warn "could not get usage stats for pool '$pool'\n";
return;
}
# max_avail -> max available space for data w/o replication in the pool
# bytes_used -> data w/o replication in the pool
my $free = $d->{stats}->{max_avail};
my $used = $d->{stats}->{stored} // $d->{stats}->{bytes_used};
my $total = $used + $free;
my $active = 1;
return ($total, $free, $used, $active);
}
sub activate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
return 1;
}
sub deactivate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
return 1;
}
sub map_volume {
my ($class, $storeid, $scfg, $volname, $snapname) = @_;
my ($vtype, $img_name, $vmid) = $class->parse_volname($volname);
my $name = $img_name;
$name .= '@'.$snapname if $snapname;
my $kerneldev = get_rbd_dev_path($scfg, $storeid, $name);
return $kerneldev if -b $kerneldev; # already mapped
# features can only be enabled/disabled for image, not for snapshot!
$krbd_feature_update->($scfg, $storeid, $img_name);
my $cmd = $rbd_cmd->($scfg, $storeid, 'map', $name);
run_rbd_command($cmd, errmsg => "can't map rbd volume $name");
return $kerneldev;
}
sub unmap_volume {
my ($class, $storeid, $scfg, $volname, $snapname) = @_;
my ($vtype, $name, $vmid) = $class->parse_volname($volname);
$name .= '@'.$snapname if $snapname;
my $kerneldev = get_rbd_dev_path($scfg, $storeid, $name);
if (-b $kerneldev) {
my $cmd = $rbd_cmd->($scfg, $storeid, 'unmap', $kerneldev);
run_rbd_command($cmd, errmsg => "can't unmap rbd device $kerneldev");
}
return 1;
}
sub activate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
$class->map_volume($storeid, $scfg, $volname, $snapname) if $scfg->{krbd};
return 1;
}
sub deactivate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
$class->unmap_volume($storeid, $scfg, $volname, $snapname);
return 1;
}
sub volume_size_info {
my ($class, $scfg, $storeid, $volname, $timeout) = @_;
my ($vtype, $name, $vmid) = $class->parse_volname($volname);
my ($size, $parent) = rbd_volume_info($scfg, $storeid, $name);
my $used = wantarray ? rbd_volume_du($scfg, $storeid, $name) : 0;
return wantarray ? ($size, 'raw', $used, $parent) : $size;
}
sub volume_resize {
my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
return 1 if $running && !$scfg->{krbd}; # FIXME???
my ($vtype, $name, $vmid) = $class->parse_volname($volname);
my $cmd = $rbd_cmd->($scfg, $storeid, 'resize', '--allow-shrink', '--size', ($size/1024/1024), $name);
run_rbd_command($cmd, errmsg => "rbd resize '$volname' error");
return undef;
}
sub volume_snapshot {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
my ($vtype, $name, $vmid) = $class->parse_volname($volname);
my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'create', '--snap', $snap, $name);
run_rbd_command($cmd, errmsg => "rbd snapshot '$volname' error");
return undef;
}
sub volume_snapshot_rollback {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
my ($vtype, $name, $vmid) = $class->parse_volname($volname);
my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'rollback', '--snap', $snap, $name);
run_rbd_command($cmd, errmsg => "rbd snapshot $volname to '$snap' error");
}
sub volume_snapshot_delete {
my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
$class->deactivate_volume($storeid, $scfg, $volname, $snap, {});
my ($vtype, $name, $vmid) = $class->parse_volname($volname);
my (undef, undef, undef, $protected) = rbd_volume_info($scfg, $storeid, $name, $snap);
if ($protected){
my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'unprotect', $name, '--snap', $snap);
run_rbd_command($cmd, errmsg => "rbd unprotect $name snap '$snap' error");
}
my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'rm', '--snap', $snap, $name);
run_rbd_command($cmd, errmsg => "rbd snapshot '$volname' error");
return undef;
}
sub volume_snapshot_needs_fsfreeze {
return 1;
}
sub volume_has_feature {
my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
my $features = {
snapshot => { current => 1, snap => 1},
clone => { base => 1, snap => 1},
template => { current => 1},
copy => { base => 1, current => 1, snap => 1},
sparseinit => { base => 1, current => 1},
rename => {current => 1},
};
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) = $class->parse_volname($volname);
my $key = undef;
if ($snapname){
$key = 'snap';
} else {
$key = $isBase ? 'base' : 'current';
}
return 1 if $features->{$feature}->{$key};
return undef;
}
sub rename_volume {
my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
my (
undef,
$source_image,
$source_vmid,
$base_name,
$base_vmid,
undef,
$format
) = $class->parse_volname($source_volname);
$target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format)
if !$target_volname;
eval {
my $cmd = $rbd_cmd->($scfg, $storeid, 'info', $target_volname);
run_rbd_command($cmd, errmsg => "exist check", quiet => 1);
};
die "target volume '${target_volname}' already exists\n" if !$@;
my $cmd = $rbd_cmd->($scfg, $storeid, 'rename', $source_image, $target_volname);
run_rbd_command(
$cmd,
errmsg => "could not rename image '${source_image}' to '${target_volname}'",
);
eval { $class->unmap_volume($storeid, $scfg, $source_volname); };
warn $@ if $@;
$base_name = $base_name ? "${base_name}/" : '';
return "${storeid}:${base_name}${target_volname}";
}
1;

View File

@ -0,0 +1,422 @@
package PVE::Storage::ZFSPlugin;
use strict;
use warnings;
use IO::File;
use POSIX;
use PVE::Tools qw(run_command);
use PVE::Storage::ZFSPoolPlugin;
use PVE::RPCEnvironment;
use base qw(PVE::Storage::ZFSPoolPlugin);
use PVE::Storage::LunCmd::Comstar;
use PVE::Storage::LunCmd::Istgt;
use PVE::Storage::LunCmd::Iet;
use PVE::Storage::LunCmd::LIO;
my @ssh_opts = ('-o', 'BatchMode=yes');
my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
my $id_rsa_path = '/etc/pve/priv/zfs';
my $lun_cmds = {
create_lu => 1,
delete_lu => 1,
import_lu => 1,
modify_lu => 1,
add_view => 1,
list_view => 1,
list_lu => 1,
};
my $zfs_unknown_scsi_provider = sub {
my ($provider) = @_;
die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
};
my $zfs_get_base = sub {
my ($scfg) = @_;
if ($scfg->{iscsiprovider} eq 'comstar') {
return PVE::Storage::LunCmd::Comstar::get_base;
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
return PVE::Storage::LunCmd::Istgt::get_base;
} elsif ($scfg->{iscsiprovider} eq 'iet') {
return PVE::Storage::LunCmd::Iet::get_base;
} elsif ($scfg->{iscsiprovider} eq 'LIO') {
return PVE::Storage::LunCmd::LIO::get_base;
} else {
$zfs_unknown_scsi_provider->($scfg->{iscsiprovider});
}
};
sub zfs_request {
my ($class, $scfg, $timeout, $method, @params) = @_;
$timeout = PVE::RPCEnvironment->is_worker() ? 60*60 : 10
if !$timeout;
my $msg = '';
if ($lun_cmds->{$method}) {
if ($scfg->{iscsiprovider} eq 'comstar') {
$msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'istgt') {
$msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'iet') {
$msg = PVE::Storage::LunCmd::Iet::run_lun_command($scfg, $timeout, $method, @params);
} elsif ($scfg->{iscsiprovider} eq 'LIO') {
$msg = PVE::Storage::LunCmd::LIO::run_lun_command($scfg, $timeout, $method, @params);
} else {
$zfs_unknown_scsi_provider->($scfg->{iscsiprovider});
}
} else {
my $target = 'root@' . $scfg->{portal};
my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target];
if ($method eq 'zpool_list') {
push @$cmd, 'zpool', 'list';
} else {
push @$cmd, 'zfs', $method;
}
push @$cmd, @params;
my $output = sub {
my $line = shift;
$msg .= "$line\n";
};
run_command($cmd, outfunc => $output, timeout => $timeout);
}
return $msg;
}
sub zfs_get_lu_name {
my ($class, $scfg, $zvol) = @_;
my $base = $zfs_get_base->($scfg);
$zvol = ($class->parse_volname($zvol))[1];
my $object = ($zvol =~ /^.+\/.+/) ? "$base/$zvol" : "$base/$scfg->{pool}/$zvol";
my $lu_name = $class->zfs_request($scfg, undef, 'list_lu', $object);
return $lu_name if $lu_name;
die "Could not find lu_name for zvol $zvol";
}
sub zfs_add_lun_mapping_entry {
my ($class, $scfg, $zvol, $guid) = @_;
if (!defined($guid)) {
$guid = $class->zfs_get_lu_name($scfg, $zvol);
}
$class->zfs_request($scfg, undef, 'add_view', $guid);
}
sub zfs_delete_lu {
my ($class, $scfg, $zvol) = @_;
my $guid = $class->zfs_get_lu_name($scfg, $zvol);
$class->zfs_request($scfg, undef, 'delete_lu', $guid);
}
sub zfs_create_lu {
my ($class, $scfg, $zvol) = @_;
my $base = $zfs_get_base->($scfg);
my $guid = $class->zfs_request($scfg, undef, 'create_lu', "$base/$scfg->{pool}/$zvol");
return $guid;
}
sub zfs_import_lu {
my ($class, $scfg, $zvol) = @_;
my $base = $zfs_get_base->($scfg);
$class->zfs_request($scfg, undef, 'import_lu', "$base/$scfg->{pool}/$zvol");
}
sub zfs_resize_lu {
my ($class, $scfg, $zvol, $size) = @_;
my $guid = $class->zfs_get_lu_name($scfg, $zvol);
$class->zfs_request($scfg, undef, 'modify_lu', "${size}K", $guid);
}
sub zfs_get_lun_number {
my ($class, $scfg, $guid) = @_;
die "could not find lun_number for guid $guid" if !$guid;
if ($class->zfs_request($scfg, undef, 'list_view', $guid) =~ /^(\d+)$/) {
return $1;
}
die "lun_number for guid $guid is not a number";
}
# Configuration
sub type {
return 'zfs';
}
sub plugindata {
return {
content => [ {images => 1}, { images => 1 }],
};
}
sub properties {
return {
iscsiprovider => {
description => "iscsi provider",
type => 'string',
},
# this will disable write caching on comstar and istgt.
# it is not implemented for iet. iet blockio always operates with
# writethrough caching when not in readonly mode
nowritecache => {
description => "disable write caching on the target",
type => 'boolean',
},
comstar_tg => {
description => "target group for comstar views",
type => 'string',
},
comstar_hg => {
description => "host group for comstar views",
type => 'string',
},
lio_tpg => {
description => "target portal group for Linux LIO targets",
type => 'string',
},
};
}
sub options {
return {
nodes => { optional => 1 },
disable => { optional => 1 },
portal => { fixed => 1 },
target => { fixed => 1 },
pool => { fixed => 1 },
blocksize => { fixed => 1 },
iscsiprovider => { fixed => 1 },
nowritecache => { optional => 1 },
sparse => { optional => 1 },
comstar_hg => { optional => 1 },
comstar_tg => { optional => 1 },
lio_tpg => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
};
}
# Storage implementation
sub path {
my ($class, $scfg, $volname, $storeid, $snapname) = @_;
die "direct access to snapshots not implemented"
if defined($snapname);
my ($vtype, $name, $vmid) = $class->parse_volname($volname);
my $target = $scfg->{target};
my $portal = $scfg->{portal};
my $guid = $class->zfs_get_lu_name($scfg, $name);
my $lun = $class->zfs_get_lun_number($scfg, $guid);
my $path = "iscsi://$portal/$target/$lun";
return ($path, $vmid, $vtype);
}
sub create_base {
my ($class, $storeid, $scfg, $volname) = @_;
my $snap = '__base__';
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
$class->parse_volname($volname);
die "create_base not possible with base image\n" if $isBase;
my $newname = $name;
$newname =~ s/^vm-/base-/;
my $newvolname = $basename ? "$basename/$newname" : "$newname";
$class->zfs_delete_lu($scfg, $name);
$class->zfs_request($scfg, undef, 'rename', "$scfg->{pool}/$name", "$scfg->{pool}/$newname");
my $guid = $class->zfs_create_lu($scfg, $newname);
$class->zfs_add_lun_mapping_entry($scfg, $newname, $guid);
my $running = undef; #fixme : is create_base always offline ?
$class->volume_snapshot($scfg, $storeid, $newname, $snap, $running);
return $newvolname;
}
sub clone_image {
my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
my $name = $class->SUPER::clone_image($scfg, $storeid, $volname, $vmid, $snap);
# get ZFS dataset name from PVE volname
my (undef, $clonedname) = $class->parse_volname($name);
my $guid = $class->zfs_create_lu($scfg, $clonedname);
$class->zfs_add_lun_mapping_entry($scfg, $clonedname, $guid);
return $name;
}
sub alloc_image {
my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
die "unsupported format '$fmt'" if $fmt ne 'raw';
die "illegal name '$name' - should be 'vm-$vmid-*'\n"
if $name && $name !~ m/^vm-$vmid-/;
my $volname = $name;
$volname = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt) if !$volname;
$class->zfs_create_zvol($scfg, $volname, $size);
my $guid = $class->zfs_create_lu($scfg, $volname);
$class->zfs_add_lun_mapping_entry($scfg, $volname, $guid);
return $volname;
}
sub free_image {
my ($class, $storeid, $scfg, $volname, $isBase) = @_;
my ($vtype, $name, $vmid) = $class->parse_volname($volname);
$class->zfs_delete_lu($scfg, $name);
eval { $class->zfs_delete_zvol($scfg, $name); };
if (my $err = $@) {
my $guid = $class->zfs_create_lu($scfg, $name);
$class->zfs_add_lun_mapping_entry($scfg, $name, $guid);
die $err;
}
return undef;
}
sub volume_resize {
my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
$volname = ($class->parse_volname($volname))[1];
my $new_size = $class->SUPER::volume_resize($scfg, $storeid, $volname, $size, $running);
$class->zfs_resize_lu($scfg, $volname, $new_size);
return $new_size;
}
sub volume_snapshot_delete {
my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
$volname = ($class->parse_volname($volname))[1];
$class->zfs_request($scfg, undef, 'destroy', "$scfg->{pool}/$volname\@$snap");
}
sub volume_snapshot_rollback {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
$volname = ($class->parse_volname($volname))[1];
$class->zfs_delete_lu($scfg, $volname);
$class->zfs_request($scfg, undef, 'rollback', "$scfg->{pool}/$volname\@$snap");
$class->zfs_import_lu($scfg, $volname);
$class->zfs_add_lun_mapping_entry($scfg, $volname);
}
sub storage_can_replicate {
my ($class, $scfg, $storeid, $format) = @_;
return 0;
}
sub volume_has_feature {
my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
my $features = {
snapshot => { current => 1, snap => 1},
clone => { base => 1},
template => { current => 1},
copy => { base => 1, current => 1},
};
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
$class->parse_volname($volname);
my $key = undef;
if ($snapname) {
$key = 'snap';
} else {
$key = $isBase ? 'base' : 'current';
}
return 1 if $features->{$feature}->{$key};
return undef;
}
sub activate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
return 1;
}
sub deactivate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
return 1;
}
sub activate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
die "unable to activate snapshot from remote zfs storage" if $snapname;
return 1;
}
sub deactivate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
die "unable to deactivate snapshot from remote zfs storage" if $snapname;
return 1;
}
1;

View File

@ -0,0 +1,852 @@
package PVE::Storage::ZFSPoolPlugin;
use strict;
use warnings;
use IO::File;
use Net::IP;
use POSIX;
use PVE::ProcFSTools;
use PVE::RPCEnvironment;
use PVE::Storage::Plugin;
use PVE::Tools qw(run_command);
use base qw(PVE::Storage::Plugin);
sub type {
return 'zfspool';
}
sub plugindata {
return {
content => [ {images => 1, rootdir => 1}, {images => 1 , rootdir => 1}],
format => [ { raw => 1, subvol => 1 } , 'raw' ],
};
}
sub properties {
return {
blocksize => {
description => "block size",
type => 'string',
},
sparse => {
description => "use sparse volumes",
type => 'boolean',
},
mountpoint => {
description => "mount point",
type => 'string', format => 'pve-storage-path',
},
};
}
sub options {
return {
pool => { fixed => 1 },
blocksize => { optional => 1 },
sparse => { optional => 1 },
nodes => { optional => 1 },
disable => { optional => 1 },
content => { optional => 1 },
bwlimit => { optional => 1 },
mountpoint => { optional => 1 },
};
}
# static zfs helper methods
sub zfs_parse_zvol_list {
my ($text, $pool) = @_;
my $list = ();
return $list if !$text;
my @lines = split /\n/, $text;
foreach my $line (@lines) {
my ($dataset, $size, $origin, $type, $refquota) = split(/\s+/, $line);
next if !($type eq 'volume' || $type eq 'filesystem');
my $zvol = {};
my @parts = split /\//, $dataset;
next if scalar(@parts) < 2; # we need pool/name
my $name = pop @parts;
my $parsed_pool = join('/', @parts);
next if $parsed_pool ne $pool;
next unless $name =~ m!^(vm|base|subvol|basevol)-(\d+)-(\S+)$!;
$zvol->{owner} = $2;
$zvol->{name} = $name;
if ($type eq 'filesystem') {
if ($refquota eq 'none') {
$zvol->{size} = 0;
} else {
$zvol->{size} = $refquota + 0;
}
$zvol->{format} = 'subvol';
} else {
$zvol->{size} = $size + 0;
$zvol->{format} = 'raw';
}
if ($origin !~ /^-$/) {
$zvol->{origin} = $origin;
}
push @$list, $zvol;
}
return $list;
}
sub parse_volname {
my ($class, $volname) = @_;
if ($volname =~ m/^(((base|basevol)-(\d+)-\S+)\/)?((base|basevol|vm|subvol)-(\d+)-\S+)$/) {
my $format = ($6 eq 'subvol' || $6 eq 'basevol') ? 'subvol' : 'raw';
my $isBase = ($6 eq 'base' || $6 eq 'basevol');
return ('images', $5, $7, $2, $4, $isBase, $format);
}
die "unable to parse zfs volume name '$volname'\n";
}
# virtual zfs methods (subclass can overwrite them)
sub on_add_hook {
my ($class, $storeid, $scfg, %param) = @_;
my $cfg_mountpoint = $scfg->{mountpoint};
# ignore failure, pool might currently not be imported
my $mountpoint;
eval {
my $res = $class->zfs_get_properties($scfg, 'mountpoint', $scfg->{pool}, 1);
$mountpoint = PVE::Storage::Plugin::verify_path($res, 1) if defined($res);
};
if (defined($cfg_mountpoint)) {
if (defined($mountpoint) && !($cfg_mountpoint =~ m|^\Q$mountpoint\E/?$|)) {
warn "warning for $storeid - mountpoint: $cfg_mountpoint " .
"does not match current mount point: $mountpoint\n";
}
} else {
$scfg->{mountpoint} = $mountpoint;
}
return;
}
sub path {
my ($class, $scfg, $volname, $storeid, $snapname) = @_;
my ($vtype, $name, $vmid) = $class->parse_volname($volname);
my $path = '';
my $mountpoint = $scfg->{mountpoint} // "/$scfg->{pool}";
if ($vtype eq "images") {
if ($name =~ m/^subvol-/ || $name =~ m/^basevol-/) {
$path = "$mountpoint/$name";
} else {
$path = "/dev/zvol/$scfg->{pool}/$name";
}
$path .= "\@$snapname" if defined($snapname);
} else {
die "$vtype is not allowed in ZFSPool!";
}
return ($path, $vmid, $vtype);
}
sub zfs_request {
my ($class, $scfg, $timeout, $method, @params) = @_;
my $cmd = [];
if ($method eq 'zpool_list') {
push @$cmd, 'zpool', 'list';
} elsif ($method eq 'zpool_import') {
push @$cmd, 'zpool', 'import';
$timeout = 15 if !$timeout || $timeout < 15;
} else {
push @$cmd, 'zfs', $method;
}
push @$cmd, @params;
my $msg = '';
my $output = sub { $msg .= "$_[0]\n" };
if (PVE::RPCEnvironment->is_worker()) {
$timeout = 60*60 if !$timeout;
$timeout = 60*5 if $timeout < 60*5;
} else {
$timeout = 10 if !$timeout;
}
run_command($cmd, errmsg => "zfs error", outfunc => $output, timeout => $timeout);
return $msg;
}
sub zfs_wait_for_zvol_link {
my ($class, $scfg, $volname, $timeout) = @_;
my $default_timeout = PVE::RPCEnvironment->is_worker() ? 60*5 : 10;
$timeout = $default_timeout if !defined($timeout);
my ($devname, undef, undef) = $class->path($scfg, $volname);
for (my $i = 1; $i <= $timeout; $i++) {
last if -b $devname;
die "timeout: no zvol device link for '$volname' found after $timeout sec found.\n"
if $i == $timeout;
sleep(1);
}
}
sub alloc_image {
my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
my $volname = $name;
if ($fmt eq 'raw') {
die "illegal name '$volname' - should be 'vm-$vmid-*'\n"
if $volname && $volname !~ m/^vm-$vmid-/;
$volname = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt)
if !$volname;
$class->zfs_create_zvol($scfg, $volname, $size);
$class->zfs_wait_for_zvol_link($scfg, $volname);
} elsif ( $fmt eq 'subvol') {
die "illegal name '$volname' - should be 'subvol-$vmid-*'\n"
if $volname && $volname !~ m/^subvol-$vmid-/;
$volname = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt)
if !$volname;
die "illegal name '$volname' - should be 'subvol-$vmid-*'\n"
if $volname !~ m/^subvol-$vmid-/;
$class->zfs_create_subvol($scfg, $volname, $size);
} else {
die "unsupported format '$fmt'";
}
return $volname;
}
sub free_image {
my ($class, $storeid, $scfg, $volname, $isBase) = @_;
my (undef, $name, undef) = $class->parse_volname($volname);
$class->zfs_delete_zvol($scfg, $name);
return undef;
}
sub list_images {
my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
my $zfs_list = $class->zfs_list_zvol($scfg);
my $res = [];
for my $info (values $zfs_list->%*) {
my $volname = $info->{name};
my $parent = $info->{parent};
my $owner = $info->{vmid};
if ($parent && $parent =~ m/^(\S+)\@__base__$/) {
my ($basename) = ($1);
$info->{volid} = "$storeid:$basename/$volname";
} else {
$info->{volid} = "$storeid:$volname";
}
if ($vollist) {
my $found = grep { $_ eq $info->{volid} } @$vollist;
next if !$found;
} else {
next if defined ($vmid) && ($owner ne $vmid);
}
push @$res, $info;
}
return $res;
}
sub zfs_get_properties {
my ($class, $scfg, $properties, $dataset, $timeout) = @_;
my $result = $class->zfs_request($scfg, $timeout, 'get', '-o', 'value',
'-Hp', $properties, $dataset);
my @values = split /\n/, $result;
return wantarray ? @values : $values[0];
}
sub zfs_get_pool_stats {
my ($class, $scfg) = @_;
my $available = 0;
my $used = 0;
my @lines = $class->zfs_get_properties($scfg, 'available,used', $scfg->{pool});
if($lines[0] =~ /^(\d+)$/) {
$available = $1;
}
if($lines[1] =~ /^(\d+)$/) {
$used = $1;
}
return ($available, $used);
}
sub zfs_create_zvol {
my ($class, $scfg, $zvol, $size) = @_;
# always align size to 1M as workaround until
# https://github.com/zfsonlinux/zfs/issues/8541 is solved
my $padding = (1024 - $size % 1024) % 1024;
$size = $size + $padding;
my $cmd = ['create'];
push @$cmd, '-s' if $scfg->{sparse};
push @$cmd, '-b', $scfg->{blocksize} if $scfg->{blocksize};
push @$cmd, '-V', "${size}k", "$scfg->{pool}/$zvol";
$class->zfs_request($scfg, undef, @$cmd);
}
sub zfs_create_subvol {
my ($class, $scfg, $volname, $size) = @_;
my $dataset = "$scfg->{pool}/$volname";
my $quota = $size ? "${size}k" : "none";
my $cmd = ['create', '-o', 'acltype=posixacl', '-o', 'xattr=sa',
'-o', "refquota=${quota}", $dataset];
$class->zfs_request($scfg, undef, @$cmd);
}
sub zfs_delete_zvol {
my ($class, $scfg, $zvol) = @_;
my $err;
for (my $i = 0; $i < 6; $i++) {
eval { $class->zfs_request($scfg, undef, 'destroy', '-r', "$scfg->{pool}/$zvol"); };
if ($err = $@) {
if ($err =~ m/^zfs error:(.*): dataset is busy.*/) {
sleep(1);
} elsif ($err =~ m/^zfs error:.*: dataset does not exist.*$/) {
$err = undef;
last;
} else {
die $err;
}
} else {
last;
}
}
die $err if $err;
}
sub zfs_list_zvol {
my ($class, $scfg) = @_;
my $text = $class->zfs_request(
$scfg,
10,
'list',
'-o',
'name,volsize,origin,type,refquota',
'-t',
'volume,filesystem',
'-d1',
'-Hp',
$scfg->{pool},
);
# It's still required to have zfs_parse_zvol_list filter by pool, because -d1 lists
# $scfg->{pool} too and while unlikely, it could be named to be mistaken for a volume.
my $zvols = zfs_parse_zvol_list($text, $scfg->{pool});
return {} if !$zvols;
my $list = {};
foreach my $zvol (@$zvols) {
my $name = $zvol->{name};
my $parent = $zvol->{origin};
if($zvol->{origin} && $zvol->{origin} =~ m/^$scfg->{pool}\/(\S+)$/){
$parent = $1;
}
$list->{$name} = {
name => $name,
size => $zvol->{size},
parent => $parent,
format => $zvol->{format},
vmid => $zvol->{owner},
};
}
return $list;
}
sub zfs_get_sorted_snapshot_list {
my ($class, $scfg, $volname, $sort_params) = @_;
my @params = ('-H', '-r', '-t', 'snapshot', '-o', 'name', $sort_params->@*);
my $vname = ($class->parse_volname($volname))[1];
push @params, "$scfg->{pool}\/$vname";
my $text = $class->zfs_request($scfg, undef, 'list', @params);
my @snapshots = split(/\n/, $text);
my $snap_names = [];
for my $snapshot (@snapshots) {
(my $snap_name = $snapshot) =~ s/^.*@//;
push $snap_names->@*, $snap_name;
}
return $snap_names;
}
sub status {
my ($class, $storeid, $scfg, $cache) = @_;
my $total = 0;
my $free = 0;
my $used = 0;
my $active = 0;
eval {
($free, $used) = $class->zfs_get_pool_stats($scfg);
$active = 1;
$total = $free + $used;
};
warn $@ if $@;
return ($total, $free, $used, $active);
}
sub volume_size_info {
my ($class, $scfg, $storeid, $volname, $timeout) = @_;
my (undef, $vname, undef, $parent, undef, undef, $format) =
$class->parse_volname($volname);
my $attr = $format eq 'subvol' ? 'refquota' : 'volsize';
my ($size, $used) = $class->zfs_get_properties($scfg, "$attr,usedbydataset", "$scfg->{pool}/$vname");
$used = ($used =~ /^(\d+)$/) ? $1 : 0;
if ($size =~ /^(\d+)$/) {
return wantarray ? ($1, $format, $used, $parent) : $1;
}
die "Could not get zfs volume size\n";
}
sub volume_snapshot {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
my $vname = ($class->parse_volname($volname))[1];
$class->zfs_request($scfg, undef, 'snapshot', "$scfg->{pool}/$vname\@$snap");
}
sub volume_snapshot_delete {
my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
my $vname = ($class->parse_volname($volname))[1];
$class->deactivate_volume($storeid, $scfg, $vname, $snap, {});
$class->zfs_request($scfg, undef, 'destroy', "$scfg->{pool}/$vname\@$snap");
}
sub volume_snapshot_rollback {
my ($class, $scfg, $storeid, $volname, $snap) = @_;
my (undef, $vname, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
my $msg = $class->zfs_request($scfg, undef, 'rollback', "$scfg->{pool}/$vname\@$snap");
# we have to unmount rollbacked subvols, to invalidate wrong kernel
# caches, they get mounted in activate volume again
# see zfs bug #10931 https://github.com/openzfs/zfs/issues/10931
if ($format eq 'subvol') {
eval { $class->zfs_request($scfg, undef, 'unmount', "$scfg->{pool}/$vname"); };
if (my $err = $@) {
die $err if $err !~ m/not currently mounted$/;
}
}
return $msg;
}
sub volume_rollback_is_possible {
my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_;
# can't use '-S creation', because zfs list won't reverse the order when the
# creation time is the same second, breaking at least our tests.
my $snapshots = $class->zfs_get_sorted_snapshot_list($scfg, $volname, ['-s', 'creation']);
my $found;
$blockers //= []; # not guaranteed to be set by caller
for my $snapshot ($snapshots->@*) {
if ($snapshot eq $snap) {
$found = 1;
} elsif ($found) {
push $blockers->@*, $snapshot;
}
}
my $volid = "${storeid}:${volname}";
die "can't rollback, snapshot '$snap' does not exist on '$volid'\n"
if !$found;
die "can't rollback, '$snap' is not most recent snapshot on '$volid'\n"
if scalar($blockers->@*) > 0;
return 1;
}
sub volume_snapshot_info {
my ($class, $scfg, $storeid, $volname) = @_;
my @params = ('-Hp', '-r', '-t', 'snapshot', '-o', 'name,guid,creation');
my $vname = ($class->parse_volname($volname))[1];
push @params, "$scfg->{pool}\/$vname";
my $text = $class->zfs_request($scfg, undef, 'list', @params);
my @lines = split(/\n/, $text);
my $info = {};
for my $line (@lines) {
my ($snapshot, $guid, $creation) = split(/\s+/, $line);
(my $snap_name = $snapshot) =~ s/^.*@//;
$info->{$snap_name} = {
id => $guid,
timestamp => $creation,
};
}
return $info;
}
my sub dataset_mounted_heuristic {
my ($dataset) = @_;
my $mounts = PVE::ProcFSTools::parse_proc_mounts();
for my $mp (@$mounts) {
my ($what, $dir, $fs) = $mp->@*;
next if $fs ne 'zfs';
# check for root-dataset or any child-dataset (root-dataset could have 'canmount=off')
# If any child is mounted heuristically assume that `zfs mount -a` was successful
next if $what !~ m!^$dataset(?:/|$)!;
return 1;
}
return 0;
}
sub activate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
# Note: $scfg->{pool} can include dataset <pool>/<dataset>
my $dataset = $scfg->{pool};
my $pool = ($dataset =~ s!/.*$!!r);
return 1 if dataset_mounted_heuristic($dataset); # early return
my $pool_imported = sub {
my @param = ('-o', 'name', '-H', $pool);
my $res = eval { $class->zfs_request($scfg, undef, 'zpool_list', @param) };
warn "$@\n" if $@;
return defined($res) && $res =~ m/$pool/;
};
if (!$pool_imported->()) {
# import can only be done if not yet imported!
my @param = ('-d', '/dev/disk/by-id/', '-o', 'cachefile=none', $pool);
eval { $class->zfs_request($scfg, undef, 'zpool_import', @param) };
if (my $err = $@) {
# just could've raced with another import, so recheck if it is imported
die "could not activate storage '$storeid', $err\n" if !$pool_imported->();
}
}
eval { $class->zfs_request($scfg, undef, 'mount', '-a') };
die "could not activate storage '$storeid', $@\n" if $@;
return 1;
}
sub deactivate_storage {
my ($class, $storeid, $scfg, $cache) = @_;
return 1;
}
sub activate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
return 1 if defined($snapname);
my (undef, $dataset, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
if ($format eq 'raw') {
$class->zfs_wait_for_zvol_link($scfg, $volname);
} elsif ($format eq 'subvol') {
my $mounted = $class->zfs_get_properties($scfg, 'mounted', "$scfg->{pool}/$dataset");
if ($mounted !~ m/^yes$/) {
$class->zfs_request($scfg, undef, 'mount', "$scfg->{pool}/$dataset");
}
}
return 1;
}
sub deactivate_volume {
my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
return 1;
}
sub clone_image {
my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
$snap ||= '__base__';
my ($vtype, $basename, $basevmid, undef, undef, $isBase, $format) =
$class->parse_volname($volname);
die "clone_image only works on base images\n" if !$isBase;
my $name = $class->find_free_diskname($storeid, $scfg, $vmid, $format);
if ($format eq 'subvol') {
my $size = $class->zfs_request($scfg, undef, 'list', '-Hp', '-o', 'refquota', "$scfg->{pool}/$basename");
chomp($size);
$class->zfs_request($scfg, undef, 'clone', "$scfg->{pool}/$basename\@$snap", "$scfg->{pool}/$name", '-o', "refquota=$size");
} else {
$class->zfs_request($scfg, undef, 'clone', "$scfg->{pool}/$basename\@$snap", "$scfg->{pool}/$name");
}
return "$basename/$name";
}
sub create_base {
my ($class, $storeid, $scfg, $volname) = @_;
my $snap = '__base__';
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) =
$class->parse_volname($volname);
die "create_base not possible with base image\n" if $isBase;
my $newname = $name;
if ( $format eq 'subvol' ) {
$newname =~ s/^subvol-/basevol-/;
} else {
$newname =~ s/^vm-/base-/;
}
my $newvolname = $basename ? "$basename/$newname" : "$newname";
$class->zfs_request($scfg, undef, 'rename', "$scfg->{pool}/$name", "$scfg->{pool}/$newname");
my $running = undef; #fixme : is create_base always offline ?
$class->volume_snapshot($scfg, $storeid, $newname, $snap, $running);
return $newvolname;
}
sub volume_resize {
my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
my $new_size = int($size/1024);
my (undef, $vname, undef, undef, undef, undef, $format) =
$class->parse_volname($volname);
my $attr = $format eq 'subvol' ? 'refquota' : 'volsize';
# align size to 1M so we always have a valid multiple of the volume block size
if ($format eq 'raw') {
my $padding = (1024 - $new_size % 1024) % 1024;
$new_size = $new_size + $padding;
}
$class->zfs_request($scfg, undef, 'set', "$attr=${new_size}k", "$scfg->{pool}/$vname");
return $new_size;
}
sub storage_can_replicate {
my ($class, $scfg, $storeid, $format) = @_;
return 1 if $format eq 'raw' || $format eq 'subvol';
return 0;
}
sub volume_has_feature {
my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
my $features = {
snapshot => { current => 1, snap => 1},
clone => { base => 1},
template => { current => 1},
copy => { base => 1, current => 1},
sparseinit => { base => 1, current => 1},
replicate => { base => 1, current => 1},
rename => {current => 1},
};
my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
$class->parse_volname($volname);
my $key = undef;
if ($snapname) {
$key = 'snap';
} else {
$key = $isBase ? 'base' : 'current';
}
return 1 if $features->{$feature}->{$key};
return undef;
}
sub volume_export {
my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots) = @_;
die "unsupported export stream format for $class: $format\n"
if $format ne 'zfs';
die "$class storage can only export snapshots\n"
if !defined($snapshot);
my $dataset = ($class->parse_volname($volname))[1];
my $fd = fileno($fh);
die "internal error: invalid file handle for volume_export\n"
if !defined($fd);
$fd = ">&$fd";
# For zfs we always create a replication stream (-R) which means the remote
# side will always delete non-existing source snapshots. This should work
# for all our use cases.
my $cmd = ['zfs', 'send', '-Rpv'];
if (defined($base_snapshot)) {
my $arg = $with_snapshots ? '-I' : '-i';
push @$cmd, $arg, $base_snapshot;
}
push @$cmd, '--', "$scfg->{pool}/$dataset\@$snapshot";
run_command($cmd, output => $fd);
return;
}
sub volume_export_formats {
my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
my @formats = ('zfs');
# TODOs:
# push @formats, 'fies' if $volname !~ /^(?:basevol|subvol)-/;
# push @formats, 'raw' if !$base_snapshot && !$with_snapshots;
return @formats;
}
sub volume_import {
my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots, $allow_rename) = @_;
die "unsupported import stream format for $class: $format\n"
if $format ne 'zfs';
my $fd = fileno($fh);
die "internal error: invalid file handle for volume_import\n"
if !defined($fd);
my (undef, $dataset, $vmid, undef, undef, undef, $volume_format) =
$class->parse_volname($volname);
my $zfspath = "$scfg->{pool}/$dataset";
my $suffix = defined($base_snapshot) ? "\@$base_snapshot" : '';
my $exists = 0 == run_command(['zfs', 'get', '-H', 'name', $zfspath.$suffix],
noerr => 1, quiet => 1);
if (defined($base_snapshot)) {
die "base snapshot '$zfspath\@$base_snapshot' doesn't exist\n" if !$exists;
} elsif ($exists) {
die "volume '$zfspath' already exists\n" if !$allow_rename;
warn "volume '$zfspath' already exists - importing with a different name\n";
$dataset = $class->find_free_diskname($storeid, $scfg, $vmid, $volume_format);
$zfspath = "$scfg->{pool}/$dataset";
}
eval { run_command(['zfs', 'recv', '-F', '--', $zfspath], input => "<&$fd") };
if (my $err = $@) {
if (defined($base_snapshot)) {
eval { run_command(['zfs', 'rollback', '-r', '--', "$zfspath\@$base_snapshot"]) };
} else {
eval { run_command(['zfs', 'destroy', '-r', '--', $zfspath]) };
}
die $err;
}
return "$storeid:$dataset";
}
sub volume_import_formats {
my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
return $class->volume_export_formats($scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots);
}
sub rename_volume {
my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
my (
undef,
$source_image,
$source_vmid,
$base_name,
$base_vmid,
undef,
$format
) = $class->parse_volname($source_volname);
$target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format)
if !$target_volname;
my $pool = $scfg->{pool};
my $source_zfspath = "${pool}/${source_image}";
my $target_zfspath = "${pool}/${target_volname}";
my $exists = 0 == run_command(['zfs', 'get', '-H', 'name', $target_zfspath],
noerr => 1, quiet => 1);
die "target volume '${target_volname}' already exists\n" if $exists;
$class->zfs_request($scfg, 5, 'rename', ${source_zfspath}, ${target_zfspath});
$base_name = $base_name ? "${base_name}/" : '';
return "${storeid}:${base_name}${target_volname}";
}
1;