]> git.proxmox.com Git - pve-storage.git/blame - src/PVE/Storage/ISCSIPlugin.pm
fix #254: iscsi: add support for multipath targets
[pve-storage.git] / src / PVE / Storage / ISCSIPlugin.pm
CommitLineData
1dc01b9f
DM
1package PVE::Storage::ISCSIPlugin;
2
3use strict;
4use warnings;
c29bad0d 5
1dc01b9f
DM
6use File::stat;
7use IO::Dir;
8use IO::File;
c29bad0d 9
1dc01b9f 10use PVE::JSONSchema qw(get_standard_option);
c29bad0d
TL
11use PVE::Storage::Plugin;
12use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach $IPV4RE $IPV6RE);
1dc01b9f
DM
13
14use base qw(PVE::Storage::Plugin);
15
16# iscsi helper function
17
18my $ISCSIADM = '/usr/bin/iscsiadm';
19$ISCSIADM = undef if ! -X $ISCSIADM;
20
90c1b10c
YK
21# Example: 192.168.122.252:3260,1 iqn.2003-01.org.linux-iscsi.proxmox-nfs.x8664:sn.00567885ba8f
22my $ISCSI_TARGET_RE = qr/^((?:$IPV4RE|\[$IPV6RE\]):\d+)\,\S+\s+(\S+)\s*$/;
23
1dc01b9f
DM
24sub check_iscsi_support {
25 my $noerr = shift;
26
27 if (!$ISCSIADM) {
28 my $msg = "no iscsi support - please install open-iscsi";
29 if ($noerr) {
30 warn "warning: $msg\n";
31 return 0;
32 }
33
34 die "error: $msg\n";
35 }
36
37 return 1;
38}
39
40sub iscsi_session_list {
41
42 check_iscsi_support ();
43
44 my $cmd = [$ISCSIADM, '--mode', 'session'];
45
46 my $res = {};
47
7acee37a
DM
48 eval {
49 run_command($cmd, errmsg => 'iscsi session scan failed', outfunc => sub {
50 my $line = shift;
90c1b10c
YK
51 # example: tcp: [1] 192.168.122.252:3260,1 iqn.2003-01.org.linux-iscsi.proxmox-nfs.x8664:sn.00567885ba8f (non-flash)
52 if ($line =~ m/^tcp:\s+\[(\S+)\]\s+((?:$IPV4RE|\[$IPV6RE\]):\d+)\,\S+\s+(\S+)\s+\S+?\s*$/) {
53 my ($session_id, $portal, $target) = ($1, $2, $3);
7acee37a 54 # there can be several sessions per target (multipath)
90c1b10c
YK
55 my %session = ( session_id => $session_id, portal => $portal );
56 push @{$res->{$target}}, \%session;
7acee37a
DM
57 }
58 });
59 };
60 if (my $err = $@) {
61 die $err if $err !~ m/: No active sessions.$/i;
62 }
1dc01b9f
DM
63
64 return $res;
65}
66
67sub iscsi_test_portal {
68 my ($portal) = @_;
69
1689e627
WB
70 my ($server, $port) = PVE::Tools::parse_host_and_port($portal);
71 return 0 if !$server;
72 return PVE::Network::tcp_ping($server, $port || 3260, 2);
1dc01b9f
DM
73}
74
90c1b10c
YK
75sub iscsi_portals {
76 my ($target, $portal_in) = @_;
1dc01b9f
DM
77
78 check_iscsi_support ();
79
90c1b10c
YK
80 my $res = [];
81 my $cmd = [$ISCSIADM, '--mode', 'node'];
82 eval {
83 run_command($cmd, outfunc => sub {
84 my $line = shift;
1dc01b9f 85
90c1b10c
YK
86 if ($line =~ $ISCSI_TARGET_RE) {
87 my ($portal, $portal_target) = ($1, $2);
88 if ($portal_target eq $target) {
89 push @{$res}, $portal;
90 }
91 }
92 });
93 };
1dc01b9f 94
90c1b10c
YK
95 if ($@) {
96 warn $@;
97 return [ $portal_in ];
98 }
99
100 return $res;
101}
102
103sub iscsi_discovery {
104 my ($portals) = @_;
105
106 check_iscsi_support ();
107
108 my $res = {};
109 for my $portal ($portals->@*) {
110 next if !iscsi_test_portal($portal); # fixme: raise exception here?
111
112 my $cmd = [$ISCSIADM, '--mode', 'discovery', '--type', 'sendtargets', '--portal', $portal];
113 eval {
114 run_command($cmd, outfunc => sub {
115 my $line = shift;
116
117 if ($line =~ $ISCSI_TARGET_RE) {
118 my ($portal, $target) = ($1, $2);
119 # one target can have more than one portal (multipath)
120 # and sendtargets should return all of them in single call
121 push @{$res->{$target}}, $portal;
122 }
123 });
124 };
125
126 # In case of multipath we can stop after receiving targets from any available portal
127 last if scalar(keys %$res) > 0;
128 }
1dc01b9f
DM
129
130 return $res;
131}
132
133sub iscsi_login {
90c1b10c 134 my ($target, $portals) = @_;
1dc01b9f 135
a1e09e49 136 check_iscsi_support();
1dc01b9f 137
90c1b10c 138 eval { iscsi_discovery($portals); };
1dc01b9f
DM
139 warn $@ if $@;
140
a1e09e49 141 run_command([$ISCSIADM, '--mode', 'node', '--targetname', $target, '--login']);
1dc01b9f
DM
142}
143
144sub iscsi_logout {
90c1b10c 145 my ($target) = @_;
1dc01b9f 146
a1e09e49 147 check_iscsi_support();
1dc01b9f 148
a1e09e49 149 run_command([$ISCSIADM, '--mode', 'node', '--targetname', $target, '--logout']);
1dc01b9f
DM
150}
151
152my $rescan_filename = "/var/run/pve-iscsi-rescan.lock";
153
154sub iscsi_session_rescan {
155 my $session_list = shift;
156
157 check_iscsi_support();
158
159 my $rstat = stat($rescan_filename);
160
161 if (!$rstat) {
162 if (my $fh = IO::File->new($rescan_filename, "a")) {
163 utime undef, undef, $fh;
164 close($fh);
165 }
166 } else {
167 my $atime = $rstat->atime;
168 my $tdiff = time() - $atime;
169 # avoid frequent rescans
170 return if !($tdiff < 0 || $tdiff > 10);
171 utime undef, undef, $rescan_filename;
172 }
173
174 foreach my $session (@$session_list) {
90c1b10c 175 my $cmd = [$ISCSIADM, '--mode', 'session', '--sid', $session->{session_id}, '--rescan'];
1dc01b9f
DM
176 eval { run_command($cmd, outfunc => sub {}); };
177 warn $@ if $@;
178 }
179}
180
181sub load_stable_scsi_paths {
182
183 my $stable_paths = {};
184
185 my $stabledir = "/dev/disk/by-id";
186
187 if (my $dh = IO::Dir->new($stabledir)) {
245b6517 188 foreach my $tmp (sort $dh->read) {
1dc01b9f 189 # exclude filenames with part in name (same disk but partitions)
4f430a43 190 # use only filenames with scsi(with multipath i have the same device
1dc01b9f 191 # with dm-uuid-mpath , dm-name and scsi in name)
245b6517 192 if($tmp !~ m/-part\d+$/ && ($tmp =~ m/^scsi-/ || $tmp =~ m/^dm-uuid-mpath-/)) {
1dc01b9f
DM
193 my $path = "$stabledir/$tmp";
194 my $bdevdest = readlink($path);
195 if ($bdevdest && $bdevdest =~ m|^../../([^/]+)|) {
196 $stable_paths->{$1}=$tmp;
197 }
198 }
199 }
200 $dh->close;
201 }
202 return $stable_paths;
203}
204
205sub iscsi_device_list {
206
207 my $res = {};
208
209 my $dirname = '/sys/class/iscsi_session';
210
211 my $stable_paths = load_stable_scsi_paths();
212
213 dir_glob_foreach($dirname, 'session(\d+)', sub {
214 my ($ent, $session) = @_;
215
216 my $target = file_read_firstline("$dirname/$ent/targetname");
217 return if !$target;
218
219 my (undef, $host) = dir_glob_regex("$dirname/$ent/device", 'target(\d+):.*');
220 return if !defined($host);
221
222 dir_glob_foreach("/sys/bus/scsi/devices", "$host:" . '(\d+):(\d+):(\d+)', sub {
223 my ($tmp, $channel, $id, $lun) = @_;
224
225 my $type = file_read_firstline("/sys/bus/scsi/devices/$tmp/type");
226 return if !defined($type) || $type ne '0'; # list disks only
227
228 my $bdev;
229 if (-d "/sys/bus/scsi/devices/$tmp/block") { # newer kernels
230 (undef, $bdev) = dir_glob_regex("/sys/bus/scsi/devices/$tmp/block/", '([A-Za-z]\S*)');
231 } else {
232 (undef, $bdev) = dir_glob_regex("/sys/bus/scsi/devices/$tmp", 'block:(\S+)');
233 }
234 return if !$bdev;
235
4f430a43
AL
236 #check multipath
237 if (-d "/sys/block/$bdev/holders") {
1dc01b9f
DM
238 my $multipathdev = dir_glob_regex("/sys/block/$bdev/holders", '[A-Za-z]\S*');
239 $bdev = $multipathdev if $multipathdev;
240 }
241
242 my $blockdev = $stable_paths->{$bdev};
243 return if !$blockdev;
244
245 my $size = file_read_firstline("/sys/block/$bdev/size");
246 return if !$size;
247
248 my $volid = "$channel.$id.$lun.$blockdev";
249
250 $res->{$target}->{$volid} = {
4f430a43
AL
251 'format' => 'raw',
252 'size' => int($size * 512),
1dc01b9f
DM
253 'vmid' => 0, # not assigned to any vm
254 'channel' => int($channel),
255 'id' => int($id),
256 'lun' => int($lun),
257 };
258
4f430a43 259 #print "TEST: $target $session $host,$bus,$tg,$lun $blockdev\n";
1dc01b9f
DM
260 });
261
262 });
263
264 return $res;
265}
266
267# Configuration
268
269sub type {
270 return 'iscsi';
271}
272
273sub plugindata {
274 return {
275 content => [ {images => 1, none => 1}, { images => 1 }],
5da48ca6 276 select_existing => 1,
1dc01b9f
DM
277 };
278}
279
280sub properties {
281 return {
282 target => {
283 description => "iSCSI target.",
284 type => 'string',
285 },
286 portal => {
287 description => "iSCSI portal (IP or DNS name with optional port).",
288 type => 'string', format => 'pve-storage-portal-dns',
289 },
290 };
291}
292
293sub options {
294 return {
295 portal => { fixed => 1 },
296 target => { fixed => 1 },
297 nodes => { optional => 1},
298 disable => { optional => 1},
299 content => { optional => 1},
9edb99a5 300 bwlimit => { optional => 1 },
1dc01b9f
DM
301 };
302}
303
304# Storage implementation
305
306sub parse_volname {
307 my ($class, $volname) = @_;
308
309 if ($volname =~ m!^\d+\.\d+\.\d+\.(\S+)$!) {
7800e84d 310 return ('images', $1, undef, undef, undef, undef, 'raw');
1dc01b9f
DM
311 }
312
313 die "unable to parse iscsi volume name '$volname'\n";
314}
315
452e3ee7 316sub filesystem_path {
e67069eb
DM
317 my ($class, $scfg, $volname, $snapname) = @_;
318
319 die "snapshot is not possible on iscsi storage\n" if defined($snapname);
1dc01b9f
DM
320
321 my ($vtype, $name, $vmid) = $class->parse_volname($volname);
4f430a43 322
1dc01b9f
DM
323 my $path = "/dev/disk/by-id/$name";
324
5521b580 325 return wantarray ? ($path, $vmid, $vtype) : $path;
1dc01b9f
DM
326}
327
5eab0272
DM
328sub create_base {
329 my ($class, $storeid, $scfg, $volname) = @_;
330
331 die "can't create base images in iscsi storage\n";
332}
333
334sub clone_image {
f236eaf8 335 my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
5eab0272
DM
336
337 die "can't clone images in iscsi storage\n";
338}
339
1dc01b9f
DM
340sub alloc_image {
341 my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
342
343 die "can't allocate space in iscsi storage\n";
344}
345
346sub free_image {
32437ed2 347 my ($class, $storeid, $scfg, $volname, $isBase) = @_;
1dc01b9f
DM
348
349 die "can't free space in iscsi storage\n";
350}
351
3587acc8
DC
352# list all luns regardless of set content_types, since we need it for
353# listing in the gui and we can only have images anyway
354sub list_volumes {
355 my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
356
357 my $res = $class->list_images($storeid, $scfg, $vmid);
358
359 for my $item (@$res) {
360 $item->{content} = 'images'; # we only have images
361 }
362
363 return $res;
364}
365
1dc01b9f
DM
366sub list_images {
367 my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
368
369 my $res = [];
370
371 $cache->{iscsi_devices} = iscsi_device_list() if !$cache->{iscsi_devices};
372
373 # we have no owner for iscsi devices
374
375 my $target = $scfg->{target};
376
377 if (my $dat = $cache->{iscsi_devices}->{$target}) {
378
379 foreach my $volname (keys %$dat) {
380
381 my $volid = "$storeid:$volname";
382
383 if ($vollist) {
384 my $found = grep { $_ eq $volid } @$vollist;
385 next if !$found;
386 } else {
387 # we have no owner for iscsi devices
388 next if defined($vmid);
389 }
390
391 my $info = $dat->{$volname};
392 $info->{volid} = $volid;
393
394 push @$res, $info;
395 }
396 }
397
398 return $res;
399}
400
553c9b21
TL
401sub iscsi_session {
402 my ($cache, $target) = @_;
403 $cache->{iscsi_sessions} = iscsi_session_list() if !$cache->{iscsi_sessions};
404 return $cache->{iscsi_sessions}->{$target};
405}
406
1dc01b9f
DM
407sub status {
408 my ($class, $storeid, $scfg, $cache) = @_;
409
553c9b21
TL
410 my $session = iscsi_session($cache, $scfg->{target});
411 my $active = defined($session) ? 1 : 0;
1dc01b9f
DM
412
413 return (0, 0, 0, $active);
414}
415
416sub activate_storage {
417 my ($class, $storeid, $scfg, $cache) = @_;
418
419 return if !check_iscsi_support(1);
420
90c1b10c
YK
421 my $sessions = iscsi_session($cache, $scfg->{target});
422 my $portals = iscsi_portals($scfg->{target}, $scfg->{portal});
423 my $do_login = !defined($sessions);
1dc01b9f 424
90c1b10c
YK
425 if (!$do_login) {
426 # We should check that sessions for all portals are available
427 my $session_portals = [ map { $_->{portal} } (@$sessions) ];
428
429 for my $portal (@$portals) {
430 if (!grep(/^\Q$portal\E$/, @$session_portals)) {
431 $do_login = 1;
432 last;
433 }
434 }
435 }
436
437 if ($do_login) {
438 eval { iscsi_login($scfg->{target}, $portals); };
1dc01b9f
DM
439 warn $@ if $@;
440 } else {
441 # make sure we get all devices
90c1b10c 442 iscsi_session_rescan($sessions);
1dc01b9f
DM
443 }
444}
445
446sub deactivate_storage {
447 my ($class, $storeid, $scfg, $cache) = @_;
448
449 return if !check_iscsi_support(1);
450
553c9b21 451 if (defined(iscsi_session($cache, $scfg->{target}))) {
90c1b10c 452 iscsi_logout($scfg->{target});
1dc01b9f
DM
453 }
454}
455
03b5bfdf
AD
456sub check_connection {
457 my ($class, $storeid, $scfg) = @_;
458
90c1b10c
YK
459 my $portals = iscsi_portals($scfg->{target}, $scfg->{portal});
460
461 for my $portal (@$portals) {
462 my $result = iscsi_test_portal($portal);
463 return $result if $result;
464 }
465
466 return 0;
03b5bfdf
AD
467}
468
0244a7b9
AD
469sub volume_resize {
470 my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
471 die "volume resize is not possible on iscsi device";
472}
473
852a55f7
AD
474sub volume_has_feature {
475 my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
476
7e8c6888
AD
477 my $features = {
478 copy => { current => 1},
479 };
480
481 my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
482 $class->parse_volname($volname);
483
484 my $key = undef;
485 if($snapname){
2c5a7097 486 $key = 'snap';
7e8c6888
AD
487 }else{
488 $key = $isBase ? 'base' : 'current';
489 }
490 return 1 if $features->{$feature}->{$key};
491
852a55f7
AD
492 return undef;
493}
494
a548fd48 495
1dc01b9f 4961;