bump version to 7.0-13
[pve-storage.git] / PVE / Storage / LunCmd / Iet.pm
1 package PVE::Storage::LunCmd::Iet;
2
3 # iscsi storage running Debian
4 # 1) apt-get install iscsitarget iscsitarget-dkms
5 # 2) Create target like (/etc/iet/ietd.conf):
6 # Target iqn.2001-04.com.example:tank
7 # Alias tank
8 # 3) Activate daemon (/etc/default/iscsitarget)
9 # ISCSITARGET_ENABLE=true
10 # 4) service iscsitarget start
11 #
12 # On one of the proxmox nodes:
13 # 1) Login as root
14 # 2) ssh-copy-id <ip_of_iscsi_storage>
15
16 use strict;
17 use warnings;
18
19 use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach);
20
21 sub get_base;
22
23 # A logical unit can max have 16864 LUNs
24 # http://manpages.ubuntu.com/manpages/precise/man5/ietd.conf.5.html
25 my $MAX_LUNS = 16864;
26
27 my $CONFIG_FILE = '/etc/iet/ietd.conf';
28 my $DAEMON = '/usr/sbin/ietadm';
29 my $SETTINGS = undef;
30 my $CONFIG = undef;
31 my $OLD_CONFIG = undef;
32
33 my @ssh_opts = ('-o', 'BatchMode=yes');
34 my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
35 my @scp_cmd = ('/usr/bin/scp', @ssh_opts);
36 my $id_rsa_path = '/etc/pve/priv/zfs';
37 my $ietadm = '/usr/sbin/ietadm';
38
39 my $execute_command = sub {
40 my ($scfg, $exec, $timeout, $method, @params) = @_;
41
42 my $msg = '';
43 my $err = undef;
44 my $target;
45 my $cmd;
46 my $res = ();
47
48 $timeout = 10 if !$timeout;
49
50 my $output = sub {
51 my $line = shift;
52 $msg .= "$line\n";
53 };
54
55 my $errfunc = sub {
56 my $line = shift;
57 $err .= "$line";
58 };
59
60 if ($exec eq 'scp') {
61 $target = 'root@[' . $scfg->{portal} . ']';
62 $cmd = [@scp_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", '--', $method, "$target:$params[0]"];
63 } else {
64 $target = 'root@' . $scfg->{portal};
65 $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, '--', $method, @params];
66 }
67
68 eval {
69 run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
70 };
71 if ($@) {
72 $res = {
73 result => 0,
74 msg => $err,
75 }
76 } else {
77 $res = {
78 result => 1,
79 msg => $msg,
80 }
81 }
82
83 return $res;
84 };
85
86 my $read_config = sub {
87 my ($scfg, $timeout) = @_;
88
89 my $msg = '';
90 my $err = undef;
91 my $luncmd = 'cat';
92 my $target;
93 $timeout = 10 if !$timeout;
94
95 my $output = sub {
96 my $line = shift;
97 $msg .= "$line\n";
98 };
99
100 my $errfunc = sub {
101 my $line = shift;
102 $err .= "$line";
103 };
104
105 $target = 'root@' . $scfg->{portal};
106
107 my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $CONFIG_FILE];
108 eval {
109 run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
110 };
111 if ($@) {
112 die $err if ($err !~ /No such file or directory/);
113 die "No configuration found. Install iet on $scfg->{portal}" if $msg eq '';
114 }
115
116 return $msg;
117 };
118
119 my $get_config = sub {
120 my ($scfg) = @_;
121 my @conf = undef;
122
123 my $config = $read_config->($scfg, undef);
124 die "Missing config file" unless $config;
125
126 $OLD_CONFIG = $config;
127
128 return $config;
129 };
130
131 my $parser = sub {
132 my ($scfg) = @_;
133
134 my $line = 0;
135
136 my $base = get_base;
137 my $config = $get_config->($scfg);
138 my @cfgfile = split "\n", $config;
139
140 my $cfg_target = 0;
141 foreach (@cfgfile) {
142 $line++;
143 if ($_ =~ /^\s*Target\s*([\w\-\:\.]+)\s*$/) {
144 if ($1 eq $scfg->{target} && ! $cfg_target) {
145 # start colect info
146 die "$line: Parse error [$_]" if $SETTINGS;
147 $SETTINGS->{target} = $1;
148 $cfg_target = 1;
149 } elsif ($1 eq $scfg->{target} && $cfg_target) {
150 die "$line: Parse error [$_]";
151 } elsif ($cfg_target) {
152 $cfg_target = 0;
153 $CONFIG .= "$_\n";
154 } else {
155 $CONFIG .= "$_\n";
156 }
157 } else {
158 if ($cfg_target) {
159 $SETTINGS->{text} .= "$_\n";
160 next if ($_ =~ /^\s*#/ || ! $_);
161 my $option = $_;
162 if ($_ =~ /^(\w+)\s*#/) {
163 $option = $1;
164 }
165 if ($option =~ /^\s*(\w+)\s+(\w+)\s*$/) {
166 if ($1 eq 'Lun') {
167 die "$line: Parse error [$_]";
168 }
169 $SETTINGS->{$1} = $2;
170 } elsif ($option =~ /^\s*(\w+)\s+(\d+)\s+([\w\-\/=,]+)\s*$/) {
171 die "$line: Parse error [$option]" unless ($1 eq 'Lun');
172 my $conf = undef;
173 my $num = $2;
174 my @lun = split ',', $3;
175 die "$line: Parse error [$option]" unless (scalar(@lun) > 1);
176 foreach (@lun) {
177 my @lun_opt = split '=', $_;
178 die "$line: Parse error [$option]" unless (scalar(@lun_opt) == 2);
179 $conf->{$lun_opt[0]} = $lun_opt[1];
180 }
181 if ($conf->{Path} && $conf->{Path} =~ /^$base\/$scfg->{pool}\/([\w\-]+)$/) {
182 $conf->{include} = 1;
183 } else {
184 $conf->{include} = 0;
185 }
186 $conf->{lun} = $num;
187 push @{$SETTINGS->{luns}}, $conf;
188 } else {
189 die "$line: Parse error [$option]";
190 }
191 } else {
192 $CONFIG .= "$_\n";
193 }
194 }
195 }
196 $CONFIG =~ s/^\s+|\s+$|"\s*//g;
197 };
198
199 my $update_config = sub {
200 my ($scfg) = @_;
201 my $file = "/tmp/config$$";
202 my $config = '';
203
204 while ((my $option, my $value) = each(%$SETTINGS)) {
205 next if ($option eq 'include' || $option eq 'luns' || $option eq 'Path' || $option eq 'text' || $option eq 'used');
206 if ($option eq 'target') {
207 $config = "\n\nTarget " . $SETTINGS->{target} . "\n" . $config;
208 } else {
209 $config .= "\t$option\t\t\t$value\n";
210 }
211 }
212 foreach my $lun (@{$SETTINGS->{luns}}) {
213 my $lun_opt = '';
214 while ((my $option, my $value) = each(%$lun)) {
215 next if ($option eq 'include' || $option eq 'lun' || $option eq 'Path');
216 if ($lun_opt eq '') {
217 $lun_opt = $option . '=' . $value;
218 } else {
219 $lun_opt .= ',' . $option . '=' . $value;
220 }
221 }
222 $config .= "\tLun $lun->{lun} Path=$lun->{Path},$lun_opt\n";
223 }
224 open(my $fh, '>', $file) or die "Could not open file '$file' $!";
225
226 print $fh $CONFIG;
227 print $fh $config;
228 close $fh;
229
230 my @params = ($CONFIG_FILE);
231 my $res = $execute_command->($scfg, 'scp', undef, $file, @params);
232 unlink $file;
233
234 die $res->{msg} unless $res->{result};
235 };
236
237 my $get_target_tid = sub {
238 my ($scfg) = @_;
239 my $proc = '/proc/net/iet/volume';
240 my $tid = undef;
241
242 my @params = ($proc);
243 my $res = $execute_command->($scfg, 'ssh', undef, 'cat', @params);
244 die $res->{msg} unless $res->{result};
245 my @cfg = split "\n", $res->{msg};
246
247 foreach (@cfg) {
248 if ($_ =~ /^\s*tid:(\d+)\s+name:([\w\-\:\.]+)\s*$/) {
249 if ($2 && $2 eq $scfg->{target}) {
250 $tid = $1;
251 last;
252 }
253 }
254 }
255
256 return $tid;
257 };
258
259 my $get_lu_name = sub {
260 my $used = ();
261 my $i;
262
263 if (! exists $SETTINGS->{used}) {
264 for ($i = 0; $i < $MAX_LUNS; $i++) {
265 $used->{$i} = 0;
266 }
267 foreach my $lun (@{$SETTINGS->{luns}}) {
268 $used->{$lun->{lun}} = 1;
269 }
270 $SETTINGS->{used} = $used;
271 }
272
273 $used = $SETTINGS->{used};
274 for ($i = 0; $i < $MAX_LUNS; $i++) {
275 last unless $used->{$i};
276 }
277 $SETTINGS->{used}->{$i} = 1;
278
279 return $i;
280 };
281
282 my $init_lu_name = sub {
283 my $used = ();
284
285 if (! exists($SETTINGS->{used})) {
286 for (my $i = 0; $i < $MAX_LUNS; $i++) {
287 $used->{$i} = 0;
288 }
289 $SETTINGS->{used} = $used;
290 }
291 foreach my $lun (@{$SETTINGS->{luns}}) {
292 $SETTINGS->{used}->{$lun->{lun}} = 1;
293 }
294 };
295
296 my $free_lu_name = sub {
297 my ($lu_name) = @_;
298 my $new;
299
300 foreach my $lun (@{$SETTINGS->{luns}}) {
301 if ($lun->{lun} != $lu_name) {
302 push @$new, $lun;
303 }
304 }
305
306 $SETTINGS->{luns} = $new;
307 $SETTINGS->{used}->{$lu_name} = 0;
308 };
309
310 my $make_lun = sub {
311 my ($scfg, $path) = @_;
312
313 die 'Maximum number of LUNs per target is 16384' if scalar @{$SETTINGS->{luns}} >= $MAX_LUNS;
314
315 my $lun = $get_lu_name->();
316 my $conf = {
317 lun => $lun,
318 Path => $path,
319 Type => 'blockio',
320 include => 1,
321 };
322 push @{$SETTINGS->{luns}}, $conf;
323
324 return $conf;
325 };
326
327 my $list_view = sub {
328 my ($scfg, $timeout, $method, @params) = @_;
329 my $lun = undef;
330
331 my $object = $params[0];
332 foreach my $lun (@{$SETTINGS->{luns}}) {
333 next unless $lun->{include} == 1;
334 if ($lun->{Path} =~ /^$object$/) {
335 return $lun->{lun} if (defined($lun->{lun}));
336 die "$lun->{Path}: Missing LUN";
337 }
338 }
339
340 return $lun;
341 };
342
343 my $list_lun = sub {
344 my ($scfg, $timeout, $method, @params) = @_;
345 my $name = undef;
346
347 my $object = $params[0];
348 foreach my $lun (@{$SETTINGS->{luns}}) {
349 next unless $lun->{include} == 1;
350 if ($lun->{Path} =~ /^$object$/) {
351 return $lun->{Path};
352 }
353 }
354
355 return $name;
356 };
357
358 my $create_lun = sub {
359 my ($scfg, $timeout, $method, @params) = @_;
360
361 if ($list_lun->($scfg, $timeout, $method, @params)) {
362 die "$params[0]: LUN exists";
363 }
364 my $lun = $params[0];
365 $lun = $make_lun->($scfg, $lun);
366 my $tid = $get_target_tid->($scfg);
367 $update_config->($scfg);
368
369 my $path = "Path=$lun->{Path},Type=$lun->{Type}";
370
371 @params = ('--op', 'new', "--tid=$tid", "--lun=$lun->{lun}", '--params', $path);
372 my $res = $execute_command->($scfg, 'ssh', $timeout, $ietadm, @params);
373 do {
374 $free_lu_name->($lun->{lun});
375 $update_config->($scfg);
376 die $res->{msg};
377 } unless $res->{result};
378
379 return $res->{msg};
380 };
381
382 my $delete_lun = sub {
383 my ($scfg, $timeout, $method, @params) = @_;
384 my $res = {msg => undef};
385
386 my $path = $params[0];
387 my $tid = $get_target_tid->($scfg);
388
389 foreach my $lun (@{$SETTINGS->{luns}}) {
390 if ($lun->{Path} eq $path) {
391 @params = ('--op', 'delete', "--tid=$tid", "--lun=$lun->{lun}");
392 $res = $execute_command->($scfg, 'ssh', $timeout, $ietadm, @params);
393 if ($res->{result}) {
394 $free_lu_name->($lun->{lun});
395 $update_config->($scfg);
396 last;
397 } else {
398 die $res->{msg};
399 }
400 }
401 }
402
403 return $res->{msg};
404 };
405
406 my $import_lun = sub {
407 my ($scfg, $timeout, $method, @params) = @_;
408
409 return $create_lun->($scfg, $timeout, $method, @params);
410 };
411
412 my $modify_lun = sub {
413 my ($scfg, $timeout, $method, @params) = @_;
414 my $lun;
415 my $res;
416
417 my $path = $params[1];
418 my $tid = $get_target_tid->($scfg);
419
420 foreach my $cfg (@{$SETTINGS->{luns}}) {
421 if ($cfg->{Path} eq $path) {
422 $lun = $cfg;
423 last;
424 }
425 }
426
427 @params = ('--op', 'delete', "--tid=$tid", "--lun=$lun->{lun}");
428 $res = $execute_command->($scfg, 'ssh', $timeout, $ietadm, @params);
429 die $res->{msg} unless $res->{result};
430
431 $path = "Path=$lun->{Path},Type=$lun->{Type}";
432 @params = ('--op', 'new', "--tid=$tid", "--lun=$lun->{lun}", '--params', $path);
433 $res = $execute_command->($scfg, 'ssh', $timeout, $ietadm, @params);
434 die $res->{msg} unless $res->{result};
435
436 return $res->{msg};
437 };
438
439 my $add_view = sub {
440 my ($scfg, $timeout, $method, @params) = @_;
441
442 return '';
443 };
444
445 my $get_lun_cmd_map = sub {
446 my ($method) = @_;
447
448 my $cmdmap = {
449 create_lu => { cmd => $create_lun },
450 delete_lu => { cmd => $delete_lun },
451 import_lu => { cmd => $import_lun },
452 modify_lu => { cmd => $modify_lun },
453 add_view => { cmd => $add_view },
454 list_view => { cmd => $list_view },
455 list_lu => { cmd => $list_lun },
456 };
457
458 die "unknown command '$method'" unless exists $cmdmap->{$method};
459
460 return $cmdmap->{$method};
461 };
462
463 sub run_lun_command {
464 my ($scfg, $timeout, $method, @params) = @_;
465
466 $parser->($scfg) unless $SETTINGS;
467 my $cmdmap = $get_lun_cmd_map->($method);
468 my $msg = $cmdmap->{cmd}->($scfg, $timeout, $method, @params);
469
470 return $msg;
471 }
472
473 sub get_base {
474 return '/dev';
475 }
476
477 1;
478