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