]> git.proxmox.com Git - pve-storage.git/blame - PVE/Storage/LunCmd/Istgt.pm
Added sparse zvol support to ZFS.
[pve-storage.git] / PVE / Storage / LunCmd / Istgt.pm
CommitLineData
a62d1e99
MR
1package 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
7use strict;
8use warnings;
9use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach);
10use Data::Dumper;
11
12my @CONFIG_FILES = (
3b219e80
MR
13 '/usr/local/etc/istgt/istgt.conf', # FreeBSD, FreeNAS
14 '/var/etc/iscsi/istgt.conf' # NAS4Free
a62d1e99
MR
15);
16my @DAEMONS = (
3b219e80
MR
17 '/usr/local/etc/rc.d/istgt', # FreeBSD, FreeNAS
18 '/var/etc/rc.d/istgt' # NAS4Free
a62d1e99
MR
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
23my $MAX_LUNS = 64;
24
25my $CONFIG_FILE = undef;
26my $DAEMON = undef;
27my $SETTINGS = undef;
28my $CONFIG = undef;
29my $OLD_CONFIG = undef;
30
31my @ssh_opts = ('-o', 'BatchMode=yes');
32my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
33my @scp_cmd = ('/usr/bin/scp', @ssh_opts);
3b219e80 34my $id_rsa_path = '/etc/pve/priv/zfs';
a62d1e99
MR
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.
3b219e80 42# Updating IG is not affected until the next login.
a62d1e99
MR
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
59sub get_base;
60sub run_lun_command;
61
62my $read_config = sub {
3b219e80
MR
63 my ($scfg, $timeout, $method) = @_;
64
a62d1e99 65 my $msg = '';
3b219e80 66 my $err = undef;
a62d1e99
MR
67 my $luncmd = 'cat';
68 my $target;
69 $timeout = 10 if !$timeout;
3b219e80 70
a62d1e99
MR
71 my $output = sub {
72 my $line = shift;
3b219e80 73 $msg .= "$line\n";
a62d1e99 74 };
3b219e80
MR
75
76 my $errfunc = sub {
a62d1e99 77 my $line = shift;
3b219e80
MR
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;
a62d1e99
MR
102};
103
104my $get_config = sub {
3b219e80
MR
105 my ($scfg) = @_;
106 my @conf = undef;
a62d1e99 107
3b219e80
MR
108 my $config = $read_config->($scfg, undef, 'get_config');
109 die "Missing config file" unless $config;
a62d1e99 110
3b219e80
MR
111 $OLD_CONFIG = $config;
112
113 return $config;
a62d1e99
MR
114};
115
116my $parse_size = sub {
117 my ($text) = @_;
118
119 return 0 if !$text;
120
121 if ($text =~ m/^(\d+(\.\d+)?)([TGMK]B)?$/) {
3b219e80
MR
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;
a62d1e99 137 } elsif ($text =~ /^auto$/i) {
3b219e80
MR
138 return 'AUTO';
139 } else {
140 return 0;
a62d1e99
MR
141 }
142};
143
144my $size_with_unit = sub {
145 my ($size, $n) = (shift, 0);
146
147 return '0KB' if !$size;
148
3b219e80
MR
149 return $size if $size eq 'AUTO';
150
a62d1e99 151 if ($size =~ m/^\d+$/) {
3b219e80
MR
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";
a62d1e99
MR
160};
161
162my $lun_dumper = sub {
3b219e80
MR
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 }
177 $config .= "\n";
178
179 return $config;
a62d1e99
MR
180};
181
182my $get_lu_name = sub {
3b219e80
MR
183 my ($target) = @_;
184 my $used = ();
185 my $i;
186
187 if (! exists $SETTINGS->{$target}->{used}) {
188 for ($i = 0; $i < $MAX_LUNS; $i++) {
189 $used->{$i} = 0;
190 }
191 foreach my $lun (@{$SETTINGS->{$target}->{luns}}) {
192 $lun->{lun} =~ /^LUN(\d+)$/;
193 $used->{$1} = 1;
194 }
195 $SETTINGS->{$target}->{used} = $used;
196 }
197
198 $used = $SETTINGS->{$target}->{used};
199 for ($i = 0; $i < $MAX_LUNS; $i++) {
200 last unless $used->{$i};
201 }
202 $SETTINGS->{$target}->{used}->{$i} = 1;
203
204 return "LUN$i";
a62d1e99
MR
205};
206
207my $init_lu_name = sub {
3b219e80
MR
208 my ($target) = @_;
209 my $used = ();
210
211 if (! exists($SETTINGS->{$target}->{used})) {
212 for (my $i = 0; $i < $MAX_LUNS; $i++) {
213 $used->{$i} = 0;
214 }
215 $SETTINGS->{$target}->{used} = $used;
216 }
217 foreach my $lun (@{$SETTINGS->{$target}->{luns}}) {
218 $lun->{lun} =~ /^LUN(\d+)$/;
219 $SETTINGS->{$target}->{used}->{$1} = 1;
220 }
a62d1e99
MR
221};
222
223my $free_lu_name = sub {
3b219e80 224 my ($target, $lu_name) = @_;
a62d1e99 225
3b219e80
MR
226 $lu_name =~ /^LUN(\d+)$/;
227 $SETTINGS->{$target}->{used}->{$1} = 0;
a62d1e99
MR
228};
229
230my $make_lun = sub {
3b219e80
MR
231 my ($path) = @_;
232
233 my $target = $SETTINGS->{current};
234 die 'Maximum number of LUNs per target is 63' if scalar @{$SETTINGS->{$target}->{luns}} >= $MAX_LUNS;
235
236 my $lun = $get_lu_name->($target);
237 my $conf = {
238 lun => $lun,
239 Storage => $path,
240 Size => 'AUTO',
241 };
242 push @{$SETTINGS->{$target}->{luns}}, $conf;
243
244 return $conf->{lun};
a62d1e99
MR
245};
246
247my $parser = sub {
3b219e80
MR
248 my ($scfg) = @_;
249
250 my $lun = undef;
251 my $line = 0;
252
253 my $config = $get_config->($scfg);
254 my @cfgfile = split "\n", $config;
255
256 foreach (@cfgfile) {
257 $line++;
258 if ($_ =~ /^\s*\[(PortalGroup\d+)\]\s*/) {
259 $lun = undef;
260 $SETTINGS->{$1} = ();
261 } elsif ($_ =~ /^\s*\[(InitiatorGroup\d+)\]\s*/) {
262 $lun = undef;
263 $SETTINGS->{$1} = ();
264 } elsif ($_ =~ /^\s*PidFile\s+"?([\w\/\.]+)"?\s*/) {
265 $lun = undef;
266 $SETTINGS->{pidfile} = $1;
267 } elsif ($_ =~ /^\s*NodeBase\s+"?([\w\-\.]+)"?\s*/) {
268 $lun = undef;
269 $SETTINGS->{nodebase} = $1;
270 } elsif ($_ =~ /^\s*\[(LogicalUnit\d+)\]\s*/) {
271 $lun = $1;
272 $SETTINGS->{$lun} = ();
273 $SETTINGS->{targets}++;
274 } elsif ($lun) {
275 next if (($_ =~ /^\s*#/) || ($_ =~ /^\s*$/));
276 if ($_ =~ /^\s*(\w+)\s+(.+)\s*/) {
277 #next if $2 =~ /^Option.*/;
278 $SETTINGS->{$lun}->{$1} = $2;
279 $SETTINGS->{$lun}->{$1} =~ s/^\s+|\s+$|"\s*//g;
280 } else {
281 die "$line: parse error [$_]";
282 }
283 }
284 $CONFIG .= "$_\n" unless $lun;
285 }
286
287 $CONFIG =~ s/\n$//;
288 die "$scfg->{target}: Target not found" unless $SETTINGS->{targets};
289 my $max = $SETTINGS->{targets};
290 my $base = get_base;
291
292 for (my $i = 1; $i <= $max; $i++) {
293 my $target = $SETTINGS->{nodebase}.':'.$SETTINGS->{"LogicalUnit$i"}->{TargetName};
294 if ($target eq $scfg->{target}) {
295 my $lu = ();
296 while ((my $key, my $val) = each(%{$SETTINGS->{"LogicalUnit$i"}})) {
297 if ($key =~ /^LUN\d+/) {
298 if ($val =~ /^Storage\s+([\w\/\-]+)\s+(\w+)/) {
299 my $storage = $1;
300 my $size = $parse_size->($2);
301 my $conf = undef;
302 if ($storage =~ /^$base\/$scfg->{pool}\/([\w\-]+)$/) {
303 $conf = {
304 lun => $key,
305 Storage => $storage,
306 Size => $size,
307 };
308 }
309 push @$lu, $conf if $conf;
310 }
311 delete $SETTINGS->{"LogicalUnit$i"}->{$key};
312 }
313 }
314 $SETTINGS->{"LogicalUnit$i"}->{luns} = $lu;
315 $SETTINGS->{current} = "LogicalUnit$i";
316 $init_lu_name->("LogicalUnit$i");
317 } else {
318 $CONFIG .= $lun_dumper->("LogicalUnit$i");
319 delete $SETTINGS->{"LogicalUnit$i"};
320 $SETTINGS->{targets}--;
321 }
322 }
323 die "$scfg->{target}: Target not found" unless $SETTINGS->{targets} > 0;
a62d1e99
MR
324};
325
326my $list_lun = sub {
3b219e80
MR
327 my ($scfg, $timeout, $method, @params) = @_;
328 my $name = undef;
329
330 my $object = $params[0];
331 for my $key (keys %$SETTINGS) {
332 next unless $key =~ /^LogicalUnit\d+$/;
333 foreach my $lun (@{$SETTINGS->{$key}->{luns}}) {
334 if ($lun->{Storage} =~ /^$object$/) {
335 return $lun->{Storage};
336 }
337 }
338 }
339
340 return $name;
a62d1e99
MR
341};
342
343my $create_lun = sub {
3b219e80
MR
344 my ($scfg, $timeout, $method, @params) = @_;
345 my $res = ();
346 my $file = "/tmp/config$$";
347
348 if ($list_lun->($scfg, $timeout, $method, @params)) {
349 die "$params[0]: LUN exists";
350 }
351 my $lun = $params[0];
352 $lun = $make_lun->($lun);
353 my $config = $lun_dumper->($SETTINGS->{current});
354 open(my $fh, '>', $file) or die "Could not open file '$file' $!";
355
356 print $fh $CONFIG;
357 print $fh $config;
358 close $fh;
359 @params = ($CONFIG_FILE);
360 $res = {
361 cmd => 'scp',
362 method => $file,
363 params => \@params,
364 msg => $lun,
365 post_exe => sub {
366 unlink $file;
367 },
368 };
369
370 return $res;
a62d1e99
MR
371};
372
373my $delete_lun = sub {
3b219e80
MR
374 my ($scfg, $timeout, $method, @params) = @_;
375 my $res = ();
376 my $file = "/tmp/config$$";
377
378 my $target = $SETTINGS->{current};
379 my $luns = ();
380
381 foreach my $conf (@{$SETTINGS->{$target}->{luns}}) {
382 if ($conf->{Storage} =~ /^$params[0]$/) {
383 $free_lu_name->($target, $conf->{lun});
384 } else {
385 push @$luns, $conf;
386 }
387 }
388 $SETTINGS->{$target}->{luns} = $luns;
389
390 my $config = $lun_dumper->($SETTINGS->{current});
391 open(my $fh, '>', $file) or die "Could not open file '$file' $!";
392
393 print $fh $CONFIG;
394 print $fh $config;
395 close $fh;
396 @params = ($CONFIG_FILE);
397 $res = {
398 cmd => 'scp',
399 method => $file,
400 params => \@params,
401 post_exe => sub {
402 unlink $file;
403 run_lun_command($scfg, undef, 'add_view', 'restart');
404 },
405 };
406
407 return $res;
a62d1e99
MR
408};
409
410my $import_lun = sub {
3b219e80 411 my ($scfg, $timeout, $method, @params) = @_;
a62d1e99 412
3b219e80 413 my $res = $create_lun->($scfg, $timeout, $method, @params);
a62d1e99 414
3b219e80 415 return $res;
a62d1e99
MR
416};
417
418my $add_view = sub {
3b219e80
MR
419 my ($scfg, $timeout, $method, @params) = @_;
420 my $cmdmap;
421
422 if (@params && $params[0] eq 'restart') {
423 @params = ('restart', '1>&2', '>', '/dev/null');
424 $cmdmap = {
425 cmd => 'ssh',
426 method => $DAEMON,
427 params => \@params,
428 };
429 } else {
430 @params = ('-HUP', '$(cat '. "$SETTINGS->{pidfile})");
431 $cmdmap = {
432 cmd => 'ssh',
433 method => 'kill',
434 params => \@params,
435 };
436 }
437
438 return $cmdmap;
a62d1e99
MR
439};
440
441my $modify_lun = sub {
3b219e80
MR
442 my ($scfg, $timeout, $method, @params) = @_;
443
444 # Current SIGHUP reload limitations
445 # LU connected by the initiator can't be reloaded by SIGHUP.
446 # Until above limitation persists modifying a LUN will require
447 # a restart of the daemon breaking all current connections
448 #die 'Modify a connected LUN is not currently supported by istgt';
449 @params = ('restart', @params);
450
451 return $add_view->($scfg, $timeout, $method, @params);
a62d1e99
MR
452};
453
454my $list_view = sub {
3b219e80
MR
455 my ($scfg, $timeout, $method, @params) = @_;
456 my $lun = undef;
457
458 my $object = $params[0];
459 for my $key (keys %$SETTINGS) {
460 next unless $key =~ /^LogicalUnit\d+$/;
461 foreach my $lun (@{$SETTINGS->{$key}->{luns}}) {
462 if ($lun->{Storage} =~ /^$object$/) {
463 if ($lun->{lun} =~ /^LUN(\d+)/) {
464 return $1;
465 }
466 die "$lun->{Storage}: Missing LUN";
467 }
468 }
469 }
470
471 return $lun;
a62d1e99
MR
472};
473
474my $get_lun_cmd_map = sub {
3b219e80
MR
475 my ($method) = @_;
476
477 my $cmdmap = {
478 create_lu => { cmd => $create_lun },
479 delete_lu => { cmd => $delete_lun },
480 import_lu => { cmd => $import_lun },
481 modify_lu => { cmd => $modify_lun },
482 add_view => { cmd => $add_view },
483 list_view => { cmd => $list_view },
484 list_lu => { cmd => $list_lun },
485 };
486
487 die "unknown command '$method'" unless exists $cmdmap->{$method};
488
489 return $cmdmap->{$method};
a62d1e99
MR
490};
491
492sub run_lun_command {
3b219e80 493 my ($scfg, $timeout, $method, @params) = @_;
a62d1e99
MR
494
495 my $msg = '';
496 my $luncmd;
497 my $target;
3b219e80
MR
498 my $cmd;
499 my $res;
a62d1e99 500 $timeout = 10 if !$timeout;
3b219e80
MR
501 my $is_add_view = 0;
502
a62d1e99
MR
503 my $output = sub {
504 my $line = shift;
3b219e80
MR
505 $msg .= "$line\n";
506 };
507
508 $target = 'root@' . $scfg->{portal};
509
510 $parser->($scfg) unless $SETTINGS;
511 my $cmdmap = $get_lun_cmd_map->($method);
512 if ($method eq 'add_view') {
513 $is_add_view = 1 ;
514 $timeout = 15;
515 }
516 if (ref $cmdmap->{cmd} eq 'CODE') {
517 $res = $cmdmap->{cmd}->($scfg, $timeout, $method, @params);
518 if (ref $res) {
519 $method = $res->{method};
520 @params = @{$res->{params}};
521 if ($res->{cmd} eq 'scp') {
522 $cmd = [@scp_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $method, "$target:$params[0]"];
523 } else {
524 $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $method, @params];
525 }
526 } else {
527 return $res;
528 }
529 } else {
530 $luncmd = $cmdmap->{cmd};
531 $method = $cmdmap->{method};
532 $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $method, @params];
533 }
534
535 eval {
536 run_command($cmd, outfunc => $output, timeout => $timeout);
a62d1e99 537 };
3b219e80
MR
538 if ($@ && $is_add_view) {
539 my $err = $@;
540 if ($OLD_CONFIG) {
541 my $err1 = undef;
542 my $file = "/tmp/config$$";
543 open(my $fh, '>', $file) or die "Could not open file '$file' $!";
544 print $fh $OLD_CONFIG;
545 close $fh;
546 $cmd = [@scp_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $file, $CONFIG_FILE];
547 eval {
548 run_command($cmd, outfunc => $output, timeout => $timeout);
549 };
550 $err1 = $@ if $@;
551 unlink $file;
552 die "$err\n$err1" if $err1;
553 eval {
554 run_lun_command($scfg, undef, 'add_view', 'restart');
555 };
556 die "$err\n$@" if ($@);
557 }
558 die $err;
559 } elsif ($@) {
560 die $@;
561 } elsif ($is_add_view) {
562 $OLD_CONFIG = undef;
563 }
564
565 if ($res->{post_exe} && ref $res->{post_exe} eq 'CODE') {
566 $res->{post_exe}->();
567 }
568
569 if ($res->{msg}) {
570 $msg = $res->{msg};
571 }
a62d1e99 572
3b219e80 573 return $msg;
a62d1e99
MR
574}
575
576sub get_base {
3b219e80 577 return '/dev/zvol';
a62d1e99
MR
578}
579
3b219e80 5801;