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