]> git.proxmox.com Git - pve-storage.git/blob - PVE/Storage/LunCmd/Istgt.pm
Fixed command substitution and output redirection in Istgt module to work in csh...
[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 next if $2 =~ /^Option.*/;
288 $SETTINGS->{$lun}->{$1} = $2;
289 $SETTINGS->{$lun}->{$1} =~ s/^\s+|\s+$|"\s*//g;
290 } else {
291 die "$line: parse error [$_]";
292 }
293 }
294 $CONFIG .= "$_\n" unless $lun;
295 }
296
297 $CONFIG =~ s/\n$//;
298 die "$scfg->{target}: Target not found" unless $SETTINGS->{targets};
299 my $max = $SETTINGS->{targets};
300 my $base = get_base;
301
302 for (my $i = 1; $i <= $max; $i++) {
303 my $target = $SETTINGS->{nodebase}.':'.$SETTINGS->{"LogicalUnit$i"}->{TargetName};
304 if ($target eq $scfg->{target}) {
305 my $lu = ();
306 while ((my $key, my $val) = each(%{$SETTINGS->{"LogicalUnit$i"}})) {
307 if ($key =~ /^LUN\d+/) {
308 if ($val =~ /^Storage\s+([\w\/\-]+)\s+(\w+)/) {
309 my $storage = $1;
310 my $size = $parse_size->($2);
311 my $conf = undef;
312 if ($storage =~ /^$base\/$scfg->{pool}\/([\w\-]+)$/) {
313 $conf = {
314 lun => $key,
315 Storage => $storage,
316 Size => $size,
317 };
318 }
319 push @$lu, $conf if $conf;
320 }
321 delete $SETTINGS->{"LogicalUnit$i"}->{$key};
322 }
323 }
324 $SETTINGS->{"LogicalUnit$i"}->{luns} = $lu;
325 $SETTINGS->{current} = "LogicalUnit$i";
326 $init_lu_name->("LogicalUnit$i");
327 } else {
328 $CONFIG .= $lun_dumper->("LogicalUnit$i");
329 delete $SETTINGS->{"LogicalUnit$i"};
330 $SETTINGS->{targets}--;
331 }
332 }
333 die "$scfg->{target}: Target not found" unless $SETTINGS->{targets} > 0;
334 };
335
336 my $list_lun = sub {
337 my ($scfg, $timeout, $method, @params) = @_;
338 my $name = undef;
339
340 my $object = $params[0];
341 for my $key (keys %$SETTINGS) {
342 next unless $key =~ /^LogicalUnit\d+$/;
343 foreach my $lun (@{$SETTINGS->{$key}->{luns}}) {
344 if ($lun->{Storage} =~ /^$object$/) {
345 return $lun->{Storage};
346 }
347 }
348 }
349
350 return $name;
351 };
352
353 my $create_lun = sub {
354 my ($scfg, $timeout, $method, @params) = @_;
355 my $res = ();
356 my $file = "/tmp/config$$";
357
358 if ($list_lun->($scfg, $timeout, $method, @params)) {
359 die "$params[0]: LUN exists";
360 }
361 my $lun = $params[0];
362 $lun = $make_lun->($scfg, $lun);
363 my $config = $lun_dumper->($SETTINGS->{current});
364 open(my $fh, '>', $file) or die "Could not open file '$file' $!";
365
366 print $fh $CONFIG;
367 print $fh $config;
368 close $fh;
369 @params = ($CONFIG_FILE);
370 $res = {
371 cmd => 'scp',
372 method => $file,
373 params => \@params,
374 msg => $lun,
375 post_exe => sub {
376 unlink $file;
377 },
378 };
379
380 return $res;
381 };
382
383 my $delete_lun = sub {
384 my ($scfg, $timeout, $method, @params) = @_;
385 my $res = ();
386 my $file = "/tmp/config$$";
387
388 my $target = $SETTINGS->{current};
389 my $luns = ();
390
391 foreach my $conf (@{$SETTINGS->{$target}->{luns}}) {
392 if ($conf->{Storage} =~ /^$params[0]$/) {
393 $free_lu_name->($target, $conf->{lun});
394 } else {
395 push @$luns, $conf;
396 }
397 }
398 $SETTINGS->{$target}->{luns} = $luns;
399
400 my $config = $lun_dumper->($SETTINGS->{current});
401 open(my $fh, '>', $file) or die "Could not open file '$file' $!";
402
403 print $fh $CONFIG;
404 print $fh $config;
405 close $fh;
406 @params = ($CONFIG_FILE);
407 $res = {
408 cmd => 'scp',
409 method => $file,
410 params => \@params,
411 post_exe => sub {
412 unlink $file;
413 run_lun_command($scfg, undef, 'add_view', 'restart');
414 },
415 };
416
417 return $res;
418 };
419
420 my $import_lun = sub {
421 my ($scfg, $timeout, $method, @params) = @_;
422
423 my $res = $create_lun->($scfg, $timeout, $method, @params);
424
425 return $res;
426 };
427
428 my $add_view = sub {
429 my ($scfg, $timeout, $method, @params) = @_;
430 my $cmdmap;
431
432 if (@params && $params[0] eq 'restart') {
433 @params = ('onerestart', '>&', '/dev/null');
434 $cmdmap = {
435 cmd => 'ssh',
436 method => $DAEMON,
437 params => \@params,
438 };
439 } else {
440 @params = ('-HUP', '`cat '. "$SETTINGS->{pidfile}`");
441 $cmdmap = {
442 cmd => 'ssh',
443 method => 'kill',
444 params => \@params,
445 };
446 }
447
448 return $cmdmap;
449 };
450
451 my $modify_lun = sub {
452 my ($scfg, $timeout, $method, @params) = @_;
453
454 # Current SIGHUP reload limitations
455 # LU connected by the initiator can't be reloaded by SIGHUP.
456 # Until above limitation persists modifying a LUN will require
457 # a restart of the daemon breaking all current connections
458 #die 'Modify a connected LUN is not currently supported by istgt';
459 @params = ('restart', @params);
460
461 return $add_view->($scfg, $timeout, $method, @params);
462 };
463
464 my $list_view = sub {
465 my ($scfg, $timeout, $method, @params) = @_;
466 my $lun = undef;
467
468 my $object = $params[0];
469 for my $key (keys %$SETTINGS) {
470 next unless $key =~ /^LogicalUnit\d+$/;
471 foreach my $lun (@{$SETTINGS->{$key}->{luns}}) {
472 if ($lun->{Storage} =~ /^$object$/) {
473 if ($lun->{lun} =~ /^LUN(\d+)/) {
474 return $1;
475 }
476 die "$lun->{Storage}: Missing LUN";
477 }
478 }
479 }
480
481 return $lun;
482 };
483
484 my $get_lun_cmd_map = sub {
485 my ($method) = @_;
486
487 my $cmdmap = {
488 create_lu => { cmd => $create_lun },
489 delete_lu => { cmd => $delete_lun },
490 import_lu => { cmd => $import_lun },
491 modify_lu => { cmd => $modify_lun },
492 add_view => { cmd => $add_view },
493 list_view => { cmd => $list_view },
494 list_lu => { cmd => $list_lun },
495 };
496
497 die "unknown command '$method'" unless exists $cmdmap->{$method};
498
499 return $cmdmap->{$method};
500 };
501
502 sub run_lun_command {
503 my ($scfg, $timeout, $method, @params) = @_;
504
505 my $msg = '';
506 my $luncmd;
507 my $target;
508 my $cmd;
509 my $res;
510 $timeout = 10 if !$timeout;
511 my $is_add_view = 0;
512
513 my $output = sub {
514 my $line = shift;
515 $msg .= "$line\n";
516 };
517
518 $target = 'root@' . $scfg->{portal};
519
520 $parser->($scfg) unless $SETTINGS;
521 my $cmdmap = $get_lun_cmd_map->($method);
522 if ($method eq 'add_view') {
523 $is_add_view = 1 ;
524 $timeout = 15;
525 }
526 if (ref $cmdmap->{cmd} eq 'CODE') {
527 $res = $cmdmap->{cmd}->($scfg, $timeout, $method, @params);
528 if (ref $res) {
529 $method = $res->{method};
530 @params = @{$res->{params}};
531 if ($res->{cmd} eq 'scp') {
532 $cmd = [@scp_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $method, "$target:$params[0]"];
533 } else {
534 $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $method, @params];
535 }
536 } else {
537 return $res;
538 }
539 } else {
540 $luncmd = $cmdmap->{cmd};
541 $method = $cmdmap->{method};
542 $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $method, @params];
543 }
544
545 eval {
546 run_command($cmd, outfunc => $output, timeout => $timeout);
547 };
548 if ($@ && $is_add_view) {
549 my $err = $@;
550 if ($OLD_CONFIG) {
551 my $err1 = undef;
552 my $file = "/tmp/config$$";
553 open(my $fh, '>', $file) or die "Could not open file '$file' $!";
554 print $fh $OLD_CONFIG;
555 close $fh;
556 $cmd = [@scp_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $file, $CONFIG_FILE];
557 eval {
558 run_command($cmd, outfunc => $output, timeout => $timeout);
559 };
560 $err1 = $@ if $@;
561 unlink $file;
562 die "$err\n$err1" if $err1;
563 eval {
564 run_lun_command($scfg, undef, 'add_view', 'restart');
565 };
566 die "$err\n$@" if ($@);
567 }
568 die $err;
569 } elsif ($@) {
570 die $@;
571 } elsif ($is_add_view) {
572 $OLD_CONFIG = undef;
573 }
574
575 if ($res->{post_exe} && ref $res->{post_exe} eq 'CODE') {
576 $res->{post_exe}->();
577 }
578
579 if ($res->{msg}) {
580 $msg = $res->{msg};
581 }
582
583 return $msg;
584 }
585
586 sub get_base {
587 return '/dev/zvol';
588 }
589
590 1;