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 use strict; use warnings; use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach); use Data::Dumper; 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;