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