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