separate packaging and source build system
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
115
src/PVE/Storage/LunCmd/Comstar.pm
Normal file
115
src/PVE/Storage/LunCmd/Comstar.pm
Normal 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;
|
||||
}
|
||||
|
||||
478
src/PVE/Storage/LunCmd/Iet.pm
Normal file
478
src/PVE/Storage/LunCmd/Iet.pm
Normal 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;
|
||||
|
||||
601
src/PVE/Storage/LunCmd/Istgt.pm
Normal file
601
src/PVE/Storage/LunCmd/Istgt.pm
Normal 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;
|
||||
420
src/PVE/Storage/LunCmd/LIO.pm
Normal file
420
src/PVE/Storage/LunCmd/LIO.pm
Normal 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;
|
||||
5
src/PVE/Storage/LunCmd/Makefile
Normal file
5
src/PVE/Storage/LunCmd/Makefile
Normal 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
|
||||
Reference in New Issue
Block a user