]> git.proxmox.com Git - pve-storage.git/blob - PVE/Storage/LunCmd/Istgt.pm
Fixed Istgt LUN Options handling.
[pve-storage.git] / PVE / Storage / LunCmd / Istgt.pm
1 package PVE::Storage::LunCmd::Istgt;
2
3 # TODO
4 # Create initial target and LUN if target is missing ?
5 # Create and use list of free LUNs
6
7 use strict;
8 use warnings;
9 use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach);
10 use Data::Dumper;
11
12 my @CONFIG_FILES = (
13 '/usr/local/etc/istgt/istgt.conf', # FreeBSD, FreeNAS
14 '/var/etc/iscsi/istgt.conf' # NAS4Free
15 );
16 my @DAEMONS = (
17 '/usr/local/etc/rc.d/istgt', # FreeBSD, FreeNAS
18 '/var/etc/rc.d/istgt' # NAS4Free
19 );
20
21 # A logical unit can max have 63 LUNs
22 # https://code.google.com/p/istgt/source/browse/src/istgt_lu.h#39
23 my $MAX_LUNS = 64;
24
25 my $CONFIG_FILE = undef;
26 my $DAEMON = undef;
27 my $SETTINGS = undef;
28 my $CONFIG = undef;
29 my $OLD_CONFIG = undef;
30
31 my @ssh_opts = ('-o', 'BatchMode=yes');
32 my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
33 my @scp_cmd = ('/usr/bin/scp', @ssh_opts);
34 my $id_rsa_path = '/etc/pve/priv/zfs';
35
36 #Current SIGHUP reload limitations (http://www.peach.ne.jp/archives/istgt/):
37 #
38 # The parameters other than PG, IG, and LU are not reloaded by SIGHUP.
39 # LU connected by the initiator can't be reloaded by SIGHUP.
40 # PG and IG mapped to LU can't be deleted by SIGHUP.
41 # If you delete an active LU, all connections of the LU are closed by SIGHUP.
42 # Updating IG is not affected until the next login.
43 #
44 # FreeBSD
45 # 1. Alt-F2 to change to native shell (zfsguru)
46 # 2. pw mod user root -w yes (change password for root to root)
47 # 3. vi /etc/ssh/sshd_config
48 # 4. uncomment PermitRootLogin yes
49 # 5. change PasswordAuthentication no to PasswordAuthentication yes
50 # 5. /etc/rc.d/sshd restart
51 # 6. On one of the proxmox nodes login as root and run: ssh-copy-id ip_freebsd_host
52 # 7. vi /etc/ssh/sshd_config
53 # 8. comment PermitRootLogin yes
54 # 9. change PasswordAuthentication yes to PasswordAuthentication no
55 # 10. /etc/rc.d/sshd restart
56 # 11. Reset passwd -> pw mod user root -w no
57 # 12. Alt-Ctrl-F1 to return to zfsguru shell (zfsguru)
58
59 sub get_base;
60 sub run_lun_command;
61
62 my $read_config = sub {
63 my ($scfg, $timeout, $method) = @_;
64
65 my $msg = '';
66 my $err = undef;
67 my $luncmd = 'cat';
68 my $target;
69 $timeout = 10 if !$timeout;
70
71 my $output = sub {
72 my $line = shift;
73 $msg .= "$line\n";
74 };
75
76 my $errfunc = sub {
77 my $line = shift;
78 $err .= "$line";
79 };
80
81 $target = 'root@' . $scfg->{portal};
82
83 my $daemon = 0;
84 foreach my $config (@CONFIG_FILES) {
85 $err = undef;
86 my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $config];
87 eval {
88 run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
89 };
90 do {
91 $err = undef;
92 $DAEMON = $DAEMONS[$daemon];
93 $CONFIG_FILE = $config;
94 last;
95 } unless $@;
96 $daemon++;
97 }
98 die $err if ($err && $err !~ /No such file or directory/);
99 die "No configuration found. Install istgt on $scfg->{portal}" if $msg eq '';
100
101 return $msg;
102 };
103
104 my $get_config = sub {
105 my ($scfg) = @_;
106 my @conf = undef;
107
108 my $config = $read_config->($scfg, undef, 'get_config');
109 die "Missing config file" unless $config;
110
111 $OLD_CONFIG = $config;
112
113 return $config;
114 };
115
116 my $parse_size = sub {
117 my ($text) = @_;
118
119 return 0 if !$text;
120
121 if ($text =~ m/^(\d+(\.\d+)?)([TGMK]B)?$/) {
122 my ($size, $reminder, $unit) = ($1, $2, $3);
123 return $size if !$unit;
124 if ($unit eq 'KB') {
125 $size *= 1024;
126 } elsif ($unit eq 'MB') {
127 $size *= 1024*1024;
128 } elsif ($unit eq 'GB') {
129 $size *= 1024*1024*1024;
130 } elsif ($unit eq 'TB') {
131 $size *= 1024*1024*1024*1024;
132 }
133 if ($reminder) {
134 $size = ceil($size);
135 }
136 return $size;
137 } elsif ($text =~ /^auto$/i) {
138 return 'AUTO';
139 } else {
140 return 0;
141 }
142 };
143
144 my $size_with_unit = sub {
145 my ($size, $n) = (shift, 0);
146
147 return '0KB' if !$size;
148
149 return $size if $size eq 'AUTO';
150
151 if ($size =~ m/^\d+$/) {
152 ++$n and $size /= 1024 until $size < 1024;
153 if ($size =~ /\./) {
154 return sprintf "%.2f%s", $size, ( qw[bytes KB MB GB TB] )[ $n ];
155 } else {
156 return sprintf "%d%s", $size, ( qw[bytes KB MB GB TB] )[ $n ];
157 }
158 }
159 die "$size: Not a number";
160 };
161
162 my $lun_dumper = sub {
163 my ($lun) = @_;
164 my $config = '';
165
166 $config .= "\n[$lun]\n";
167 $config .= 'TargetName ' . $SETTINGS->{$lun}->{TargetName} . "\n";
168 $config .= 'Mapping ' . $SETTINGS->{$lun}->{Mapping} . "\n";
169 $config .= 'AuthGroup ' . $SETTINGS->{$lun}->{AuthGroup} . "\n";
170 $config .= 'UnitType ' . $SETTINGS->{$lun}->{UnitType} . "\n";
171 $config .= 'QueueDepth ' . $SETTINGS->{$lun}->{QueueDepth} . "\n";
172
173 foreach my $conf (@{$SETTINGS->{$lun}->{luns}}) {
174 $config .= "$conf->{lun} Storage " . $conf->{Storage};
175 $config .= ' ' . $size_with_unit->($conf->{Size}) . "\n";
176 foreach ($conf->{options}) {
177 if ($_) {
178 $config .= "$conf->{lun} Option " . $_ . "\n";
179 }
180 }
181 }
182 $config .= "\n";
183
184 return $config;
185 };
186
187 my $get_lu_name = sub {
188 my ($target) = @_;
189 my $used = ();
190 my $i;
191
192 if (! exists $SETTINGS->{$target}->{used}) {
193 for ($i = 0; $i < $MAX_LUNS; $i++) {
194 $used->{$i} = 0;
195 }
196 foreach my $lun (@{$SETTINGS->{$target}->{luns}}) {
197 $lun->{lun} =~ /^LUN(\d+)$/;
198 $used->{$1} = 1;
199 }
200 $SETTINGS->{$target}->{used} = $used;
201 }
202
203 $used = $SETTINGS->{$target}->{used};
204 for ($i = 0; $i < $MAX_LUNS; $i++) {
205 last unless $used->{$i};
206 }
207 $SETTINGS->{$target}->{used}->{$i} = 1;
208
209 return "LUN$i";
210 };
211
212 my $init_lu_name = sub {
213 my ($target) = @_;
214 my $used = ();
215
216 if (! exists($SETTINGS->{$target}->{used})) {
217 for (my $i = 0; $i < $MAX_LUNS; $i++) {
218 $used->{$i} = 0;
219 }
220 $SETTINGS->{$target}->{used} = $used;
221 }
222 foreach my $lun (@{$SETTINGS->{$target}->{luns}}) {
223 $lun->{lun} =~ /^LUN(\d+)$/;
224 $SETTINGS->{$target}->{used}->{$1} = 1;
225 }
226 };
227
228 my $free_lu_name = sub {
229 my ($target, $lu_name) = @_;
230
231 $lu_name =~ /^LUN(\d+)$/;
232 $SETTINGS->{$target}->{used}->{$1} = 0;
233 };
234
235 my $make_lun = sub {
236 my ($scfg, $path) = @_;
237
238 my $target = $SETTINGS->{current};
239 die 'Maximum number of LUNs per target is 63' if scalar @{$SETTINGS->{$target}->{luns}} >= $MAX_LUNS;
240
241 my @options = ();
242 my $lun = $get_lu_name->($target);
243 if ($scfg->{nowritecache}) {
244 push @options, "WriteCache Disable";
245 }
246 my $conf = {
247 lun => $lun,
248 Storage => $path,
249 Size => 'AUTO',
250 options => @options,
251 };
252 push @{$SETTINGS->{$target}->{luns}}, $conf;
253
254 return $conf->{lun};
255 };
256
257 my $parser = sub {
258 my ($scfg) = @_;
259
260 my $lun = undef;
261 my $line = 0;
262
263 my $config = $get_config->($scfg);
264 my @cfgfile = split "\n", $config;
265
266 foreach (@cfgfile) {
267 $line++;
268 if ($_ =~ /^\s*\[(PortalGroup\d+)\]\s*/) {
269 $lun = undef;
270 $SETTINGS->{$1} = ();
271 } elsif ($_ =~ /^\s*\[(InitiatorGroup\d+)\]\s*/) {
272 $lun = undef;
273 $SETTINGS->{$1} = ();
274 } elsif ($_ =~ /^\s*PidFile\s+"?([\w\/\.]+)"?\s*/) {
275 $lun = undef;
276 $SETTINGS->{pidfile} = $1;
277 } elsif ($_ =~ /^\s*NodeBase\s+"?([\w\-\.]+)"?\s*/) {
278 $lun = undef;
279 $SETTINGS->{nodebase} = $1;
280 } elsif ($_ =~ /^\s*\[(LogicalUnit\d+)\]\s*/) {
281 $lun = $1;
282 $SETTINGS->{$lun} = ();
283 $SETTINGS->{targets}++;
284 } elsif ($lun) {
285 next if (($_ =~ /^\s*#/) || ($_ =~ /^\s*$/));
286 if ($_ =~ /^\s*(\w+)\s+(.+)\s*/) {
287 my $arg1 = $1;
288 $2 =~ s/^\s+|\s+$|"\s*//g;
289 if ($2 =~ /^Storage\s*(.+)/i) {
290 $SETTINGS->{$lun}->{$arg1}->{storage} = $1;
291 } elsif ($2 =~ /^Option\s*(.+)/i) {
292 push @{$SETTINGS->{$lun}->{$arg1}->{options}}, $1;
293 } else {
294 $SETTINGS->{$lun}->{$arg1} = $2;
295 }
296 } else {
297 die "$line: parse error [$_]";
298 }
299 }
300 $CONFIG .= "$_\n" unless $lun;
301 }
302
303 $CONFIG =~ s/\n$//;
304 die "$scfg->{target}: Target not found" unless $SETTINGS->{targets};
305 my $max = $SETTINGS->{targets};
306 my $base = get_base;
307
308 for (my $i = 1; $i <= $max; $i++) {
309 my $target = $SETTINGS->{nodebase}.':'.$SETTINGS->{"LogicalUnit$i"}->{TargetName};
310 if ($target eq $scfg->{target}) {
311 my $lu = ();
312 while ((my $key, my $val) = each(%{$SETTINGS->{"LogicalUnit$i"}})) {
313 if ($key =~ /^LUN\d+/) {
314 $val->{storage} =~ /^([\w\/\-]+)\s+(\w+)/;
315 my $storage = $1;
316 my $size = $parse_size->($2);
317 my $conf = undef;
318 my @options = ();
319 if ($val->{options}) {
320 @options = @{$val->{options}};
321 }
322 if ($storage =~ /^$base\/$scfg->{pool}\/([\w\-]+)$/) {
323 $conf = {
324 lun => $key,
325 Storage => $storage,
326 Size => $size,
327 options => @options,
328 }
329 }
330 push @$lu, $conf if $conf;
331 delete $SETTINGS->{"LogicalUnit$i"}->{$key};
332 }
333 }
334 $SETTINGS->{"LogicalUnit$i"}->{luns} = $lu;
335 $SETTINGS->{current} = "LogicalUnit$i";
336 $init_lu_name->("LogicalUnit$i");
337 } else {
338 $CONFIG .= $lun_dumper->("LogicalUnit$i");
339 delete $SETTINGS->{"LogicalUnit$i"};
340 $SETTINGS->{targets}--;
341 }
342 }
343 die "$scfg->{target}: Target not found" unless $SETTINGS->{targets} > 0;
344 };
345
346 my $list_lun = sub {
347 my ($scfg, $timeout, $method, @params) = @_;
348 my $name = undef;
349
350 my $object = $params[0];
351 for my $key (keys %$SETTINGS) {
352 next unless $key =~ /^LogicalUnit\d+$/;
353 foreach my $lun (@{$SETTINGS->{$key}->{luns}}) {
354 if ($lun->{Storage} =~ /^$object$/) {
355 return $lun->{Storage};
356 }
357 }
358 }
359
360 return $name;
361 };
362
363 my $create_lun = sub {
364 my ($scfg, $timeout, $method, @params) = @_;
365 my $res = ();
366 my $file = "/tmp/config$$";
367
368 if ($list_lun->($scfg, $timeout, $method, @params)) {
369 die "$params[0]: LUN exists";
370 }
371 my $lun = $params[0];
372 $lun = $make_lun->($scfg, $lun);
373 my $config = $lun_dumper->($SETTINGS->{current});
374 open(my $fh, '>', $file) or die "Could not open file '$file' $!";
375
376 print $fh $CONFIG;
377 print $fh $config;
378 close $fh;
379 @params = ($CONFIG_FILE);
380 $res = {
381 cmd => 'scp',
382 method => $file,
383 params => \@params,
384 msg => $lun,
385 post_exe => sub {
386 unlink $file;
387 },
388 };
389
390 return $res;
391 };
392
393 my $delete_lun = sub {
394 my ($scfg, $timeout, $method, @params) = @_;
395 my $res = ();
396 my $file = "/tmp/config$$";
397
398 my $target = $SETTINGS->{current};
399 my $luns = ();
400
401 foreach my $conf (@{$SETTINGS->{$target}->{luns}}) {
402 if ($conf->{Storage} =~ /^$params[0]$/) {
403 $free_lu_name->($target, $conf->{lun});
404 } else {
405 push @$luns, $conf;
406 }
407 }
408 $SETTINGS->{$target}->{luns} = $luns;
409
410 my $config = $lun_dumper->($SETTINGS->{current});
411 open(my $fh, '>', $file) or die "Could not open file '$file' $!";
412
413 print $fh $CONFIG;
414 print $fh $config;
415 close $fh;
416 @params = ($CONFIG_FILE);
417 $res = {
418 cmd => 'scp',
419 method => $file,
420 params => \@params,
421 post_exe => sub {
422 unlink $file;
423 run_lun_command($scfg, undef, 'add_view', 'restart');
424 },
425 };
426
427 return $res;
428 };
429
430 my $import_lun = sub {
431 my ($scfg, $timeout, $method, @params) = @_;
432
433 my $res = $create_lun->($scfg, $timeout, $method, @params);
434
435 return $res;
436 };
437
438 my $add_view = sub {
439 my ($scfg, $timeout, $method, @params) = @_;
440 my $cmdmap;
441
442 if (@params && $params[0] eq 'restart') {
443 @params = ('onerestart', '>&', '/dev/null');
444 $cmdmap = {
445 cmd => 'ssh',
446 method => $DAEMON,
447 params => \@params,
448 };
449 } else {
450 @params = ('-HUP', '`cat '. "$SETTINGS->{pidfile}`");
451 $cmdmap = {
452 cmd => 'ssh',
453 method => 'kill',
454 params => \@params,
455 };
456 }
457
458 return $cmdmap;
459 };
460
461 my $modify_lun = sub {
462 my ($scfg, $timeout, $method, @params) = @_;
463
464 # Current SIGHUP reload limitations
465 # LU connected by the initiator can't be reloaded by SIGHUP.
466 # Until above limitation persists modifying a LUN will require
467 # a restart of the daemon breaking all current connections
468 #die 'Modify a connected LUN is not currently supported by istgt';
469 @params = ('restart', @params);
470
471 return $add_view->($scfg, $timeout, $method, @params);
472 };
473
474 my $list_view = sub {
475 my ($scfg, $timeout, $method, @params) = @_;
476 my $lun = undef;
477
478 my $object = $params[0];
479 for my $key (keys %$SETTINGS) {
480 next unless $key =~ /^LogicalUnit\d+$/;
481 foreach my $lun (@{$SETTINGS->{$key}->{luns}}) {
482 if ($lun->{Storage} =~ /^$object$/) {
483 if ($lun->{lun} =~ /^LUN(\d+)/) {
484 return $1;
485 }
486 die "$lun->{Storage}: Missing LUN";
487 }
488 }
489 }
490
491 return $lun;
492 };
493
494 my $get_lun_cmd_map = sub {
495 my ($method) = @_;
496
497 my $cmdmap = {
498 create_lu => { cmd => $create_lun },
499 delete_lu => { cmd => $delete_lun },
500 import_lu => { cmd => $import_lun },
501 modify_lu => { cmd => $modify_lun },
502 add_view => { cmd => $add_view },
503 list_view => { cmd => $list_view },
504 list_lu => { cmd => $list_lun },
505 };
506
507 die "unknown command '$method'" unless exists $cmdmap->{$method};
508
509 return $cmdmap->{$method};
510 };
511
512 sub run_lun_command {
513 my ($scfg, $timeout, $method, @params) = @_;
514
515 my $msg = '';
516 my $luncmd;
517 my $target;
518 my $cmd;
519 my $res;
520 $timeout = 10 if !$timeout;
521 my $is_add_view = 0;
522
523 my $output = sub {
524 my $line = shift;
525 $msg .= "$line\n";
526 };
527
528 $target = 'root@' . $scfg->{portal};
529
530 $parser->($scfg) unless $SETTINGS;
531 my $cmdmap = $get_lun_cmd_map->($method);
532 if ($method eq 'add_view') {
533 $is_add_view = 1 ;
534 $timeout = 15;
535 }
536 if (ref $cmdmap->{cmd} eq 'CODE') {
537 $res = $cmdmap->{cmd}->($scfg, $timeout, $method, @params);
538 if (ref $res) {
539 $method = $res->{method};
540 @params = @{$res->{params}};
541 if ($res->{cmd} eq 'scp') {
542 $cmd = [@scp_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $method, "$target:$params[0]"];
543 } else {
544 $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $method, @params];
545 }
546 } else {
547 return $res;
548 }
549 } else {
550 $luncmd = $cmdmap->{cmd};
551 $method = $cmdmap->{method};
552 $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $method, @params];
553 }
554
555 eval {
556 run_command($cmd, outfunc => $output, timeout => $timeout);
557 };
558 if ($@ && $is_add_view) {
559 my $err = $@;
560 if ($OLD_CONFIG) {
561 my $err1 = undef;
562 my $file = "/tmp/config$$";
563 open(my $fh, '>', $file) or die "Could not open file '$file' $!";
564 print $fh $OLD_CONFIG;
565 close $fh;
566 $cmd = [@scp_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $file, $CONFIG_FILE];
567 eval {
568 run_command($cmd, outfunc => $output, timeout => $timeout);
569 };
570 $err1 = $@ if $@;
571 unlink $file;
572 die "$err\n$err1" if $err1;
573 eval {
574 run_lun_command($scfg, undef, 'add_view', 'restart');
575 };
576 die "$err\n$@" if ($@);
577 }
578 die $err;
579 } elsif ($@) {
580 die $@;
581 } elsif ($is_add_view) {
582 $OLD_CONFIG = undef;
583 }
584
585 if ($res->{post_exe} && ref $res->{post_exe} eq 'CODE') {
586 $res->{post_exe}->();
587 }
588
589 if ($res->{msg}) {
590 $msg = $res->{msg};
591 }
592
593 return $msg;
594 }
595
596 sub get_base {
597 return '/dev/zvol';
598 }
599
600 1;