[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