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