]> git.proxmox.com Git - qemu-server.git/blame - PVE/QemuServer/Drive.pm
Change format for unused drives
[qemu-server.git] / PVE / QemuServer / Drive.pm
CommitLineData
e0fd2b2f
FE
1package PVE::QemuServer::Drive;
2
3use strict;
4use warnings;
5
6use PVE::Storage;
7use PVE::JSONSchema qw(get_standard_option);
8
9use base qw(Exporter);
10
11our @EXPORT_OK = qw(
12is_valid_drivename
13drive_is_cloudinit
14drive_is_cdrom
15parse_drive
16print_drive
17foreach_drive
18foreach_volid
19);
20
21our $QEMU_FORMAT_RE = qr/raw|cow|qcow|qcow2|qed|vmdk|cloop/;
22
23PVE::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
30my $MAX_IDE_DISKS = 4;
31my $MAX_SCSI_DISKS = 31;
32my $MAX_VIRTIO_DISKS = 16;
33our $MAX_SATA_DISKS = 6;
34our $MAX_UNUSED_DISKS = 256;
35
36our $drivedesc_hash;
37
38my %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
153my %iothread_fmt = ( iothread => {
154 type => 'boolean',
155 description => "Whether to use iothreads for this drive",
156 optional => 1,
157});
158
159my %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
170my %queues_fmt = (
171 queues => {
172 type => 'integer',
173 description => "Number of queues.",
174 minimum => 2,
175 optional => 1
176 }
177);
178
179my %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
188my %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
196my %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
206my $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
250my $ide_fmt = {
251 %drivedesc_base,
252 %model_fmt,
253 %ssd_fmt,
254 %wwn_fmt,
255};
256PVE::JSONSchema::register_format("pve-qm-ide", $ide_fmt);
257
258my $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};
263PVE::JSONSchema::register_standard_option("pve-qm-ide", $idedesc);
264
265my $scsi_fmt = {
266 %drivedesc_base,
267 %iothread_fmt,
268 %queues_fmt,
269 %scsiblock_fmt,
270 %ssd_fmt,
271 %wwn_fmt,
272};
273my $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};
278PVE::JSONSchema::register_standard_option("pve-qm-scsi", $scsidesc);
279
280my $sata_fmt = {
281 %drivedesc_base,
282 %ssd_fmt,
283 %wwn_fmt,
284};
285my $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};
290PVE::JSONSchema::register_standard_option("pve-qm-sata", $satadesc);
291
292my $virtio_fmt = {
293 %drivedesc_base,
294 %iothread_fmt,
295};
296my $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};
301PVE::JSONSchema::register_standard_option("pve-qm-virtio", $virtiodesc);
302
303my $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
313my $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
332my $efidisk_desc = {
333 optional => 1,
334 type => 'string', format => $efidisk_fmt,
335 description => "Configure a Disk for storing EFI vars",
336};
337
338PVE::JSONSchema::register_standard_option("pve-qm-efidisk", $efidisk_desc);
339
340for (my $i = 0; $i < $MAX_IDE_DISKS; $i++) {
341 $drivedesc_hash->{"ide$i"} = $idedesc;
342}
343
344for (my $i = 0; $i < $MAX_SATA_DISKS; $i++) {
345 $drivedesc_hash->{"sata$i"} = $satadesc;
346}
347
348for (my $i = 0; $i < $MAX_SCSI_DISKS; $i++) {
349 $drivedesc_hash->{"scsi$i"} = $scsidesc;
350}
351
352for (my $i = 0; $i < $MAX_VIRTIO_DISKS; $i++) {
353 $drivedesc_hash->{"virtio$i"} = $virtiodesc;
354}
355
356$drivedesc_hash->{efidisk0} = $efidisk_desc;
357
758a08eb
FE
358my $unused_fmt = {
359 volume => { alias => 'file' },
360 file => {
361 type => 'string',
362 format => 'pve-volume-id',
363 default_key => 1,
364 format_description => 'volume',
365 description => "The drive's backing volume.",
366 },
367};
368
e0fd2b2f
FE
369our $unuseddesc = {
370 optional => 1,
758a08eb 371 type => 'string', format => $unused_fmt,
e0fd2b2f
FE
372 description => "Reference to unused volumes. This is used internally, and should not be modified manually.",
373};
374
375sub valid_drive_names {
376 # order is important - used to autoselect boot disk
377 return ((map { "ide$_" } (0 .. ($MAX_IDE_DISKS - 1))),
378 (map { "scsi$_" } (0 .. ($MAX_SCSI_DISKS - 1))),
379 (map { "virtio$_" } (0 .. ($MAX_VIRTIO_DISKS - 1))),
380 (map { "sata$_" } (0 .. ($MAX_SATA_DISKS - 1))),
381 'efidisk0');
382}
383
384sub is_valid_drivename {
385 my $dev = shift;
386
387 return defined($drivedesc_hash->{$dev});
388}
389
390PVE::JSONSchema::register_format('pve-qm-bootdisk', \&verify_bootdisk);
391sub verify_bootdisk {
392 my ($value, $noerr) = @_;
393
394 return $value if is_valid_drivename($value);
395
396 return undef if $noerr;
397
398 die "invalid boot disk '$value'\n";
399}
400
401sub drive_is_cloudinit {
402 my ($drive) = @_;
403 return $drive->{file} =~ m@[:/]vm-\d+-cloudinit(?:\.$QEMU_FORMAT_RE)?$@;
404}
405
406sub drive_is_cdrom {
407 my ($drive, $exclude_cloudinit) = @_;
408
409 return 0 if $exclude_cloudinit && drive_is_cloudinit($drive);
410
411 return $drive && $drive->{media} && ($drive->{media} eq 'cdrom');
412}
413
414# ideX = [volume=]volume-id[,media=d][,cyls=c,heads=h,secs=s[,trans=t]]
415# [,snapshot=on|off][,cache=on|off][,format=f][,backup=yes|no]
416# [,rerror=ignore|report|stop][,werror=enospc|ignore|report|stop]
417# [,aio=native|threads][,discard=ignore|on][,detect_zeroes=on|off]
418# [,iothread=on][,serial=serial][,model=model]
419
420sub parse_drive {
421 my ($key, $data) = @_;
422
423 my ($interface, $index);
424
425 if ($key =~ m/^([^\d]+)(\d+)$/) {
426 $interface = $1;
427 $index = $2;
428 } else {
429 return undef;
430 }
431
758a08eb 432 my $desc = $key =~ /^unused\d+$/ ? $unuseddesc->{format}
e0fd2b2f
FE
433 : $drivedesc_hash->{$key}->{format};
434 if (!$desc) {
435 warn "invalid drive key: $key\n";
436 return undef;
437 }
438 my $res = eval { PVE::JSONSchema::parse_property_string($desc, $data) };
439 return undef if !$res;
440 $res->{interface} = $interface;
441 $res->{index} = $index;
442
443 my $error = 0;
444 foreach my $opt (qw(bps bps_rd bps_wr)) {
445 if (my $bps = defined(delete $res->{$opt})) {
446 if (defined($res->{"m$opt"})) {
447 warn "both $opt and m$opt specified\n";
448 ++$error;
449 next;
450 }
451 $res->{"m$opt"} = sprintf("%.3f", $bps / (1024*1024.0));
452 }
453 }
454
455 # can't use the schema's 'requires' because of the mbps* => bps* "transforming aliases"
456 for my $requirement (
457 [mbps_max => 'mbps'],
458 [mbps_rd_max => 'mbps_rd'],
459 [mbps_wr_max => 'mbps_wr'],
460 [miops_max => 'miops'],
461 [miops_rd_max => 'miops_rd'],
462 [miops_wr_max => 'miops_wr'],
463 [bps_max_length => 'mbps_max'],
464 [bps_rd_max_length => 'mbps_rd_max'],
465 [bps_wr_max_length => 'mbps_wr_max'],
466 [iops_max_length => 'iops_max'],
467 [iops_rd_max_length => 'iops_rd_max'],
468 [iops_wr_max_length => 'iops_wr_max']) {
469 my ($option, $requires) = @$requirement;
470 if ($res->{$option} && !$res->{$requires}) {
471 warn "$option requires $requires\n";
472 ++$error;
473 }
474 }
475
476 return undef if $error;
477
478 return undef if $res->{mbps_rd} && $res->{mbps};
479 return undef if $res->{mbps_wr} && $res->{mbps};
480 return undef if $res->{iops_rd} && $res->{iops};
481 return undef if $res->{iops_wr} && $res->{iops};
482
483 if ($res->{media} && ($res->{media} eq 'cdrom')) {
484 return undef if $res->{snapshot} || $res->{trans} || $res->{format};
485 return undef if $res->{heads} || $res->{secs} || $res->{cyls};
486 return undef if $res->{interface} eq 'virtio';
487 }
488
489 if (my $size = $res->{size}) {
490 return undef if !defined($res->{size} = PVE::JSONSchema::parse_size($size));
491 }
492
493 return $res;
494}
495
496sub print_drive {
497 my ($drive) = @_;
498 my $skip = [ 'index', 'interface' ];
499 return PVE::JSONSchema::print_property_string($drive, $alldrive_fmt, $skip);
500}
501
502sub foreach_drive {
503 my ($conf, $func, @param) = @_;
504
505 foreach my $ds (valid_drive_names()) {
506 next if !defined($conf->{$ds});
507
508 my $drive = parse_drive($ds, $conf->{$ds});
509 next if !$drive;
510
511 &$func($ds, $drive, @param);
512 }
513}
514
515sub foreach_volid {
516 my ($conf, $func, @param) = @_;
517
518 my $volhash = {};
519
520 my $test_volid = sub {
521 my ($volid, $is_cdrom, $replicate, $shared, $snapname, $size) = @_;
522
523 return if !$volid;
524
525 $volhash->{$volid}->{cdrom} //= 1;
526 $volhash->{$volid}->{cdrom} = 0 if !$is_cdrom;
527
528 $volhash->{$volid}->{replicate} //= 0;
529 $volhash->{$volid}->{replicate} = 1 if $replicate;
530
531 $volhash->{$volid}->{shared} //= 0;
532 $volhash->{$volid}->{shared} = 1 if $shared;
533
534 $volhash->{$volid}->{referenced_in_config} //= 0;
535 $volhash->{$volid}->{referenced_in_config} = 1 if !defined($snapname);
536
537 $volhash->{$volid}->{referenced_in_snapshot}->{$snapname} = 1
538 if defined($snapname);
539 $volhash->{$volid}->{size} = $size if $size;
540 };
541
542 foreach_drive($conf, sub {
543 my ($ds, $drive) = @_;
544 $test_volid->($drive->{file}, drive_is_cdrom($drive), $drive->{replicate} // 1, $drive->{shared}, undef, $drive->{size});
545 });
546
547 foreach my $snapname (keys %{$conf->{snapshots}}) {
548 my $snap = $conf->{snapshots}->{$snapname};
549 $test_volid->($snap->{vmstate}, 0, 1, $snapname);
550 foreach_drive($snap, sub {
551 my ($ds, $drive) = @_;
552 $test_volid->($drive->{file}, drive_is_cdrom($drive), $drive->{replicate} // 1, $drive->{shared}, $snapname);
553 });
554 }
555
556 foreach my $volid (keys %$volhash) {
557 &$func($volid, $volhash->{$volid}, @param);
558 }
559}
560
776c5f50 561sub bootdisk_size {
e0fd2b2f
FE
562 my ($storecfg, $conf) = @_;
563
564 my $bootdisk = $conf->{bootdisk};
565 return undef if !$bootdisk;
566 return undef if !is_valid_drivename($bootdisk);
567
568 return undef if !$conf->{$bootdisk};
569
570 my $drive = parse_drive($bootdisk, $conf->{$bootdisk});
571 return undef if !defined($drive);
572
573 return undef if drive_is_cdrom($drive);
574
575 my $volid = $drive->{file};
576 return undef if !$volid;
577
578 return $drive->{size};
579}
580
581sub update_disksize {
582 my ($drive, $volid_hash) = @_;
583
584 my $volid = $drive->{file};
585 return undef if !defined($volid);
b2d27b32 586 return undef if !defined($volid_hash->{$volid}) || !defined($volid_hash->{$volid}->{size});
e0fd2b2f 587
63e313f3 588 my $oldsize = $drive->{size} // 0;
e0fd2b2f
FE
589 my $newsize = $volid_hash->{$volid}->{size};
590
63e313f3 591 if ($newsize != $oldsize) {
e0fd2b2f
FE
592 $drive->{size} = $newsize;
593
594 my $old_fmt = PVE::JSONSchema::format_size($oldsize);
595 my $new_fmt = PVE::JSONSchema::format_size($newsize);
596
597 return wantarray ? ($drive, $old_fmt, $new_fmt) : $drive;
598 }
599
600 return undef;
601}
602
603sub is_volume_in_use {
604 my ($storecfg, $conf, $skip_drive, $volid) = @_;
605
606 my $path = PVE::Storage::path($storecfg, $volid);
607
608 my $scan_config = sub {
609 my ($cref, $snapname) = @_;
610
611 foreach my $key (keys %$cref) {
612 my $value = $cref->{$key};
613 if (is_valid_drivename($key)) {
614 next if $skip_drive && $key eq $skip_drive;
615 my $drive = parse_drive($key, $value);
616 next if !$drive || !$drive->{file} || drive_is_cdrom($drive);
617 return 1 if $volid eq $drive->{file};
618 if ($drive->{file} =~ m!^/!) {
619 return 1 if $drive->{file} eq $path;
620 } else {
621 my ($storeid, $volname) = PVE::Storage::parse_volume_id($drive->{file}, 1);
622 next if !$storeid;
623 my $scfg = PVE::Storage::storage_config($storecfg, $storeid, 1);
624 next if !$scfg;
625 return 1 if $path eq PVE::Storage::path($storecfg, $drive->{file}, $snapname);
626 }
627 }
628 }
629
630 return 0;
631 };
632
633 return 1 if &$scan_config($conf);
634
635 undef $skip_drive;
636
637 foreach my $snapname (keys %{$conf->{snapshots}}) {
638 return 1 if &$scan_config($conf->{snapshots}->{$snapname}, $snapname);
639 }
640
641 return 0;
642}
643
644sub resolve_first_disk {
645 my $conf = shift;
646 my @disks = valid_drive_names();
647 my $firstdisk;
648 foreach my $ds (reverse @disks) {
649 next if !$conf->{$ds};
650 my $disk = parse_drive($ds, $conf->{$ds});
651 next if drive_is_cdrom($disk);
652 $firstdisk = $ds;
653 }
654 return $firstdisk;
655}
656
6571;