]> git.proxmox.com Git - pve-storage.git/blame_incremental - PVE/Storage/ZFSPlugin.pm
zfsdir: cleanup zfs_parse_size
[pve-storage.git] / PVE / Storage / ZFSPlugin.pm
... / ...
CommitLineData
1package PVE::Storage::ZFSPlugin;
2
3use strict;
4use warnings;
5use IO::File;
6use POSIX;
7use PVE::Tools qw(run_command);
8use PVE::Storage::ZFSDirPlugin;
9
10use base qw(PVE::Storage::ZFSDirPlugin);
11use PVE::Storage::LunCmd::Comstar;
12use PVE::Storage::LunCmd::Istgt;
13use PVE::Storage::LunCmd::Iet;
14
15my @ssh_opts = ('-o', 'BatchMode=yes');
16my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
17my $id_rsa_path = '/etc/pve/priv/zfs';
18
19my $lun_cmds = {
20 create_lu => 1,
21 delete_lu => 1,
22 import_lu => 1,
23 modify_lu => 1,
24 add_view => 1,
25 list_view => 1,
26 list_lu => 1,
27};
28
29my $zfs_unknown_scsi_provider = sub {
30 my ($provider) = @_;
31
32 die "$provider: unknown iscsi provider. Available [comstar, istgt, iet]";
33};
34
35my $zfs_get_base = sub {
36 my ($scfg) = @_;
37
38 if ($scfg->{iscsiprovider} eq 'comstar') {
39 return PVE::Storage::LunCmd::Comstar::get_base;
40 } elsif ($scfg->{iscsiprovider} eq 'istgt') {
41 return PVE::Storage::LunCmd::Istgt::get_base;
42 } elsif ($scfg->{iscsiprovider} eq 'iet') {
43 return PVE::Storage::LunCmd::Iet::get_base;
44 } else {
45 $zfs_unknown_scsi_provider->($scfg->{iscsiprovider});
46 }
47};
48
49sub zfs_request {
50 my ($scfg, $timeout, $method, @params) = @_;
51
52 my $cmdmap;
53 my $zfscmd;
54 my $target;
55 my $msg;
56
57 $timeout = 5 if !$timeout;
58
59 if ($lun_cmds->{$method}) {
60 if ($scfg->{iscsiprovider} eq 'comstar') {
61 $msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
62 } elsif ($scfg->{iscsiprovider} eq 'istgt') {
63 $msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
64 } elsif ($scfg->{iscsiprovider} eq 'iet') {
65 $msg = PVE::Storage::LunCmd::Iet::run_lun_command($scfg, $timeout, $method, @params);
66 } else {
67 $zfs_unknown_scsi_provider->($scfg->{iscsiprovider});
68 }
69 } else {
70 if ($method eq 'zpool_list') {
71 $zfscmd = 'zpool';
72 $method = 'list',
73 } else {
74 $zfscmd = 'zfs';
75 }
76
77 $target = 'root@' . $scfg->{portal};
78
79 my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $zfscmd, $method, @params];
80
81 $msg = '';
82
83 my $output = sub {
84 my $line = shift;
85 $msg .= "$line\n";
86 };
87
88 run_command($cmd, outfunc => $output, timeout => $timeout);
89 }
90
91 return $msg;
92}
93
94sub zfs_get_pool_stats {
95 my ($scfg) = @_;
96
97 my $available = 0;
98 my $used = 0;
99
100 my $text = zfs_request($scfg, undef, 'get', '-o', 'value', '-Hp',
101 'available,used', $scfg->{pool});
102
103 my @lines = split /\n/, $text;
104
105 if($lines[0] =~ /^(\d+)$/) {
106 $available = $1;
107 }
108
109 if($lines[1] =~ /^(\d+)$/) {
110 $used = $1;
111 }
112
113 return ($available, $used);
114}
115
116sub zfs_parse_zvol_list {
117 my ($text) = @_;
118
119 my $list = ();
120
121 return $list if !$text;
122
123 my @lines = split /\n/, $text;
124 foreach my $line (@lines) {
125 if ($line =~ /^(.+)\s+([a-zA-Z0-9\.]+|\-)\s+(.+)$/) {
126 my $zvol = {};
127 my @parts = split /\//, $1;
128 my $name = pop @parts;
129 my $pool = join('/', @parts);
130
131 if ($pool !~ /^rpool$/) {
132 next unless $name =~ m!^(\w+)-(\d+)-(\w+)-(\d+)$!;
133 $name = $pool . '/' . $name;
134 } else {
135 next;
136 }
137
138 $zvol->{pool} = $pool;
139 $zvol->{name} = $name;
140 $zvol->{size} = zfs_parse_size($2);
141 if ($3 !~ /^-$/) {
142 $zvol->{origin} = $3;
143 }
144 push @$list, $zvol;
145 }
146 }
147
148 return $list;
149}
150
151sub zfs_get_lu_name {
152 my ($scfg, $zvol) = @_;
153 my $object;
154
155 my $base = $zfs_get_base->($scfg);
156 if ($zvol =~ /^.+\/.+/) {
157 $object = "$base/$zvol";
158 } else {
159 $object = "$base/$scfg->{pool}/$zvol";
160 }
161
162 my $lu_name = zfs_request($scfg, undef, 'list_lu', $object);
163
164 return $lu_name if $lu_name;
165
166 die "Could not find lu_name for zvol $zvol";
167}
168
169sub zfs_get_zvol_size {
170 my ($scfg, $zvol) = @_;
171
172 my $text = zfs_request($scfg, undef, 'get', '-Hp', 'volsize', "$scfg->{pool}/$zvol");
173
174 if($text =~ /volsize\s(\d+)/){
175 return $1;
176 }
177
178 die "Could not get zvol size";
179}
180
181sub zfs_add_lun_mapping_entry {
182 my ($scfg, $zvol, $guid) = @_;
183
184 if (! defined($guid)) {
185 $guid = zfs_get_lu_name($scfg, $zvol);
186 }
187
188 zfs_request($scfg, undef, 'add_view', $guid);
189}
190
191sub zfs_delete_lu {
192 my ($scfg, $zvol) = @_;
193
194 my $guid = zfs_get_lu_name($scfg, $zvol);
195
196 zfs_request($scfg, undef, 'delete_lu', $guid);
197}
198
199sub zfs_create_lu {
200 my ($scfg, $zvol) = @_;
201
202 my $base = $zfs_get_base->($scfg);
203 my $guid = zfs_request($scfg, undef, 'create_lu', "$base/$scfg->{pool}/$zvol");
204
205 return $guid;
206}
207
208sub zfs_import_lu {
209 my ($scfg, $zvol) = @_;
210
211 my $base = $zfs_get_base->($scfg);
212 zfs_request($scfg, undef, 'import_lu', "$base/$scfg->{pool}/$zvol");
213}
214
215sub zfs_resize_lu {
216 my ($scfg, $zvol, $size) = @_;
217
218 my $guid = zfs_get_lu_name($scfg, $zvol);
219
220 zfs_request($scfg, undef, 'modify_lu', "${size}K", $guid);
221}
222
223sub zfs_create_zvol {
224 my ($scfg, $zvol, $size) = @_;
225
226 my $sparse = '';
227 if ($scfg->{sparse}) {
228 $sparse = '-s';
229 }
230
231 zfs_request($scfg, undef, 'create', $sparse, '-b', $scfg->{blocksize}, '-V', "${size}k", "$scfg->{pool}/$zvol");
232}
233
234sub zfs_delete_zvol {
235 my ($scfg, $zvol) = @_;
236
237 zfs_request($scfg, undef, 'destroy', '-r', "$scfg->{pool}/$zvol");
238}
239
240sub zfs_get_lun_number {
241 my ($scfg, $guid) = @_;
242
243 die "could not find lun_number for guid $guid" if !$guid;
244
245 return zfs_request($scfg, undef, 'list_view', $guid);
246}
247
248sub zfs_list_zvol {
249 my ($scfg) = @_;
250
251 my $text = zfs_request($scfg, 10, 'list', '-o', 'name,volsize,origin', '-t', 'volume', '-Hr');
252 my $zvols = zfs_parse_zvol_list($text);
253 return undef if !$zvols;
254
255 my $list = ();
256 foreach my $zvol (@$zvols) {
257 my @values = split('/', $zvol->{name});
258
259 my $image = pop @values;
260 my $pool = join('/', @values);
261
262 next if $image !~ m/^((vm|base)-(\d+)-\S+)$/;
263 my $owner = $3;
264
265 my $parent = $zvol->{origin};
266 if($zvol->{origin} && $zvol->{origin} =~ m/^$scfg->{pool}\/(\S+)$/){
267 $parent = $1;
268 }
269
270 $list->{$pool}->{$image} = {
271 name => $image,
272 size => $zvol->{size},
273 parent => $parent,
274 format => 'raw',
275 vmid => $owner
276 };
277 }
278
279 return $list;
280}
281
282# Configuration
283
284sub type {
285 return 'zfs';
286}
287
288sub plugindata {
289 return {
290 content => [ {images => 1}, { images => 1 }],
291 };
292}
293
294sub properties {
295 return {
296 iscsiprovider => {
297 description => "iscsi provider",
298 type => 'string',
299 },
300 blocksize => {
301 description => "block size",
302 type => 'string',
303 },
304 # this will disable write caching on comstar and istgt.
305 # it is not implemented for iet. iet blockio always operates with
306 # writethrough caching when not in readonly mode
307 nowritecache => {
308 description => "disable write caching on the target",
309 type => 'boolean',
310 },
311 sparse => {
312 description => "use sparse volumes",
313 type => 'boolean',
314 },
315 comstar_tg => {
316 description => "target group for comstar views",
317 type => 'string',
318 },
319 comstar_hg => {
320 description => "host group for comstar views",
321 type => 'string',
322 },
323 };
324}
325
326sub options {
327 return {
328 nodes => { optional => 1 },
329 disable => { optional => 1 },
330 portal => { fixed => 1 },
331 target => { fixed => 1 },
332 pool => { fixed => 1 },
333 blocksize => { fixed => 1 },
334 iscsiprovider => { fixed => 1 },
335 nowritecache => { optional => 1 },
336 sparse => { optional => 1 },
337 comstar_hg => { optional => 1 },
338 comstar_tg => { optional => 1 },
339 content => { optional => 1 },
340 };
341}
342
343# Storage implementation
344
345sub parse_volname {
346 my ($class, $volname) = @_;
347
348 if ($volname =~ m/^(((base|vm)-(\d+)-\S+)\/)?((base)?(vm)?-(\d+)-\S+)$/) {
349 return ('images', $5, $8, $2, $4, $6);
350 }
351
352 die "unable to parse zfs volume name '$volname'\n";
353}
354
355sub path {
356 my ($class, $scfg, $volname) = @_;
357
358 my ($vtype, $name, $vmid) = $class->parse_volname($volname);
359
360 my $target = $scfg->{target};
361 my $portal = $scfg->{portal};
362
363 my $guid = zfs_get_lu_name($scfg, $name);
364 my $lun = zfs_get_lun_number($scfg, $guid);
365
366 my $path = "iscsi://$portal/$target/$lun";
367
368 return ($path, $vmid, $vtype);
369}
370
371my $find_free_diskname = sub {
372 my ($storeid, $scfg, $vmid) = @_;
373
374 my $name = undef;
375 my $volumes = zfs_list_zvol($scfg);
376
377 my $disk_ids = {};
378 my $dat = $volumes->{$scfg->{pool}};
379
380 foreach my $image (keys %$dat) {
381 my $volname = $dat->{$image}->{name};
382 if ($volname =~ m/(vm|base)-$vmid-disk-(\d+)/){
383 $disk_ids->{$2} = 1;
384 }
385 }
386
387 for (my $i = 1; $i < 100; $i++) {
388 if (!$disk_ids->{$i}) {
389 return "vm-$vmid-disk-$i";
390 }
391 }
392
393 die "unable to allocate an image name for VM $vmid in storage '$storeid'\n";
394};
395
396sub create_base {
397 my ($class, $storeid, $scfg, $volname) = @_;
398
399 my $snap = '__base__';
400
401 my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
402 $class->parse_volname($volname);
403
404 die "create_base not possible with base image\n" if $isBase;
405
406 my $newname = $name;
407 $newname =~ s/^vm-/base-/;
408
409 my $newvolname = $basename ? "$basename/$newname" : "$newname";
410
411 zfs_delete_lu($scfg, $name);
412 zfs_request($scfg, undef, 'rename', "$scfg->{pool}/$name", "$scfg->{pool}/$newname");
413
414 my $guid = zfs_create_lu($scfg, $newname);
415 zfs_add_lun_mapping_entry($scfg, $newname, $guid);
416
417 my $running = undef; #fixme : is create_base always offline ?
418
419 $class->volume_snapshot($scfg, $storeid, $newname, $snap, $running);
420
421 return $newvolname;
422}
423
424sub clone_image {
425 my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
426
427 $snap ||= '__base__';
428
429 my ($vtype, $basename, $basevmid, undef, undef, $isBase) =
430 $class->parse_volname($volname);
431
432 die "clone_image only works on base images\n" if !$isBase;
433
434 my $name = &$find_free_diskname($storeid, $scfg, $vmid);
435
436 warn "clone $volname: $basename to $name\n";
437
438 zfs_request($scfg, undef, 'clone', "$scfg->{pool}/$basename\@$snap", "$scfg->{pool}/$name");
439
440 my $guid = zfs_create_lu($scfg, $name);
441 zfs_add_lun_mapping_entry($scfg, $name, $guid);
442
443 return $name;
444}
445
446sub alloc_image {
447 my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
448
449 die "unsupported format '$fmt'" if $fmt ne 'raw';
450
451 die "illegal name '$name' - sould be 'vm-$vmid-*'\n"
452 if $name && $name !~ m/^vm-$vmid-/;
453
454 $name = &$find_free_diskname($storeid, $scfg, $vmid) if !$name;
455
456 zfs_create_zvol($scfg, $name, $size);
457 my $guid = zfs_create_lu($scfg, $name);
458 zfs_add_lun_mapping_entry($scfg, $name, $guid);
459
460 return $name;
461}
462
463sub free_image {
464 my ($class, $storeid, $scfg, $volname, $isBase) = @_;
465
466 my ($vtype, $name, $vmid) = $class->parse_volname($volname);
467
468 zfs_delete_lu($scfg, $name);
469 eval {
470 zfs_delete_zvol($scfg, $name);
471 };
472 do {
473 my $err = $@;
474 my $guid = zfs_create_lu($scfg, $name);
475 zfs_add_lun_mapping_entry($scfg, $name, $guid);
476 die $err;
477 } if $@;
478
479 return undef;
480}
481
482sub list_images {
483 my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
484
485 $cache->{zfs} = zfs_list_zvol($scfg) if !$cache->{zfs};
486 my $zfspool = $scfg->{pool};
487 my $res = [];
488
489 if (my $dat = $cache->{zfs}->{$zfspool}) {
490
491 foreach my $image (keys %$dat) {
492
493 my $volname = $dat->{$image}->{name};
494 my $parent = $dat->{$image}->{parent};
495
496 my $volid = undef;
497 if ($parent && $parent =~ m/^(\S+)@(\S+)$/) {
498 my ($basename) = ($1);
499 $volid = "$storeid:$basename/$volname";
500 } else {
501 $volid = "$storeid:$volname";
502 }
503
504 my $owner = $dat->{$volname}->{vmid};
505 if ($vollist) {
506 my $found = grep { $_ eq $volid } @$vollist;
507 next if !$found;
508 } else {
509 next if defined ($vmid) && ($owner ne $vmid);
510 }
511
512 my $info = $dat->{$volname};
513 $info->{volid} = $volid;
514 push @$res, $info;
515 }
516 }
517
518 return $res;
519}
520
521sub status {
522 my ($class, $storeid, $scfg, $cache) = @_;
523
524 my $total = 0;
525 my $free = 0;
526 my $used = 0;
527 my $active = 0;
528
529 eval {
530 ($free, $used) = zfs_get_pool_stats($scfg);
531 $active = 1;
532 $total = $free + $used;
533 };
534 warn $@ if $@;
535
536 return ($total, $free, $used, $active);
537}
538
539sub activate_storage {
540 my ($class, $storeid, $scfg, $cache) = @_;
541 return 1;
542}
543
544sub deactivate_storage {
545 my ($class, $storeid, $scfg, $cache) = @_;
546 return 1;
547}
548
549sub activate_volume {
550 my ($class, $storeid, $scfg, $volname, $exclusive, $cache) = @_;
551 return 1;
552}
553
554sub deactivate_volume {
555 my ($class, $storeid, $scfg, $volname, $exclusive, $cache) = @_;
556 return 1;
557}
558
559sub volume_size_info {
560 my ($class, $scfg, $storeid, $volname, $timeout) = @_;
561
562 return zfs_get_zvol_size($scfg, $volname);
563}
564
565sub volume_resize {
566 my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
567
568 my $new_size = ($size/1024);
569
570 zfs_request($scfg, undef, 'set', 'volsize=' . $new_size . 'k', "$scfg->{pool}/$volname");
571 zfs_resize_lu($scfg, $volname, $new_size);
572}
573
574sub volume_snapshot {
575 my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
576
577 zfs_request($scfg, undef, 'snapshot', "$scfg->{pool}/$volname\@$snap");
578}
579
580sub volume_snapshot_rollback {
581 my ($class, $scfg, $storeid, $volname, $snap) = @_;
582
583 # abort rollback if snapshot is not the latest
584 my @params = ('-t', 'snapshot', '-o', 'name', '-s', 'creation');
585 my $text = zfs_request($scfg, undef, 'list', @params);
586 my @snapshots = split(/\n/, $text);
587 my $recentsnap = undef;
588 foreach (@snapshots) {
589 if (/$scfg->{pool}\/$volname/) {
590 s/^.*@//;
591 $recentsnap = $_;
592 }
593 }
594 if ($snap ne $recentsnap) {
595 die "cannot rollback, more recent snapshots exist\n";
596 }
597
598 zfs_delete_lu($scfg, $volname);
599
600 zfs_request($scfg, undef, 'rollback', "$scfg->{pool}/$volname\@$snap");
601
602 zfs_import_lu($scfg, $volname);
603
604 zfs_add_lun_mapping_entry($scfg, $volname);
605}
606
607sub volume_snapshot_delete {
608 my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
609
610 zfs_request($scfg, undef, 'destroy', "$scfg->{pool}/$volname\@$snap");
611}
612
613sub volume_has_feature {
614 my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
615
616 my $features = {
617 snapshot => { current => 1, snap => 1},
618 clone => { base => 1},
619 template => { current => 1},
620 copy => { base => 1, current => 1},
621 };
622
623 my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
624 $class->parse_volname($volname);
625
626 my $key = undef;
627
628 if ($snapname) {
629 $key = 'snap';
630 } else {
631 $key = $isBase ? 'base' : 'current';
632 }
633
634 return 1 if $features->{$feature}->{$key};
635
636 return undef;
637}
638
6391;