]> git.proxmox.com Git - qemu-server.git/blob - PVE/QemuServer/Drive.pm
Create Drive.pm and move drive-related code there
[qemu-server.git] / PVE / QemuServer / Drive.pm
1 package PVE::QemuServer::Drive;
2
3 use strict;
4 use warnings;
5
6 use PVE::Storage;
7 use PVE::JSONSchema qw(get_standard_option);
8
9 use base qw(Exporter);
10
11 our @EXPORT_OK = qw(
12 is_valid_drivename
13 drive_is_cloudinit
14 drive_is_cdrom
15 parse_drive
16 print_drive
17 foreach_drive
18 foreach_volid
19 );
20
21 our $QEMU_FORMAT_RE = qr/raw|cow|qcow|qcow2|qed|vmdk|cloop/;
22
23 PVE::JSONSchema::register_standard_option('pve-qm-image-format', {
24 type => 'string',
25 enum => [qw(raw cow qcow qed qcow2 vmdk cloop)],
26 description => "The drive's backing file's data format.",
27 optional => 1,
28 });
29
30 my $MAX_IDE_DISKS = 4;
31 my $MAX_SCSI_DISKS = 31;
32 my $MAX_VIRTIO_DISKS = 16;
33 our $MAX_SATA_DISKS = 6;
34 our $MAX_UNUSED_DISKS = 256;
35
36 our $drivedesc_hash;
37
38 my %drivedesc_base = (
39 volume => { alias => 'file' },
40 file => {
41 type => 'string',
42 format => 'pve-volume-id-or-qm-path',
43 default_key => 1,
44 format_description => 'volume',
45 description => "The drive's backing volume.",
46 },
47 media => {
48 type => 'string',
49 enum => [qw(cdrom disk)],
50 description => "The drive's media type.",
51 default => 'disk',
52 optional => 1
53 },
54 cyls => {
55 type => 'integer',
56 description => "Force the drive's physical geometry to have a specific cylinder count.",
57 optional => 1
58 },
59 heads => {
60 type => 'integer',
61 description => "Force the drive's physical geometry to have a specific head count.",
62 optional => 1
63 },
64 secs => {
65 type => 'integer',
66 description => "Force the drive's physical geometry to have a specific sector count.",
67 optional => 1
68 },
69 trans => {
70 type => 'string',
71 enum => [qw(none lba auto)],
72 description => "Force disk geometry bios translation mode.",
73 optional => 1,
74 },
75 snapshot => {
76 type => 'boolean',
77 description => "Controls qemu's snapshot mode feature."
78 . " If activated, changes made to the disk are temporary and will"
79 . " be discarded when the VM is shutdown.",
80 optional => 1,
81 },
82 cache => {
83 type => 'string',
84 enum => [qw(none writethrough writeback unsafe directsync)],
85 description => "The drive's cache mode",
86 optional => 1,
87 },
88 format => get_standard_option('pve-qm-image-format'),
89 size => {
90 type => 'string',
91 format => 'disk-size',
92 format_description => 'DiskSize',
93 description => "Disk size. This is purely informational and has no effect.",
94 optional => 1,
95 },
96 backup => {
97 type => 'boolean',
98 description => "Whether the drive should be included when making backups.",
99 optional => 1,
100 },
101 replicate => {
102 type => 'boolean',
103 description => 'Whether the drive should considered for replication jobs.',
104 optional => 1,
105 default => 1,
106 },
107 rerror => {
108 type => 'string',
109 enum => [qw(ignore report stop)],
110 description => 'Read error action.',
111 optional => 1,
112 },
113 werror => {
114 type => 'string',
115 enum => [qw(enospc ignore report stop)],
116 description => 'Write error action.',
117 optional => 1,
118 },
119 aio => {
120 type => 'string',
121 enum => [qw(native threads)],
122 description => 'AIO type to use.',
123 optional => 1,
124 },
125 discard => {
126 type => 'string',
127 enum => [qw(ignore on)],
128 description => 'Controls whether to pass discard/trim requests to the underlying storage.',
129 optional => 1,
130 },
131 detect_zeroes => {
132 type => 'boolean',
133 description => 'Controls whether to detect and try to optimize writes of zeroes.',
134 optional => 1,
135 },
136 serial => {
137 type => 'string',
138 format => 'urlencoded',
139 format_description => 'serial',
140 maxLength => 20*3, # *3 since it's %xx url enoded
141 description => "The drive's reported serial number, url-encoded, up to 20 bytes long.",
142 optional => 1,
143 },
144 shared => {
145 type => 'boolean',
146 description => 'Mark this locally-managed volume as available on all nodes',
147 verbose_description => "Mark this locally-managed volume as available on all nodes.\n\nWARNING: This option does not share the volume automatically, it assumes it is shared already!",
148 optional => 1,
149 default => 0,
150 }
151 );
152
153 my %iothread_fmt = ( iothread => {
154 type => 'boolean',
155 description => "Whether to use iothreads for this drive",
156 optional => 1,
157 });
158
159 my %model_fmt = (
160 model => {
161 type => 'string',
162 format => 'urlencoded',
163 format_description => 'model',
164 maxLength => 40*3, # *3 since it's %xx url enoded
165 description => "The drive's reported model name, url-encoded, up to 40 bytes long.",
166 optional => 1,
167 },
168 );
169
170 my %queues_fmt = (
171 queues => {
172 type => 'integer',
173 description => "Number of queues.",
174 minimum => 2,
175 optional => 1
176 }
177 );
178
179 my %scsiblock_fmt = (
180 scsiblock => {
181 type => 'boolean',
182 description => "whether to use scsi-block for full passthrough of host block device\n\nWARNING: can lead to I/O errors in combination with low memory or high memory fragmentation on host",
183 optional => 1,
184 default => 0,
185 },
186 );
187
188 my %ssd_fmt = (
189 ssd => {
190 type => 'boolean',
191 description => "Whether to expose this drive as an SSD, rather than a rotational hard disk.",
192 optional => 1,
193 },
194 );
195
196 my %wwn_fmt = (
197 wwn => {
198 type => 'string',
199 pattern => qr/^(0x)[0-9a-fA-F]{16}/,
200 format_description => 'wwn',
201 description => "The drive's worldwide name, encoded as 16 bytes hex string, prefixed by '0x'.",
202 optional => 1,
203 },
204 );
205
206 my $add_throttle_desc = sub {
207 my ($key, $type, $what, $unit, $longunit, $minimum) = @_;
208 my $d = {
209 type => $type,
210 format_description => $unit,
211 description => "Maximum $what in $longunit.",
212 optional => 1,
213 };
214 $d->{minimum} = $minimum if defined($minimum);
215 $drivedesc_base{$key} = $d;
216 };
217 # throughput: (leaky bucket)
218 $add_throttle_desc->('bps', 'integer', 'r/w speed', 'bps', 'bytes per second');
219 $add_throttle_desc->('bps_rd', 'integer', 'read speed', 'bps', 'bytes per second');
220 $add_throttle_desc->('bps_wr', 'integer', 'write speed', 'bps', 'bytes per second');
221 $add_throttle_desc->('mbps', 'number', 'r/w speed', 'mbps', 'megabytes per second');
222 $add_throttle_desc->('mbps_rd', 'number', 'read speed', 'mbps', 'megabytes per second');
223 $add_throttle_desc->('mbps_wr', 'number', 'write speed', 'mbps', 'megabytes per second');
224 $add_throttle_desc->('iops', 'integer', 'r/w I/O', 'iops', 'operations per second');
225 $add_throttle_desc->('iops_rd', 'integer', 'read I/O', 'iops', 'operations per second');
226 $add_throttle_desc->('iops_wr', 'integer', 'write I/O', 'iops', 'operations per second');
227
228 # pools: (pool of IO before throttling starts taking effect)
229 $add_throttle_desc->('mbps_max', 'number', 'unthrottled r/w pool', 'mbps', 'megabytes per second');
230 $add_throttle_desc->('mbps_rd_max', 'number', 'unthrottled read pool', 'mbps', 'megabytes per second');
231 $add_throttle_desc->('mbps_wr_max', 'number', 'unthrottled write pool', 'mbps', 'megabytes per second');
232 $add_throttle_desc->('iops_max', 'integer', 'unthrottled r/w I/O pool', 'iops', 'operations per second');
233 $add_throttle_desc->('iops_rd_max', 'integer', 'unthrottled read I/O pool', 'iops', 'operations per second');
234 $add_throttle_desc->('iops_wr_max', 'integer', 'unthrottled write I/O pool', 'iops', 'operations per second');
235
236 # burst lengths
237 $add_throttle_desc->('bps_max_length', 'integer', 'length of I/O bursts', 'seconds', 'seconds', 1);
238 $add_throttle_desc->('bps_rd_max_length', 'integer', 'length of read I/O bursts', 'seconds', 'seconds', 1);
239 $add_throttle_desc->('bps_wr_max_length', 'integer', 'length of write I/O bursts', 'seconds', 'seconds', 1);
240 $add_throttle_desc->('iops_max_length', 'integer', 'length of I/O bursts', 'seconds', 'seconds', 1);
241 $add_throttle_desc->('iops_rd_max_length', 'integer', 'length of read I/O bursts', 'seconds', 'seconds', 1);
242 $add_throttle_desc->('iops_wr_max_length', 'integer', 'length of write I/O bursts', 'seconds', 'seconds', 1);
243
244 # legacy support
245 $drivedesc_base{'bps_rd_length'} = { alias => 'bps_rd_max_length' };
246 $drivedesc_base{'bps_wr_length'} = { alias => 'bps_wr_max_length' };
247 $drivedesc_base{'iops_rd_length'} = { alias => 'iops_rd_max_length' };
248 $drivedesc_base{'iops_wr_length'} = { alias => 'iops_wr_max_length' };
249
250 my $ide_fmt = {
251 %drivedesc_base,
252 %model_fmt,
253 %ssd_fmt,
254 %wwn_fmt,
255 };
256 PVE::JSONSchema::register_format("pve-qm-ide", $ide_fmt);
257
258 my $idedesc = {
259 optional => 1,
260 type => 'string', format => $ide_fmt,
261 description => "Use volume as IDE hard disk or CD-ROM (n is 0 to " .($MAX_IDE_DISKS -1) . ").",
262 };
263 PVE::JSONSchema::register_standard_option("pve-qm-ide", $idedesc);
264
265 my $scsi_fmt = {
266 %drivedesc_base,
267 %iothread_fmt,
268 %queues_fmt,
269 %scsiblock_fmt,
270 %ssd_fmt,
271 %wwn_fmt,
272 };
273 my $scsidesc = {
274 optional => 1,
275 type => 'string', format => $scsi_fmt,
276 description => "Use volume as SCSI hard disk or CD-ROM (n is 0 to " . ($MAX_SCSI_DISKS - 1) . ").",
277 };
278 PVE::JSONSchema::register_standard_option("pve-qm-scsi", $scsidesc);
279
280 my $sata_fmt = {
281 %drivedesc_base,
282 %ssd_fmt,
283 %wwn_fmt,
284 };
285 my $satadesc = {
286 optional => 1,
287 type => 'string', format => $sata_fmt,
288 description => "Use volume as SATA hard disk or CD-ROM (n is 0 to " . ($MAX_SATA_DISKS - 1). ").",
289 };
290 PVE::JSONSchema::register_standard_option("pve-qm-sata", $satadesc);
291
292 my $virtio_fmt = {
293 %drivedesc_base,
294 %iothread_fmt,
295 };
296 my $virtiodesc = {
297 optional => 1,
298 type => 'string', format => $virtio_fmt,
299 description => "Use volume as VIRTIO hard disk (n is 0 to " . ($MAX_VIRTIO_DISKS - 1) . ").",
300 };
301 PVE::JSONSchema::register_standard_option("pve-qm-virtio", $virtiodesc);
302
303 my $alldrive_fmt = {
304 %drivedesc_base,
305 %iothread_fmt,
306 %model_fmt,
307 %queues_fmt,
308 %scsiblock_fmt,
309 %ssd_fmt,
310 %wwn_fmt,
311 };
312
313 my $efidisk_fmt = {
314 volume => { alias => 'file' },
315 file => {
316 type => 'string',
317 format => 'pve-volume-id-or-qm-path',
318 default_key => 1,
319 format_description => 'volume',
320 description => "The drive's backing volume.",
321 },
322 format => get_standard_option('pve-qm-image-format'),
323 size => {
324 type => 'string',
325 format => 'disk-size',
326 format_description => 'DiskSize',
327 description => "Disk size. This is purely informational and has no effect.",
328 optional => 1,
329 },
330 };
331
332 my $efidisk_desc = {
333 optional => 1,
334 type => 'string', format => $efidisk_fmt,
335 description => "Configure a Disk for storing EFI vars",
336 };
337
338 PVE::JSONSchema::register_standard_option("pve-qm-efidisk", $efidisk_desc);
339
340 for (my $i = 0; $i < $MAX_IDE_DISKS; $i++) {
341 $drivedesc_hash->{"ide$i"} = $idedesc;
342 }
343
344 for (my $i = 0; $i < $MAX_SATA_DISKS; $i++) {
345 $drivedesc_hash->{"sata$i"} = $satadesc;
346 }
347
348 for (my $i = 0; $i < $MAX_SCSI_DISKS; $i++) {
349 $drivedesc_hash->{"scsi$i"} = $scsidesc;
350 }
351
352 for (my $i = 0; $i < $MAX_VIRTIO_DISKS; $i++) {
353 $drivedesc_hash->{"virtio$i"} = $virtiodesc;
354 }
355
356 $drivedesc_hash->{efidisk0} = $efidisk_desc;
357
358 our $unuseddesc = {
359 optional => 1,
360 type => 'string', format => 'pve-volume-id',
361 description => "Reference to unused volumes. This is used internally, and should not be modified manually.",
362 };
363
364 sub valid_drive_names {
365 # order is important - used to autoselect boot disk
366 return ((map { "ide$_" } (0 .. ($MAX_IDE_DISKS - 1))),
367 (map { "scsi$_" } (0 .. ($MAX_SCSI_DISKS - 1))),
368 (map { "virtio$_" } (0 .. ($MAX_VIRTIO_DISKS - 1))),
369 (map { "sata$_" } (0 .. ($MAX_SATA_DISKS - 1))),
370 'efidisk0');
371 }
372
373 sub is_valid_drivename {
374 my $dev = shift;
375
376 return defined($drivedesc_hash->{$dev});
377 }
378
379 PVE::JSONSchema::register_format('pve-qm-bootdisk', \&verify_bootdisk);
380 sub verify_bootdisk {
381 my ($value, $noerr) = @_;
382
383 return $value if is_valid_drivename($value);
384
385 return undef if $noerr;
386
387 die "invalid boot disk '$value'\n";
388 }
389
390 sub drive_is_cloudinit {
391 my ($drive) = @_;
392 return $drive->{file} =~ m@[:/]vm-\d+-cloudinit(?:\.$QEMU_FORMAT_RE)?$@;
393 }
394
395 sub drive_is_cdrom {
396 my ($drive, $exclude_cloudinit) = @_;
397
398 return 0 if $exclude_cloudinit && drive_is_cloudinit($drive);
399
400 return $drive && $drive->{media} && ($drive->{media} eq 'cdrom');
401 }
402
403 # ideX = [volume=]volume-id[,media=d][,cyls=c,heads=h,secs=s[,trans=t]]
404 # [,snapshot=on|off][,cache=on|off][,format=f][,backup=yes|no]
405 # [,rerror=ignore|report|stop][,werror=enospc|ignore|report|stop]
406 # [,aio=native|threads][,discard=ignore|on][,detect_zeroes=on|off]
407 # [,iothread=on][,serial=serial][,model=model]
408
409 sub parse_drive {
410 my ($key, $data) = @_;
411
412 my ($interface, $index);
413
414 if ($key =~ m/^([^\d]+)(\d+)$/) {
415 $interface = $1;
416 $index = $2;
417 } else {
418 return undef;
419 }
420
421 my $desc = $key =~ /^unused\d+$/ ? $alldrive_fmt
422 : $drivedesc_hash->{$key}->{format};
423 if (!$desc) {
424 warn "invalid drive key: $key\n";
425 return undef;
426 }
427 my $res = eval { PVE::JSONSchema::parse_property_string($desc, $data) };
428 return undef if !$res;
429 $res->{interface} = $interface;
430 $res->{index} = $index;
431
432 my $error = 0;
433 foreach my $opt (qw(bps bps_rd bps_wr)) {
434 if (my $bps = defined(delete $res->{$opt})) {
435 if (defined($res->{"m$opt"})) {
436 warn "both $opt and m$opt specified\n";
437 ++$error;
438 next;
439 }
440 $res->{"m$opt"} = sprintf("%.3f", $bps / (1024*1024.0));
441 }
442 }
443
444 # can't use the schema's 'requires' because of the mbps* => bps* "transforming aliases"
445 for my $requirement (
446 [mbps_max => 'mbps'],
447 [mbps_rd_max => 'mbps_rd'],
448 [mbps_wr_max => 'mbps_wr'],
449 [miops_max => 'miops'],
450 [miops_rd_max => 'miops_rd'],
451 [miops_wr_max => 'miops_wr'],
452 [bps_max_length => 'mbps_max'],
453 [bps_rd_max_length => 'mbps_rd_max'],
454 [bps_wr_max_length => 'mbps_wr_max'],
455 [iops_max_length => 'iops_max'],
456 [iops_rd_max_length => 'iops_rd_max'],
457 [iops_wr_max_length => 'iops_wr_max']) {
458 my ($option, $requires) = @$requirement;
459 if ($res->{$option} && !$res->{$requires}) {
460 warn "$option requires $requires\n";
461 ++$error;
462 }
463 }
464
465 return undef if $error;
466
467 return undef if $res->{mbps_rd} && $res->{mbps};
468 return undef if $res->{mbps_wr} && $res->{mbps};
469 return undef if $res->{iops_rd} && $res->{iops};
470 return undef if $res->{iops_wr} && $res->{iops};
471
472 if ($res->{media} && ($res->{media} eq 'cdrom')) {
473 return undef if $res->{snapshot} || $res->{trans} || $res->{format};
474 return undef if $res->{heads} || $res->{secs} || $res->{cyls};
475 return undef if $res->{interface} eq 'virtio';
476 }
477
478 if (my $size = $res->{size}) {
479 return undef if !defined($res->{size} = PVE::JSONSchema::parse_size($size));
480 }
481
482 return $res;
483 }
484
485 sub print_drive {
486 my ($drive) = @_;
487 my $skip = [ 'index', 'interface' ];
488 return PVE::JSONSchema::print_property_string($drive, $alldrive_fmt, $skip);
489 }
490
491 sub foreach_drive {
492 my ($conf, $func, @param) = @_;
493
494 foreach my $ds (valid_drive_names()) {
495 next if !defined($conf->{$ds});
496
497 my $drive = parse_drive($ds, $conf->{$ds});
498 next if !$drive;
499
500 &$func($ds, $drive, @param);
501 }
502 }
503
504 sub foreach_volid {
505 my ($conf, $func, @param) = @_;
506
507 my $volhash = {};
508
509 my $test_volid = sub {
510 my ($volid, $is_cdrom, $replicate, $shared, $snapname, $size) = @_;
511
512 return if !$volid;
513
514 $volhash->{$volid}->{cdrom} //= 1;
515 $volhash->{$volid}->{cdrom} = 0 if !$is_cdrom;
516
517 $volhash->{$volid}->{replicate} //= 0;
518 $volhash->{$volid}->{replicate} = 1 if $replicate;
519
520 $volhash->{$volid}->{shared} //= 0;
521 $volhash->{$volid}->{shared} = 1 if $shared;
522
523 $volhash->{$volid}->{referenced_in_config} //= 0;
524 $volhash->{$volid}->{referenced_in_config} = 1 if !defined($snapname);
525
526 $volhash->{$volid}->{referenced_in_snapshot}->{$snapname} = 1
527 if defined($snapname);
528 $volhash->{$volid}->{size} = $size if $size;
529 };
530
531 foreach_drive($conf, sub {
532 my ($ds, $drive) = @_;
533 $test_volid->($drive->{file}, drive_is_cdrom($drive), $drive->{replicate} // 1, $drive->{shared}, undef, $drive->{size});
534 });
535
536 foreach my $snapname (keys %{$conf->{snapshots}}) {
537 my $snap = $conf->{snapshots}->{$snapname};
538 $test_volid->($snap->{vmstate}, 0, 1, $snapname);
539 foreach_drive($snap, sub {
540 my ($ds, $drive) = @_;
541 $test_volid->($drive->{file}, drive_is_cdrom($drive), $drive->{replicate} // 1, $drive->{shared}, $snapname);
542 });
543 }
544
545 foreach my $volid (keys %$volhash) {
546 &$func($volid, $volhash->{$volid}, @param);
547 }
548 }
549
550 sub disksize {
551 my ($storecfg, $conf) = @_;
552
553 my $bootdisk = $conf->{bootdisk};
554 return undef if !$bootdisk;
555 return undef if !is_valid_drivename($bootdisk);
556
557 return undef if !$conf->{$bootdisk};
558
559 my $drive = parse_drive($bootdisk, $conf->{$bootdisk});
560 return undef if !defined($drive);
561
562 return undef if drive_is_cdrom($drive);
563
564 my $volid = $drive->{file};
565 return undef if !$volid;
566
567 return $drive->{size};
568 }
569
570 sub update_disksize {
571 my ($drive, $volid_hash) = @_;
572
573 my $volid = $drive->{file};
574 return undef if !defined($volid);
575
576 my $oldsize = $drive->{size};
577 my $newsize = $volid_hash->{$volid}->{size};
578
579 if (defined($newsize) && defined($oldsize) && $newsize != $oldsize) {
580 $drive->{size} = $newsize;
581
582 my $old_fmt = PVE::JSONSchema::format_size($oldsize);
583 my $new_fmt = PVE::JSONSchema::format_size($newsize);
584
585 return wantarray ? ($drive, $old_fmt, $new_fmt) : $drive;
586 }
587
588 return undef;
589 }
590
591 sub is_volume_in_use {
592 my ($storecfg, $conf, $skip_drive, $volid) = @_;
593
594 my $path = PVE::Storage::path($storecfg, $volid);
595
596 my $scan_config = sub {
597 my ($cref, $snapname) = @_;
598
599 foreach my $key (keys %$cref) {
600 my $value = $cref->{$key};
601 if (is_valid_drivename($key)) {
602 next if $skip_drive && $key eq $skip_drive;
603 my $drive = parse_drive($key, $value);
604 next if !$drive || !$drive->{file} || drive_is_cdrom($drive);
605 return 1 if $volid eq $drive->{file};
606 if ($drive->{file} =~ m!^/!) {
607 return 1 if $drive->{file} eq $path;
608 } else {
609 my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file}, 1);
610 next if !$storeid;
611 my $scfg = PVE::Storage::storage_config($storecfg, $storeid, 1);
612 next if !$scfg;
613 return 1 if $path eq PVE::Storage::path($storecfg, $drive->{file}, $snapname);
614 }
615 }
616 }
617
618 return 0;
619 };
620
621 return 1 if &$scan_config($conf);
622
623 undef $skip_drive;
624
625 foreach my $snapname (keys %{$conf->{snapshots}}) {
626 return 1 if &$scan_config($conf->{snapshots}->{$snapname}, $snapname);
627 }
628
629 return 0;
630 }
631
632 sub resolve_first_disk {
633 my $conf = shift;
634 my @disks = valid_drive_names();
635 my $firstdisk;
636 foreach my $ds (reverse @disks) {
637 next if !$conf->{$ds};
638 my $disk = parse_drive($ds, $conf->{$ds});
639 next if drive_is_cdrom($disk);
640 $firstdisk = $ds;
641 }
642 return $firstdisk;
643 }
644
645 1;