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