[pve-devel] [PATCH 4/5] Added existing LunCmd code rewritten as independent scripts
Pablo Ruiz García
pablo.ruiz at gmail.com
Sun Apr 13 02:08:13 CEST 2014
Signed-off-by: Pablo Ruiz García <pablo.ruiz at gmail.com>
---
zfs-helpers/Common.pm | 285 ++++++++++++++++++++++++
zfs-helpers/comstar | 114 ++++++++++
zfs-helpers/iet | 488 ++++++++++++++++++++++++++++++++++++++++
zfs-helpers/istgt | 593 +++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 1480 insertions(+), 0 deletions(-)
create mode 100644 zfs-helpers/Common.pm
create mode 100644 zfs-helpers/comstar
create mode 100644 zfs-helpers/iet
create mode 100644 zfs-helpers/istgt
diff --git a/zfs-helpers/Common.pm b/zfs-helpers/Common.pm
new file mode 100644
index 0000000..1c240d1
--- /dev/null
+++ b/zfs-helpers/Common.pm
@@ -0,0 +1,285 @@
+package Common;
+
+use strict;
+use warnings;
+use POSIX qw(EINTR);
+use IO::Socket::INET;
+use IO::Select;
+use File::Basename;
+use File::Path qw(make_path);
+use IO::File;
+use IO::Dir;
+use IPC::Open3;
+use Fcntl qw(:DEFAULT :flock);
+#use base 'Exporter';
+use URI::Escape;
+use Encode;
+use Digest::SHA;
+use Text::ParseWords;
+use String::ShellQuote;
+
+our @EXPORT_OK = qw(
+run_command
+);
+
+sub run_command {
+ my ($cmd, %param) = @_;
+
+ my $old_umask;
+ my $cmdstr;
+
+ if (!ref($cmd)) {
+ $cmdstr = $cmd;
+ if ($cmd =~ m/|/) {
+ # see 'man bash' for option pipefail
+ $cmd = [ '/bin/bash', '-c', "set -o pipefail && $cmd" ];
+ } else {
+ $cmd = [ $cmd ];
+ }
+ } else {
+ $cmdstr = cmd2string($cmd);
+ }
+
+ my $errmsg;
+ my $laststderr;
+ my $timeout;
+ my $oldtimeout;
+ my $pid;
+
+ my $outfunc;
+ my $errfunc;
+ my $logfunc;
+ my $input;
+ my $output;
+ my $afterfork;
+
+ eval {
+
+ foreach my $p (keys %param) {
+ if ($p eq 'timeout') {
+ $timeout = $param{$p};
+ } elsif ($p eq 'umask') {
+ $old_umask = umask($param{$p});
+ } elsif ($p eq 'errmsg') {
+ $errmsg = $param{$p};
+ } elsif ($p eq 'input') {
+ $input = $param{$p};
+ } elsif ($p eq 'output') {
+ $output = $param{$p};
+ } elsif ($p eq 'outfunc') {
+ $outfunc = $param{$p};
+ } elsif ($p eq 'errfunc') {
+ $errfunc = $param{$p};
+ } elsif ($p eq 'logfunc') {
+ $logfunc = $param{$p};
+ } elsif ($p eq 'afterfork') {
+ $afterfork = $param{$p};
+ } else {
+ die "got unknown parameter '$p' for run_command\n";
+ }
+ }
+
+ if ($errmsg) {
+ my $origerrfunc = $errfunc;
+ $errfunc = sub {
+ if ($laststderr) {
+ if ($origerrfunc) {
+ &$origerrfunc("$laststderr\n");
+ } else {
+ print STDERR "$laststderr\n" if $laststderr;
+ }
+ }
+ $laststderr = shift;
+ };
+ }
+
+ my $reader = $output && $output =~ m/^>&/ ? $output : IO::File->new();
+ my $writer = $input && $input =~ m/^<&/ ? $input : IO::File->new();
+ my $error = IO::File->new();
+
+ # try to avoid locale related issues/warnings
+ my $lang = $param{lang} || 'C';
+
+ my $orig_pid = $$;
+
+ eval {
+ local $ENV{LC_ALL} = $lang;
+
+ # suppress LVM warnings like: "File descriptor 3 left open";
+ local $ENV{LVM_SUPPRESS_FD_WARNINGS} = "1";
+
+ $pid = open3($writer, $reader, $error, @$cmd) || die $!;
+
+ # if we pipe fron STDIN, open3 closes STDIN, so we we
+ # a perl warning "Filehandle STDIN reopened as GENXYZ .. "
+ # as soon as we open a new file.
+ # to avoid that we open /dev/null
+ if (!ref($writer) && !defined(fileno(STDIN))) {
+ POSIX::close(0);
+ open(STDIN, "</dev/null");
+ }
+ };
+
+ my $err = $@;
+
+ # catch exec errors
+ if ($orig_pid != $$) {
+ warn "ERROR: $err";
+ POSIX::_exit (1);
+ kill ('KILL', $$);
+ }
+
+ die $err if $err;
+
+ local $SIG{ALRM} = sub { die "got timeout\n"; } if $timeout;
+ $oldtimeout = alarm($timeout) if $timeout;
+
+ &$afterfork() if $afterfork;
+
+ if (ref($writer)) {
+ print $writer $input if defined $input;
+ close $writer;
+ }
+
+ my $select = new IO::Select;
+ $select->add($reader) if ref($reader);
+ $select->add($error);
+
+ my $outlog = '';
+ my $errlog = '';
+
+ my $starttime = time();
+
+ while ($select->count) {
+ my @handles = $select->can_read(1);
+
+ foreach my $h (@handles) {
+ my $buf = '';
+ my $count = sysread ($h, $buf, 4096);
+ if (!defined ($count)) {
+ my $err = $!;
+ kill (9, $pid);
+ waitpid ($pid, 0);
+ die $err;
+ }
+ $select->remove ($h) if !$count;
+ if ($h eq $reader) {
+ if ($outfunc || $logfunc) {
+ eval {
+ $outlog .= $buf;
+ while ($outlog =~ s/^([^\010\r\n]*)(\r|\n|(\010)+|\r\n)//s) {
+ my $line = $1;
+ &$outfunc($line) if $outfunc;
+ &$logfunc($line) if $logfunc;
+ }
+ };
+ my $err = $@;
+ if ($err) {
+ kill (9, $pid);
+ waitpid ($pid, 0);
+ die $err;
+ }
+ } else {
+ print $buf;
+ *STDOUT->flush();
+ }
+ } elsif ($h eq $error) {
+ if ($errfunc || $logfunc) {
+ eval {
+ $errlog .= $buf;
+ while ($errlog =~ s/^([^\010\r\n]*)(\r|\n|(\010)+|\r\n)//s) {
+ my $line = $1;
+ &$errfunc($line) if $errfunc;
+ &$logfunc($line) if $logfunc;
+ }
+ };
+ my $err = $@;
+ if ($err) {
+ kill (9, $pid);
+ waitpid ($pid, 0);
+ die $err;
+ }
+ } else {
+ print STDERR $buf;
+ *STDERR->flush();
+ }
+ }
+ }
+ }
+
+ &$outfunc($outlog) if $outfunc && $outlog;
+ &$logfunc($outlog) if $logfunc && $outlog;
+
+ &$errfunc($errlog) if $errfunc && $errlog;
+ &$logfunc($errlog) if $logfunc && $errlog;
+
+ waitpid ($pid, 0);
+
+ if ($? == -1) {
+ die "failed to execute\n";
+ } elsif (my $sig = ($? & 127)) {
+ die "got signal $sig\n";
+ } elsif (my $ec = ($? >> 8)) {
+ if (!($ec == 24 && ($cmdstr =~ m|^(\S+/)?rsync\s|))) {
+ if ($errmsg && $laststderr) {
+ my $lerr = $laststderr;
+ $laststderr = undef;
+ die "$lerr\n";
+ }
+ die "exit code $ec\n";
+ }
+ }
+
+ alarm(0);
+ };
+
+ my $err = $@;
+
+ alarm(0);
+
+ if ($errmsg && $laststderr) {
+ &$errfunc(undef); # flush laststderr
+ }
+
+ umask ($old_umask) if defined($old_umask);
+
+ alarm($oldtimeout) if $oldtimeout;
+
+ if ($err) {
+ if ($pid && ($err eq "got timeout\n")) {
+ kill (9, $pid);
+ waitpid ($pid, 0);
+ die "command '$cmdstr' failed: $err";
+ }
+
+ if ($errmsg) {
+ $err =~ s/^usermod:\s*// if $cmdstr =~ m|^(\S+/)?usermod\s|;
+ die "$errmsg: $err";
+ } else {
+ die "command '$cmdstr' failed: $err";
+ }
+ }
+
+ return undef;
+}
+
+sub shellquote {
+ my $str = shift;
+
+ return String::ShellQuote::shell_quote($str);
+}
+
+sub cmd2string {
+ my ($cmd) = @_;
+
+ die "no arguments" if !$cmd;
+
+ return $cmd if !ref($cmd);
+
+ my @qa = ();
+ foreach my $arg (@$cmd) { push @qa, shellquote($arg); }
+
+ return join (' ', @qa);
+}
+
+1;
diff --git a/zfs-helpers/comstar b/zfs-helpers/comstar
new file mode 100644
index 0000000..67a6b43
--- /dev/null
+++ b/zfs-helpers/comstar
@@ -0,0 +1,114 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use Digest::MD5 qw(md5_hex);
+use Data::Dumper;
+use Common qw(run_command);
+
+$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
+die "please run as root\n" if $> != 0;
+
+my $POOL = $ENV{'PMXVAR_POOL'};
+my $TARGET = $ENV{'PMXVAR_TARGET'};
+my $PORTAL = $ENV{'PMXVAR_PORTAL'};
+my $SSHKEY = $ENV{'PMXVAR_SSHKEY'};
+my @ssh_opts = ('-o', 'BatchMode=yes');
+my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
+
+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' },
+ 'resize-lu' => { cmd => $stmfadmcmd, method => 'modify-lu' },
+ 'share-lu' => { cmd => $stmfadmcmd, method => 'add-view' },
+ 'get-lun-no' => { cmd => $stmfadmcmd, method => 'list-view' },
+ 'get-lun-id' => { cmd => $sbdadmcmd, method => 'list-lu' },
+ };
+
+ die "unknown command '$method'" unless exists $cmdmap->{$method};
+
+ return $cmdmap->{$method};
+};
+
+sub run_lun_command {
+ my ($method, @params) = @_;
+
+ my $msg = '';
+ my $luncmd;
+ my $target;
+ my $guid;
+ my $timeout = 10;
+
+ my $output = sub {
+ my $line = shift;
+ $msg .= "$line\n";
+ };
+
+ if ($method eq 'create-lu') {
+ my $prefix = '600144f';
+ my $digest = md5_hex($params[0]);
+ $digest =~ /(\w{7}(.*))/;
+ $guid = "$prefix$2";
+ @params = ('-p', 'wcd=false', '-p', "guid=$guid", @params);
+ } elsif ($method eq 'resize-lu') {
+ @params = ('-s', @params);
+ } elsif ($method eq 'get-lun-no') {
+ @params = ('-l', @params);
+ } elsif ($method eq 'get-lun-id') {
+ $guid = $params[0];
+ @params = undef;
+ }
+
+ my $cmdmap = $get_lun_cmd_map->($method);
+ $luncmd = $cmdmap->{cmd};
+ my $lunmethod = $cmdmap->{method};
+
+ $target = 'root@' . $PORTAL;
+
+ my $cmd = [@ssh_cmd, '-i', "$SSHKEY", $target, $luncmd, $lunmethod, @params];
+
+ Common::run_command($cmd, outfunc => $output, timeout => $timeout);
+
+ if ($method eq 'get-lun-no') {
+ my @lines = split /\n/, $msg;
+ $msg = undef;
+ foreach my $line (@lines) {
+ if ($line =~ /^\s*LUN\s*:\s*(\d+)$/) {
+ $msg = $1;
+ last;
+ }
+ }
+ } elsif ($method eq 'get-lun-id') {
+ 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;
+ }
+
+ print $msg;
+}
+
+my $command = shift;
+
+die "No command specified." if !$command;
+die "No POOL name available at env." if !$POOL;
+die "No PORTAL address available at env." if !$PORTAL;
+die "No SSH key file path available at env." if !$SSHKEY;
+
+run_lun_command($command, @ARGV);
+
+exit 0;
diff --git a/zfs-helpers/iet b/zfs-helpers/iet
new file mode 100644
index 0000000..f178694
--- /dev/null
+++ b/zfs-helpers/iet
@@ -0,0 +1,488 @@
+#!/usr/bin/perl
+
+# 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 Data::Dumper;
+use Common qw(run_command);
+
+$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
+die "please run as root\n" if $> != 0;
+
+# 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 $POOL = $ENV{'PMXVAR_POOL'};
+my $TARGET = $ENV{'PMXVAR_TARGET'};
+my $PORTAL = $ENV{'PMXVAR_PORTAL'};
+my $SSHKEY = $ENV{'PMXVAR_SSHKEY'};
+
+my @ssh_opts = ('-o', 'BatchMode=yes');
+my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
+my @scp_cmd = ('/usr/bin/scp', @ssh_opts);
+my $ietadm = '/usr/sbin/ietadm';
+
+sub get_base {
+ return '/dev';
+}
+
+my $execute_command = sub {
+ my ($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";
+ };
+
+ $target = "root@$PORTAL";
+
+ if ($exec eq 'scp') {
+ $cmd = [@scp_cmd, '-i', "$SSHKEY", $method, "$target:$params[0]"];
+ } else {
+ $cmd = [@ssh_cmd, '-i', "$SSHKEY", $target, $method, @params];
+ }
+
+ eval {
+ Common::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 ($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@' . $PORTAL;
+
+ my $cmd = [@ssh_cmd, '-i', "$SSHKEY", $target, $luncmd, $CONFIG_FILE];
+ eval {
+ Common::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 $PORTAL" if $msg eq '';
+ }
+
+ return $msg;
+};
+
+my $get_config = sub {
+ my @conf = undef;
+
+ my $config = $read_config->(undef);
+ die "Missing config file" unless $config;
+
+ $OLD_CONFIG = $config;
+
+ return $config;
+};
+
+my $parser = sub {
+ my $line = 0;
+
+ my $base = get_base;
+ my $config = $get_config->();
+ my @cfgfile = split "\n", $config;
+
+ my $cfg_target = 0;
+ foreach (@cfgfile) {
+ $line++;
+ if ($_ =~ /^\s*Target\s*([\w\-\:\.]+)\s*$/) {
+ if ($1 eq $TARGET && ! $cfg_target) {
+ # start colect info
+ die "$line: Parse error [$_]" if $SETTINGS;
+ $SETTINGS->{target} = $1;
+ $cfg_target = 1;
+ } elsif ($1 eq $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\/$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 $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->('scp', undef, $file, @params);
+ unlink $file;
+
+ die $res->{msg} unless $res->{result};
+};
+
+my $get_target_tid = sub {
+ my $proc = '/proc/net/iet/volume';
+ my $tid = undef;
+
+ my @params = ($proc);
+ my $res = $execute_command->('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 $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 ($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 ($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 ($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 ($timeout, $method, @params) = @_;
+
+ if ($list_lun->($timeout, $method, @params)) {
+ die "$params[0]: LUN exists";
+ }
+ my $lun = $params[0];
+ $lun = $make_lun->($lun);
+ my $tid = $get_target_tid->();
+ $update_config->();
+
+ my $path = "Path=$lun->{Path},Type=$lun->{Type}";
+
+ @params = ('--op', 'new', "--tid=$tid", "--lun=$lun->{lun}", '--params', $path);
+ my $res = $execute_command->('ssh', $timeout, $ietadm, @params);
+ do {
+ $free_lu_name->($lun->{lun});
+ $update_config->();
+ die $res->{msg};
+ } unless $res->{result};
+
+ return $res->{msg};
+};
+
+my $delete_lun = sub {
+ my ($timeout, $method, @params) = @_;
+ my $res = {msg => undef};
+
+ my $path = $params[0];
+ my $tid = $get_target_tid->();
+
+ foreach my $lun (@{$SETTINGS->{luns}}) {
+ if ($lun->{Path} eq $path) {
+ @params = ('--op', 'delete', "--tid=$tid", "--lun=$lun->{lun}");
+ $res = $execute_command->('ssh', $timeout, $ietadm, @params);
+ if ($res->{result}) {
+ $free_lu_name->($lun->{lun});
+ $update_config->();
+ last;
+ } else {
+ die $res->{msg};
+ }
+ }
+ }
+
+ return $res->{msg};
+};
+
+my $import_lun = sub {
+ my ($timeout, $method, @params) = @_;
+
+ return $create_lun->($timeout, $method, @params);
+};
+
+my $modify_lun = sub {
+ my ($timeout, $method, @params) = @_;
+ my $lun;
+ my $res;
+
+ my $path = $params[1];
+ my $tid = $get_target_tid->();
+
+ foreach my $cfg (@{$SETTINGS->{luns}}) {
+ if ($cfg->{Path} eq $path) {
+ $lun = $cfg;
+ last;
+ }
+ }
+
+ @params = ('--op', 'delete', "--tid=$tid", "--lun=$lun->{lun}");
+ $res = $execute_command->('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->('ssh', $timeout, $ietadm, @params);
+ die $res->{msg} unless $res->{result};
+
+ return $res->{msg};
+};
+
+my $add_view = sub {
+ my ($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 },
+ 'resize-lu' => { cmd => $modify_lun },
+ 'share-lu' => { cmd => $add_view },
+ 'get-lun-no' => { cmd => $list_view },
+ 'get-lun-id' => { cmd => $list_lun },
+ };
+
+ die "unknown command '$method'" unless exists $cmdmap->{$method};
+
+ return $cmdmap->{$method};
+};
+
+sub run_lun_command {
+ my ($timeout, $method, @params) = @_;
+
+ $parser->() unless $SETTINGS;
+ my $cmdmap = $get_lun_cmd_map->($method);
+ my $msg = $cmdmap->{cmd}->($timeout, $method, @params);
+
+ return $msg;
+}
+
+
+my $command = shift;
+
+die "No command specified." if !$command;
+die "No POOL name available at env." if !$POOL;
+die "No PORTAL address available at env." if !$PORTAL;
+die "No SSH key file path available at env." if !$SSHKEY;
+
+run_lun_command(undef, $command, @ARGV);
+
+exit 0;
+
diff --git a/zfs-helpers/istgt b/zfs-helpers/istgt
new file mode 100644
index 0000000..7757b5b
--- /dev/null
+++ b/zfs-helpers/istgt
@@ -0,0 +1,593 @@
+#!/usr/bin/perl
+
+# TODO
+# Create initial target and LUN if target is missing ?
+# Create and use list of free LUNs
+
+use strict;
+use warnings;
+use Data::Dumper;
+use Common qw(run_command);
+
+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
+);
+
+$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
+die "please run as root\n" if $> != 0;
+
+# 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 $POOL = $ENV{'PMXVAR_POOL'};
+my $TARGET = $ENV{'PMXVAR_TARGET'};
+my $PORTAL = $ENV{'PMXVAR_PORTAL'};
+my $SSHKEY = $ENV{'PMXVAR_SSHKEY'};
+
+my @ssh_opts = ('-o', 'BatchMode=yes');
+my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
+my @scp_cmd = ('/usr/bin/scp', @ssh_opts);
+
+#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 ($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@' . $PORTAL;
+
+ my $daemon = 0;
+ foreach my $config (@CONFIG_FILES) {
+ $err = undef;
+ my $cmd = [@ssh_cmd, '-i', "$SSHKEY", $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 $PORTAL" if $msg eq '';
+
+ return $msg;
+};
+
+my $get_config = sub {
+ my @conf = undef;
+
+ my $config = $read_config->(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";
+ }
+ $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 ($path) = @_;
+
+ my $target = $SETTINGS->{current};
+ die 'Maximum number of LUNs per target is 63' if scalar @{$SETTINGS->{$target}->{luns}} >= $MAX_LUNS;
+
+ my $lun = $get_lu_name->($target);
+ my $conf = {
+ lun => $lun,
+ Storage => $path,
+ Size => 'AUTO',
+ };
+ push @{$SETTINGS->{$target}->{luns}}, $conf;
+
+ return $conf->{lun};
+};
+
+my $parser = sub {
+ my $lun = undef;
+ my $line = 0;
+
+ my $config = $get_config->();
+ 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*/) {
+ #next if $2 =~ /^Option.*/;
+ $SETTINGS->{$lun}->{$1} = $2;
+ $SETTINGS->{$lun}->{$1} =~ s/^\s+|\s+$|"\s*//g;
+ } else {
+ die "$line: parse error [$_]";
+ }
+ }
+ $CONFIG .= "$_\n" unless $lun;
+ }
+
+ $CONFIG =~ s/\n$//;
+ die "$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 $TARGET) {
+ my $lu = ();
+ while ((my $key, my $val) = each(%{$SETTINGS->{"LogicalUnit$i"}})) {
+ if ($key =~ /^LUN\d+/) {
+ if ($val =~ /^Storage\s+([\w\/\-]+)\s+(\w+)/) {
+ my $storage = $1;
+ my $size = $parse_size->($2);
+ my $conf = undef;
+ if ($storage =~ /^$base\/$POOL\/([\w\-]+)$/) {
+ $conf = {
+ lun => $key,
+ Storage => $storage,
+ Size => $size,
+ };
+ }
+ 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 "$TARGET: Target not found" unless $SETTINGS->{targets} > 0;
+};
+
+my $list_lun = sub {
+ my ($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 ($timeout, $method, @params) = @_;
+ my $res = ();
+ my $file = "/tmp/config$$";
+
+ if ($list_lun->($timeout, $method, @params)) {
+ die "$params[0]: LUN exists";
+ }
+ my $lun = $params[0];
+ $lun = $make_lun->($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 ($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(undef, 'add_view', 'restart');
+ },
+ };
+
+ return $res;
+};
+
+my $import_lun = sub {
+ my ($timeout, $method, @params) = @_;
+
+ my $res = $create_lun->($timeout, $method, @params);
+
+ return $res;
+};
+
+my $add_view = sub {
+ my ($timeout, $method, @params) = @_;
+ my $cmdmap;
+
+ if (@params && $params[0] eq 'restart') {
+ @params = ('restart', '1>&2', '>', '/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 ($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->($timeout, $method, @params);
+};
+
+my $list_view = sub {
+ my ($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 },
+ 'resize-lu' => { cmd => $modify_lun },
+ 'share-lu' => { cmd => $add_view },
+ 'get-lun-no' => { cmd => $list_view },
+ 'get-lun-id' => { cmd => $list_lun },
+ };
+
+ die "unknown command '$method'" unless exists $cmdmap->{$method};
+
+ return $cmdmap->{$method};
+};
+
+sub run_lun_command {
+ my ($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@' . $PORTAL;
+
+ $parser->() 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}->($timeout, $method, @params);
+ if (ref $res) {
+ $method = $res->{method};
+ @params = @{$res->{params}};
+ if ($res->{cmd} eq 'scp') {
+ $cmd = [@scp_cmd, '-i', "$SSHKEY", $method, "$target:$params[0]"];
+ } else {
+ $cmd = [@ssh_cmd, '-i', "$SSHKEY", $target, $method, @params];
+ }
+ } else {
+ return $res;
+ }
+ } else {
+ $luncmd = $cmdmap->{cmd};
+ $method = $cmdmap->{method};
+ $cmd = [@ssh_cmd, '-i', "$SSHKEY", $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', "$SSHKEY", $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(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';
+}
+
+my $command = shift;
+
+die "No command specified." if !$command;
+die "No POOL name available at env." if !$POOL;
+die "No PORTAL address available at env." if !$PORTAL;
+die "No SSH key file path available at env." if !$SSHKEY;
+
+run_lun_command(undef, $command, @ARGV);
+
+exit 0;
--
1.7.1
More information about the pve-devel
mailing list