]> git.proxmox.com Git - pve-storage.git/blob - PVE/Storage/ZFSPlugin.pm
69c6ecc0c669c3ad69f7ae5cb77dd011ee98ed26
[pve-storage.git] / PVE / Storage / ZFSPlugin.pm
1 package PVE::Storage::ZFSPlugin;
2
3 use strict;
4 use warnings;
5 use IO::File;
6 use POSIX;
7 use PVE::Tools qw(run_command);
8 use PVE::Storage::ZFSDirPlugin;
9
10 use base qw(PVE::Storage::ZFSDirPlugin);
11 use PVE::Storage::LunCmd::Comstar;
12 use PVE::Storage::LunCmd::Istgt;
13 use PVE::Storage::LunCmd::Iet;
14
15 my @ssh_opts = ('-o', 'BatchMode=yes');
16 my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
17 my $id_rsa_path = '/etc/pve/priv/zfs';
18
19 my $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
29 my $zfs_unknown_scsi_provider = sub {
30 my ($provider) = @_;
31
32 die "$provider: unknown iscsi provider. Available [comstar, istgt, iet]";
33 };
34
35 my $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
49 sub zfs_request {
50 my ($class, $scfg, $timeout, $method, @params) = @_;
51
52 $timeout = 5 if !$timeout;
53
54 my $msg = '';
55
56 if ($lun_cmds->{$method}) {
57 if ($scfg->{iscsiprovider} eq 'comstar') {
58 $msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
59 } elsif ($scfg->{iscsiprovider} eq 'istgt') {
60 $msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
61 } elsif ($scfg->{iscsiprovider} eq 'iet') {
62 $msg = PVE::Storage::LunCmd::Iet::run_lun_command($scfg, $timeout, $method, @params);
63 } else {
64 $zfs_unknown_scsi_provider->($scfg->{iscsiprovider});
65 }
66 } else {
67
68 my $target = 'root@' . $scfg->{portal};
69
70 my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target];
71
72 if ($method eq 'zpool_list') {
73 push @$cmd, 'zpool', 'list';
74 } else {
75 push @$cmd, 'zfs', $method;
76 }
77
78 push @$cmd, @params;
79
80 my $output = sub {
81 my $line = shift;
82 $msg .= "$line\n";
83 };
84
85 run_command($cmd, outfunc => $output, timeout => $timeout);
86 }
87
88 return $msg;
89 }
90
91 sub zfs_get_lu_name {
92 my ($class, $scfg, $zvol) = @_;
93 my $object;
94
95 my $base = $zfs_get_base->($scfg);
96 if ($zvol =~ /^.+\/.+/) {
97 $object = "$base/$zvol";
98 } else {
99 $object = "$base/$scfg->{pool}/$zvol";
100 }
101
102 my $lu_name = $class->zfs_request($scfg, undef, 'list_lu', $object);
103
104 return $lu_name if $lu_name;
105
106 die "Could not find lu_name for zvol $zvol";
107 }
108
109 sub zfs_add_lun_mapping_entry {
110 my ($class, $scfg, $zvol, $guid) = @_;
111
112 if (! defined($guid)) {
113 $guid = $class->zfs_get_lu_name($scfg, $zvol);
114 }
115
116 $class->zfs_request($scfg, undef, 'add_view', $guid);
117 }
118
119 sub zfs_delete_lu {
120 my ($class, $scfg, $zvol) = @_;
121
122 my $guid = $class->zfs_get_lu_name($scfg, $zvol);
123
124 $class->zfs_request($scfg, undef, 'delete_lu', $guid);
125 }
126
127 sub zfs_create_lu {
128 my ($class, $scfg, $zvol) = @_;
129
130 my $base = $zfs_get_base->($scfg);
131 my $guid = $class->zfs_request($scfg, undef, 'create_lu', "$base/$scfg->{pool}/$zvol");
132
133 return $guid;
134 }
135
136 sub zfs_import_lu {
137 my ($class, $scfg, $zvol) = @_;
138
139 my $base = $zfs_get_base->($scfg);
140 $class->zfs_request($scfg, undef, 'import_lu', "$base/$scfg->{pool}/$zvol");
141 }
142
143 sub zfs_resize_lu {
144 my ($class, $scfg, $zvol, $size) = @_;
145
146 my $guid = $class->zfs_get_lu_name($scfg, $zvol);
147
148 $class->zfs_request($scfg, undef, 'modify_lu', "${size}K", $guid);
149 }
150
151 sub zfs_get_lun_number {
152 my ($class, $scfg, $guid) = @_;
153
154 die "could not find lun_number for guid $guid" if !$guid;
155
156 return $class->zfs_request($scfg, undef, 'list_view', $guid);
157 }
158
159 # Configuration
160
161 sub type {
162 return 'zfs';
163 }
164
165 sub plugindata {
166 return {
167 content => [ {images => 1}, { images => 1 }],
168 };
169 }
170
171 sub properties {
172 return {
173 iscsiprovider => {
174 description => "iscsi provider",
175 type => 'string',
176 },
177 # this will disable write caching on comstar and istgt.
178 # it is not implemented for iet. iet blockio always operates with
179 # writethrough caching when not in readonly mode
180 nowritecache => {
181 description => "disable write caching on the target",
182 type => 'boolean',
183 },
184 comstar_tg => {
185 description => "target group for comstar views",
186 type => 'string',
187 },
188 comstar_hg => {
189 description => "host group for comstar views",
190 type => 'string',
191 },
192 };
193 }
194
195 sub options {
196 return {
197 nodes => { optional => 1 },
198 disable => { optional => 1 },
199 portal => { fixed => 1 },
200 target => { fixed => 1 },
201 pool => { fixed => 1 },
202 blocksize => { fixed => 1 },
203 iscsiprovider => { fixed => 1 },
204 nowritecache => { optional => 1 },
205 sparse => { optional => 1 },
206 comstar_hg => { optional => 1 },
207 comstar_tg => { optional => 1 },
208 content => { optional => 1 },
209 };
210 }
211
212 # Storage implementation
213
214 sub parse_volname {
215 my ($class, $volname) = @_;
216
217 if ($volname =~ m/^(((base|vm)-(\d+)-\S+)\/)?((base)?(vm)?-(\d+)-\S+)$/) {
218 return ('images', $5, $8, $2, $4, $6);
219 }
220
221 die "unable to parse zfs volume name '$volname'\n";
222 }
223
224 sub path {
225 my ($class, $scfg, $volname) = @_;
226
227 my ($vtype, $name, $vmid) = $class->parse_volname($volname);
228
229 my $target = $scfg->{target};
230 my $portal = $scfg->{portal};
231
232 my $guid = $class->zfs_get_lu_name($scfg, $name);
233 my $lun = $class->zfs_get_lun_number($scfg, $guid);
234
235 my $path = "iscsi://$portal/$target/$lun";
236
237 return ($path, $vmid, $vtype);
238 }
239
240 sub create_base {
241 my ($class, $storeid, $scfg, $volname) = @_;
242
243 my $snap = '__base__';
244
245 my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
246 $class->parse_volname($volname);
247
248 die "create_base not possible with base image\n" if $isBase;
249
250 my $newname = $name;
251 $newname =~ s/^vm-/base-/;
252
253 my $newvolname = $basename ? "$basename/$newname" : "$newname";
254
255 $class->zfs_delete_lu($scfg, $name);
256 $class->zfs_request($scfg, undef, 'rename', "$scfg->{pool}/$name", "$scfg->{pool}/$newname");
257
258 my $guid = $class->zfs_create_lu($scfg, $newname);
259 $class->zfs_add_lun_mapping_entry($scfg, $newname, $guid);
260
261 my $running = undef; #fixme : is create_base always offline ?
262
263 $class->volume_snapshot($scfg, $storeid, $newname, $snap, $running);
264
265 return $newvolname;
266 }
267
268 sub clone_image {
269 my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
270
271 $snap ||= '__base__';
272
273 my ($vtype, $basename, $basevmid, undef, undef, $isBase) =
274 $class->parse_volname($volname);
275
276 die "clone_image only works on base images\n" if !$isBase;
277
278 my $name = $class->zfs_find_free_diskname($storeid, $scfg, $vmid);
279
280 warn "clone $volname: $basename to $name\n";
281
282 $class->zfs_request($scfg, undef, 'clone', "$scfg->{pool}/$basename\@$snap", "$scfg->{pool}/$name");
283
284 my $guid = $class->zfs_create_lu($scfg, $name);
285 $class->zfs_add_lun_mapping_entry($scfg, $name, $guid);
286
287 return $name;
288 }
289
290 sub alloc_image {
291 my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
292
293 die "unsupported format '$fmt'" if $fmt ne 'raw';
294
295 die "illegal name '$name' - sould be 'vm-$vmid-*'\n"
296 if $name && $name !~ m/^vm-$vmid-/;
297
298 $name = $class->zfs_find_free_diskname($storeid, $scfg, $vmid) if !$name;
299
300 $class->zfs_create_zvol($scfg, $name, $size);
301 my $guid = $class->zfs_create_lu($scfg, $name);
302 $class->zfs_add_lun_mapping_entry($scfg, $name, $guid);
303
304 return $name;
305 }
306
307 sub free_image {
308 my ($class, $storeid, $scfg, $volname, $isBase) = @_;
309
310 my ($vtype, $name, $vmid) = $class->parse_volname($volname);
311
312 $class->zfs_delete_lu($scfg, $name);
313 eval {
314 $class->zfs_delete_zvol($scfg, $name);
315 };
316 do {
317 my $err = $@;
318 my $guid = $class->zfs_create_lu($scfg, $name);
319 $class->zfs_add_lun_mapping_entry($scfg, $name, $guid);
320 die $err;
321 } if $@;
322
323 return undef;
324 }
325
326 sub list_images {
327 my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
328
329 $cache->{zfs} = $class->zfs_list_zvol($scfg) if !$cache->{zfs};
330 my $zfspool = $scfg->{pool};
331 my $res = [];
332
333 if (my $dat = $cache->{zfs}->{$zfspool}) {
334
335 foreach my $image (keys %$dat) {
336
337 my $volname = $dat->{$image}->{name};
338 my $parent = $dat->{$image}->{parent};
339
340 my $volid = undef;
341 if ($parent && $parent =~ m/^(\S+)@(\S+)$/) {
342 my ($basename) = ($1);
343 $volid = "$storeid:$basename/$volname";
344 } else {
345 $volid = "$storeid:$volname";
346 }
347
348 my $owner = $dat->{$volname}->{vmid};
349 if ($vollist) {
350 my $found = grep { $_ eq $volid } @$vollist;
351 next if !$found;
352 } else {
353 next if defined ($vmid) && ($owner ne $vmid);
354 }
355
356 my $info = $dat->{$volname};
357 $info->{volid} = $volid;
358 push @$res, $info;
359 }
360 }
361
362 return $res;
363 }
364
365 sub status {
366 my ($class, $storeid, $scfg, $cache) = @_;
367
368 my $total = 0;
369 my $free = 0;
370 my $used = 0;
371 my $active = 0;
372
373 eval {
374 ($free, $used) = $class->zfs_get_pool_stats($scfg);
375 $active = 1;
376 $total = $free + $used;
377 };
378 warn $@ if $@;
379
380 return ($total, $free, $used, $active);
381 }
382
383 sub activate_storage {
384 my ($class, $storeid, $scfg, $cache) = @_;
385 return 1;
386 }
387
388 sub deactivate_storage {
389 my ($class, $storeid, $scfg, $cache) = @_;
390 return 1;
391 }
392
393 sub activate_volume {
394 my ($class, $storeid, $scfg, $volname, $exclusive, $cache) = @_;
395 return 1;
396 }
397
398 sub deactivate_volume {
399 my ($class, $storeid, $scfg, $volname, $exclusive, $cache) = @_;
400 return 1;
401 }
402
403 sub volume_size_info {
404 my ($class, $scfg, $storeid, $volname, $timeout) = @_;
405
406 return $class->zfs_get_zvol_size($scfg, $volname);
407 }
408
409 sub volume_resize {
410 my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
411
412 my $new_size = ($size/1024);
413
414 $class->zfs_request($scfg, undef, 'set', 'volsize=' . $new_size . 'k', "$scfg->{pool}/$volname");
415 $class->zfs_resize_lu($scfg, $volname, $new_size);
416 }
417
418 sub volume_snapshot {
419 my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
420
421 $class->zfs_request($scfg, undef, 'snapshot', "$scfg->{pool}/$volname\@$snap");
422 }
423
424 sub volume_snapshot_rollback {
425 my ($class, $scfg, $storeid, $volname, $snap) = @_;
426
427 # abort rollback if snapshot is not the latest
428 my @params = ('-t', 'snapshot', '-o', 'name', '-s', 'creation');
429 my $text = $class->zfs_request($scfg, undef, 'list', @params);
430 my @snapshots = split(/\n/, $text);
431 my $recentsnap = undef;
432 foreach (@snapshots) {
433 if (/$scfg->{pool}\/$volname/) {
434 s/^.*@//;
435 $recentsnap = $_;
436 }
437 }
438 if ($snap ne $recentsnap) {
439 die "cannot rollback, more recent snapshots exist\n";
440 }
441
442 $class->zfs_delete_lu($scfg, $volname);
443
444 $class->zfs_request($scfg, undef, 'rollback', "$scfg->{pool}/$volname\@$snap");
445
446 $class->zfs_import_lu($scfg, $volname);
447
448 $class->zfs_add_lun_mapping_entry($scfg, $volname);
449 }
450
451 sub volume_snapshot_delete {
452 my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
453
454 $class->zfs_request($scfg, undef, 'destroy', "$scfg->{pool}/$volname\@$snap");
455 }
456
457 sub volume_has_feature {
458 my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
459
460 my $features = {
461 snapshot => { current => 1, snap => 1},
462 clone => { base => 1},
463 template => { current => 1},
464 copy => { base => 1, current => 1},
465 };
466
467 my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
468 $class->parse_volname($volname);
469
470 my $key = undef;
471
472 if ($snapname) {
473 $key = 'snap';
474 } else {
475 $key = $isBase ? 'base' : 'current';
476 }
477
478 return 1 if $features->{$feature}->{$key};
479
480 return undef;
481 }
482
483 1;